diff --git a/rio-window b/rio-window deleted file mode 160000 index 59c0b815..00000000 --- a/rio-window +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 59c0b815babfcd24f4509b1a51c01ff0e3dc6afd diff --git a/rio-window/Cargo.lock b/rio-window/Cargo.lock new file mode 100644 index 00000000..596b2a34 --- /dev/null +++ b/rio-window/Cargo.lock @@ -0,0 +1,2172 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ab_glyph" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79faae4620f45232f599d9bc7b290f88247a0834162c4495ab2f02d60004adfb" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.6.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "arrayref" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" + +[[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 = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.6.0", + "log", + "polling", + "rustix", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[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_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +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", + "core-graphics-types", + "foreign-types", + "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", + "libc", +] + +[[package]] +name = "core-text" +version = "20.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" +dependencies = [ + "core-foundation", + "core-graphics", + "foreign-types", + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crossfont" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44e28d120f3c9254800ea53349b09cbb45ac1f15f09215011a16241ae0289bc" +dependencies = [ + "cocoa", + "core-foundation", + "core-foundation-sys", + "core-graphics", + "core-text", + "dwrote", + "foreign-types", + "freetype-rs", + "libc", + "log", + "objc", + "once_cell", + "pkg-config", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "cstr" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68523903c8ae5aacfa32a0d9ae60cadeb764e1da14ee0d26b1f3089f13a54636" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "ctor-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" + +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" +dependencies = [ + "serde", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[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.1" +dependencies = [ + "mint", + "serde", +] + +[[package]] +name = "dwrote" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da3498378ed373237bdef1eddcc64e7be2d3ba4841f4c22a998e81cadeea83c" +dependencies = [ + "lazy_static", + "libc", + "serde", + "serde_derive", + "winapi", + "wio", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.0", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "freetype-rs" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5442dee36ca09604133580dc0553780e867936bb3cbef3275859e889026d2b17" +dependencies = [ + "bitflags 2.6.0", + "freetype-sys", + "libc", +] + +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "image" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +dependencies = [ + "bytemuck", + "byteorder-lite", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libredox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall 0.4.1", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mint" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror", +] + +[[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", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-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.6.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.6.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.6.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.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.6.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.6.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.6.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.6.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 = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "orbclient" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166" +dependencies = [ + "libredox", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owned_ttf_parser" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490d3a563d3122bf7c911a59b0add9389e5ec0f5f0c3ac6b91ff235a0e6a7f90" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.7.4", +] + +[[package]] +name = "polling" +version = "3.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rio-winit-fork" +version = "0.30.9" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.6.0", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "console_error_panic_hook", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "image", + "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", + "sctk-adwaita", + "serde", + "smithay-client-toolkit", + "smol_str", + "softbuffer", + "tracing", + "tracing-subscriber", + "tracing-web", + "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 = "rustix" +version = "0.38.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "crossfont", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "serde" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.6.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix", + "thiserror", + "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 = "softbuffer" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d623bff5d06f60d738990980d782c8c866997d9194cfe79ecad00aa2f76826dd" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "cfg_aliases", + "core-graphics", + "fastrand", + "foreign-types", + "js-sys", + "log", + "memmap2", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall 0.5.3", + "rustix", + "tiny-xlib", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.52.0", + "x11rb", +] + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[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-xlib" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d52f22673960ad13af14ff4025997312def1223bfa7c8e4949d099e6b3d5d1c" +dependencies = [ + "as-raw-xcb-connection", + "ctor-lite", + "libloading", + "pkg-config", + "tracing", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-web" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e6a141feebd51f8d91ebfd785af50fca223c570b86852166caa3b141defe7c" +dependencies = [ + "js-sys", + "tracing-core", + "tracing-subscriber", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "ttf-parser" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "wayland-backend" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" +dependencies = [ + "bitflags 2.6.0", + "rustix", + "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.6.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79f2d57c7fcc6ab4d602adba364bf59a5c24de57bd194486bf9b8360e06bfc4" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.6.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 = "yeslogic-fontconfig-sys" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb6b23999a8b1a997bf47c7bb4d19ad4029c3327bb3386ebe0a5ff584b33c7a" +dependencies = [ + "cstr", + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/rio-window/Cargo.toml b/rio-window/Cargo.toml new file mode 100644 index 00000000..3b2ae933 --- /dev/null +++ b/rio-window/Cargo.toml @@ -0,0 +1,293 @@ +[package] +name = "rio-window" +authors = [ + "The winit contributors", + "Pierre Krieger ", + "Raphael Amorim " +] +version.workspace = true +edition.workspace = true +license.workspace = true +keywords.workspace = true + +# Features are documented in either `lib.rs` or under `winit::platform`. +[features] +default = ["rwh_06", "x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] +x11 = ["x11-dl", "bytemuck", "percent-encoding", "xkbcommon-dl/x11", "x11rb"] +rwh_06 = ["dep:rwh_06"] +wayland = [ + "wayland-client", + "wayland-backend", + "wayland-protocols", + "wayland-protocols-plasma", + "sctk", + "ahash", + "memmap2", +] +wayland-dlopen = ["wayland-backend/dlopen"] +wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/ab_glyph"] +wayland-csd-adwaita-crossfont = ["sctk-adwaita", "sctk-adwaita/crossfont"] +wayland-csd-adwaita-notitle = ["sctk-adwaita"] + +[build-dependencies] +cfg_aliases = "0.2.0" + +[dependencies] +bitflags = { workspace = true } +cursor-icon = "1.1.0" +dpi = { version = "0.1.1" } +rwh_06 = { package = "raw-window-handle", version = "0.6", features = ["std"], optional = true } +smol_str = { workspace = true } +tracing = { version = "0.1.40", default-features = false } + +[dev-dependencies] +image = { version = "0.25.0", default-features = false, features = ["png"] } +tracing = { version = "0.1.40", default-features = false, features = ["log"] } +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } + +[target.'cfg(not(any(target_os = "ios")))'.dev-dependencies] +softbuffer = { version = "0.4.0", default-features = false, features = [ + "x11", + "x11-dlopen", + "wayland", + "wayland-dlopen", +] } + +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] +core-foundation = "0.9.3" +objc2 = "0.5.2" + +[target.'cfg(target_os = "macos")'.dependencies] +core-graphics = "0.23.1" + +[target.'cfg(target_os = "macos")'.dependencies.objc2-foundation] +version = "0.2.2" +features = [ + "dispatch", + "NSArray", + "NSAttributedString", + "NSData", + "NSDictionary", + "NSDistributedNotificationCenter", + "NSEnumerator", + "NSNotification", + "NSObjCRuntime", + "NSPathUtilities", + "NSProcessInfo", + "NSRunLoop", + "NSString", + "NSThread", + "NSValue", +] + +[target.'cfg(target_os = "macos")'.dependencies.objc2-app-kit] +version = "0.2.2" +features = [ + "NSAppearance", + "NSApplication", + "NSBitmapImageRep", + "NSButton", + "NSColor", + "NSControl", + "NSCursor", + "NSDragging", + "NSEvent", + "NSGraphics", + "NSGraphicsContext", + "NSImage", + "NSImageRep", + "NSMenu", + "NSMenuItem", + "NSOpenGLView", + "NSPasteboard", + "NSResponder", + "NSRunningApplication", + "NSScreen", + "NSTextInputClient", + "NSTextInputContext", + "NSView", + "NSWindow", + "NSWindowScripting", + "NSWindowTabGroup", +] + +[target.'cfg(target_os = "ios")'.dependencies.objc2-foundation] +version = "0.2.2" +features = [ + "dispatch", + "NSArray", + "NSEnumerator", + "NSGeometry", + "NSObjCRuntime", + "NSString", + "NSProcessInfo", + "NSThread", + "NSSet", +] + +[target.'cfg(target_os = "ios")'.dependencies.objc2-ui-kit] +version = "0.2.2" +features = [ + "UIApplication", + "UIDevice", + "UIEvent", + "UIGeometry", + "UIGestureRecognizer", + "UIOrientation", + "UIPanGestureRecognizer", + "UIPinchGestureRecognizer", + "UIResponder", + "UIRotationGestureRecognizer", + "UIScreen", + "UIScreenMode", + "UITapGestureRecognizer", + "UITouch", + "UITraitCollection", + "UIView", + "UIViewController", + "UIWindow", +] + +[target.'cfg(target_os = "windows")'.dependencies] +unicode-segmentation = "1.7.1" + +[target.'cfg(target_os = "windows")'.dependencies.windows-sys] +version = "0.52.0" +features = [ + "Win32_Devices_HumanInterfaceDevice", + "Win32_Foundation", + "Win32_Globalization", + "Win32_Graphics_Dwm", + "Win32_Graphics_Gdi", + "Win32_Media", + "Win32_System_Com_StructuredStorage", + "Win32_System_Com", + "Win32_System_LibraryLoader", + "Win32_System_Ole", + "Win32_System_SystemInformation", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_System_WindowsProgramming", + "Win32_UI_Accessibility", + "Win32_UI_Controls", + "Win32_UI_HiDpi", + "Win32_UI_Input_Ime", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_Input_Pointer", + "Win32_UI_Input_Touch", + "Win32_UI_Shell", + "Win32_UI_TextServices", + "Win32_UI_WindowsAndMessaging", +] + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "ios", target_os = "macos"))))'.dependencies] +ahash = { version = "0.8.7", features = ["no-rng"], optional = true } +bytemuck = { version = "1.13.1", default-features = false, optional = true } +calloop = "0.13.0" +libc = "0.2.64" +memmap2 = { version = "0.9.0", optional = true } +percent-encoding = { version = "2.0", optional = true } +rustix = { version = "0.38.4", default-features = false, features = [ + "std", + "system", + "thread", + "process", +] } +sctk = { package = "smithay-client-toolkit", version = "0.19.2", default-features = false, features = [ + "calloop", +], optional = true } +sctk-adwaita = { version = "0.10.1", default-features = false, optional = true } +wayland-backend = { version = "0.3.5", default-features = false, features = [ + "client_system", +], optional = true } +wayland-client = { version = "0.31.4", optional = true } +wayland-protocols = { version = "0.32.2", features = [ + "staging", +], optional = true } +wayland-protocols-plasma = { version = "0.3.2", features = [ + "client", +], optional = true } +x11-dl = { version = "2.19.1", optional = true } +x11rb = { version = "0.13.0", default-features = false, features = [ + "allow-unsafe-code", + "cursor", + "dl-libxcb", + "randr", + "resource_manager", + "xinput", + "xkb", +], optional = true } +xkbcommon-dl = "0.4.2" + +[target.'cfg(target_os = "redox")'.dependencies] +orbclient = { version = "0.3.47", default-features = false } +redox_syscall = "0.4.1" + +[target.'cfg(target_family = "wasm")'.dependencies.web_sys] +package = "web-sys" +version = "0.3.64" +features = [ + 'AbortController', + 'AbortSignal', + 'Blob', + 'console', + 'CssStyleDeclaration', + 'Document', + 'DomException', + 'DomRect', + 'DomRectReadOnly', + 'Element', + 'Event', + 'EventTarget', + 'FocusEvent', + 'HtmlCanvasElement', + 'HtmlElement', + 'HtmlImageElement', + 'ImageBitmap', + 'ImageBitmapOptions', + 'ImageBitmapRenderingContext', + 'ImageData', + 'IntersectionObserver', + 'IntersectionObserverEntry', + 'KeyboardEvent', + 'MediaQueryList', + 'MessageChannel', + 'MessagePort', + 'Node', + 'PageTransitionEvent', + 'PointerEvent', + 'PremultiplyAlpha', + 'ResizeObserver', + 'ResizeObserverBoxOptions', + 'ResizeObserverEntry', + 'ResizeObserverOptions', + 'ResizeObserverSize', + 'VisibilityState', + 'Window', + 'WheelEvent', + 'Url', +] + +[target.'cfg(target_family = "wasm")'.dependencies] +js-sys = "0.3.64" +pin-project = "1" +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +web-time = "1" + +[target.'cfg(all(target_family = "wasm", target_feature = "atomics"))'.dependencies] +atomic-waker = "1" +concurrent-queue = { version = "2", default-features = false } + +[target.'cfg(target_family = "wasm")'.dev-dependencies] +console_error_panic_hook = "0.1" +tracing-web = "0.1" + +[[example]] +doc-scrape-examples = true +name = "window" +required-features = ["rwh_06"] + +[[example]] +name = "child_window" +required-features = ["rwh_06"] \ No newline at end of file diff --git a/rio-window/README.md b/rio-window/README.md new file mode 100644 index 00000000..a48cf7b3 --- /dev/null +++ b/rio-window/README.md @@ -0,0 +1,3 @@ +# rio-window + +Rio window is a fork of Winit maintained only for Rio terminal. \ No newline at end of file diff --git a/rio-window/build.rs b/rio-window/build.rs new file mode 100644 index 00000000..034288e2 --- /dev/null +++ b/rio-window/build.rs @@ -0,0 +1,39 @@ +use cfg_aliases::cfg_aliases; + +fn main() { + // The script doesn't depend on our code + println!("cargo:rerun-if-changed=build.rs"); + + // Setup cfg aliases + cfg_aliases! { + // Systems. + android_platform: { target_os = "android" }, + web_platform: { all(target_family = "wasm", target_os = "unknown") }, + macos_platform: { target_os = "macos" }, + ios_platform: { target_os = "ios" }, + windows_platform: { target_os = "windows" }, + apple: { any(target_os = "ios", target_os = "macos") }, + free_unix: { all(unix, not(apple), not(android_platform), not(target_os = "emscripten")) }, + redox: { target_os = "redox" }, + + // Native displays. + x11_platform: { all(feature = "x11", free_unix, not(redox)) }, + wayland_platform: { all(feature = "wayland", free_unix, not(redox)) }, + orbital_platform: { redox }, + } + + println!("cargo:rustc-check-cfg=cfg(android_platform)"); + println!("cargo:rustc-check-cfg=cfg(web_platform)"); + println!("cargo:rustc-check-cfg=cfg(macos_platform)"); + println!("cargo:rustc-check-cfg=cfg(ios_platform)"); + println!("cargo:rustc-check-cfg=cfg(windows_platform)"); + println!("cargo:rustc-check-cfg=cfg(apple)"); + println!("cargo:rustc-check-cfg=cfg(free_unix)"); + println!("cargo:rustc-check-cfg=cfg(redox)"); + + println!("cargo:rustc-check-cfg=cfg(x11_platform)"); + println!("cargo:rustc-check-cfg=cfg(wayland_platform)"); + println!("cargo:rustc-check-cfg=cfg(orbital_platform)"); + + println!("cargo:rustc-check-cfg=cfg(unreleased_changelogs)"); +} diff --git a/rio-window/examples/child_window.rs b/rio-window/examples/child_window.rs new file mode 100644 index 00000000..ca22c39f --- /dev/null +++ b/rio-window/examples/child_window.rs @@ -0,0 +1,92 @@ +#[cfg(any(x11_platform, macos_platform, windows_platform))] +#[allow(deprecated)] +fn main() -> Result<(), impl std::error::Error> { + use std::collections::HashMap; + + use rio_window::dpi::{LogicalPosition, LogicalSize, Position}; + use rio_window::event::{ElementState, Event, KeyEvent, WindowEvent}; + use rio_window::event_loop::{ActiveEventLoop, EventLoop}; + use rio_window::raw_window_handle::HasRawWindowHandle; + use rio_window::window::Window; + + #[path = "util/fill.rs"] + mod fill; + + fn spawn_child_window(parent: &Window, event_loop: &ActiveEventLoop) -> Window { + let parent = parent.raw_window_handle().unwrap(); + let mut window_attributes = Window::default_attributes() + .with_title("child window") + .with_inner_size(LogicalSize::new(200.0f32, 200.0f32)) + .with_position(Position::Logical(LogicalPosition::new(0.0, 0.0))) + .with_visible(true); + // `with_parent_window` is unsafe. Parent window must be a valid window. + window_attributes = unsafe { window_attributes.with_parent_window(Some(parent)) }; + + event_loop.create_window(window_attributes).unwrap() + } + + let mut windows = HashMap::new(); + + let event_loop: EventLoop<()> = EventLoop::new().unwrap(); + let mut parent_window_id = None; + + event_loop.run(move |event: Event<()>, event_loop| { + match event { + Event::Resumed => { + let attributes = Window::default_attributes() + .with_title("parent window") + .with_position(Position::Logical(LogicalPosition::new(0.0, 0.0))) + .with_inner_size(LogicalSize::new(640.0f32, 480.0f32)); + let window = event_loop.create_window(attributes).unwrap(); + + parent_window_id = Some(window.id()); + + println!("Parent window id: {parent_window_id:?})"); + windows.insert(window.id(), window); + } + Event::WindowEvent { window_id, event } => match event { + WindowEvent::CloseRequested => { + windows.clear(); + event_loop.exit(); + } + WindowEvent::CursorEntered { device_id: _ } => { + // On x11, println when the cursor entered in a window even if the child window + // is created by some key inputs. + // the child windows are always placed at (0, 0) with size (200, 200) in the + // parent window, so we also can see this log when we move + // the cursor around (200, 200) in parent window. + println!("cursor entered in the window {window_id:?}"); + } + WindowEvent::KeyboardInput { + event: + KeyEvent { + state: ElementState::Pressed, + .. + }, + .. + } => { + let parent_window = windows.get(&parent_window_id.unwrap()).unwrap(); + let child_window = spawn_child_window(parent_window, event_loop); + let child_id = child_window.id(); + println!("Child window created with id: {child_id:?}"); + windows.insert(child_id, child_window); + } + WindowEvent::RedrawRequested => { + if let Some(window) = windows.get(&window_id) { + fill::fill_window(window); + } + } + _ => (), + }, + _ => (), + } + }) +} + +#[cfg(not(any(x11_platform, macos_platform, windows_platform)))] +fn main() { + panic!( + "This example is supported only on x11, macOS, and Windows, with the `rwh_06` feature \ + enabled." + ); +} diff --git a/rio-window/examples/control_flow.rs b/rio-window/examples/control_flow.rs new file mode 100644 index 00000000..4a4d5165 --- /dev/null +++ b/rio-window/examples/control_flow.rs @@ -0,0 +1,154 @@ +#![allow(clippy::single_match)] + +use std::thread; +#[cfg(not(web_platform))] +use std::time; + +use ::tracing::{info, warn}; +#[cfg(web_platform)] +use web_time as time; + +use rio_window::application::ApplicationHandler; +use rio_window::event::{ElementState, KeyEvent, StartCause, WindowEvent}; +use rio_window::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +use rio_window::keyboard::{Key, NamedKey}; +use rio_window::window::{Window, WindowId}; + +#[path = "util/fill.rs"] +mod fill; +#[path = "util/tracing.rs"] +mod tracing; + +const WAIT_TIME: time::Duration = time::Duration::from_millis(100); +const POLL_SLEEP_TIME: time::Duration = time::Duration::from_millis(100); + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +enum Mode { + #[default] + Wait, + WaitUntil, + Poll, +} + +fn main() -> Result<(), impl std::error::Error> { + #[cfg(web_platform)] + console_error_panic_hook::set_once(); + + tracing::init(); + + info!("Press '1' to switch to Wait mode."); + info!("Press '2' to switch to WaitUntil mode."); + info!("Press '3' to switch to Poll mode."); + info!("Press 'R' to toggle request_redraw() calls."); + info!("Press 'Esc' to close the window."); + + let event_loop = EventLoop::new().unwrap(); + + let mut app = ControlFlowDemo::default(); + event_loop.run_app(&mut app) +} + +#[derive(Default)] +struct ControlFlowDemo { + mode: Mode, + request_redraw: bool, + wait_cancelled: bool, + close_requested: bool, + window: Option, +} + +impl ApplicationHandler for ControlFlowDemo { + fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) { + info!("new_events: {cause:?}"); + + self.wait_cancelled = match cause { + StartCause::WaitCancelled { .. } => self.mode == Mode::WaitUntil, + _ => false, + } + } + + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attributes = Window::default_attributes().with_title( + "Press 1, 2, 3 to change control flow mode. Press R to toggle redraw requests.", + ); + self.window = Some(event_loop.create_window(window_attributes).unwrap()); + } + + fn window_event( + &mut self, + _event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + info!("{event:?}"); + + match event { + WindowEvent::CloseRequested => { + self.close_requested = true; + } + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: key, + state: ElementState::Pressed, + .. + }, + .. + } => match key.as_ref() { + // WARNING: Consider using `key_without_modifiers()` if available on your platform. + // See the `key_binding` example + Key::Character("1") => { + self.mode = Mode::Wait; + warn!("mode: {:?}", self.mode); + } + Key::Character("2") => { + self.mode = Mode::WaitUntil; + warn!("mode: {:?}", self.mode); + } + Key::Character("3") => { + self.mode = Mode::Poll; + warn!("mode: {:?}", self.mode); + } + Key::Character("r") => { + self.request_redraw = !self.request_redraw; + warn!("request_redraw: {}", self.request_redraw); + } + Key::Named(NamedKey::Escape) => { + self.close_requested = true; + } + _ => (), + }, + WindowEvent::RedrawRequested => { + let window = self.window.as_ref().unwrap(); + window.pre_present_notify(); + fill::fill_window(window); + } + _ => (), + } + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if self.request_redraw && !self.wait_cancelled && !self.close_requested { + self.window.as_ref().unwrap().request_redraw(); + } + + match self.mode { + Mode::Wait => event_loop.set_control_flow(ControlFlow::Wait), + Mode::WaitUntil => { + if !self.wait_cancelled { + event_loop.set_control_flow(ControlFlow::WaitUntil( + time::Instant::now() + WAIT_TIME, + )); + } + } + Mode::Poll => { + thread::sleep(POLL_SLEEP_TIME); + event_loop.set_control_flow(ControlFlow::Poll); + } + }; + + if self.close_requested { + event_loop.exit(); + } + } +} diff --git a/rio-window/examples/data/cross.png b/rio-window/examples/data/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..9bfdf369b3ba297216a2859beda741508d02a43d GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7(mY)pLo7}wCmdk@XKe82 z|M>$S28z})If-$*=9Y>$GI&Uew1VS-2@BYG9Ta-h#CJb@VEDrzB|%d=CC)1M>aI||_qs^|SL&Bsy0~<^ h)sJf;`W5R-Im(oHYD=ftF)%PNc)I$ztaD0e0su+aD)ayV literal 0 HcmV?d00001 diff --git a/rio-window/examples/data/gradient.png b/rio-window/examples/data/gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..41ce610e66221b0803d20b0dcca6f9215745ac3b GIT binary patch literal 229 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7)_A%&hFF}QJLxXhAp;&a z|5wlEJZe?wQ0RES@q4|_5zm+jv+^NVnLJQc0UxtOgk}KeJ}Z(*Gq7 iy3edk&Rc6(#mL^SzT-}mfC2*p1B0ilpUXO@geCy&om(~l literal 0 HcmV?d00001 diff --git a/rio-window/examples/data/icon.png b/rio-window/examples/data/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..aa3fbf3cfc18a3723cca4de446cf6f6079216b50 GIT binary patch literal 2950 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}Y)RhkE)4%caKYZ?lNlHo zI14-?iy0WWg+Z8+Vb&Z81_lQ95>H=O_D3wz0)lMLG0sXLIZqeIkch**(<@6tukJl= zUp@a}pQNXggbmy@onTumo@95`B9R3&W@E(v%u;0Lb!z*+_-%iB{35Wmo ztzlu!iivfcA*><`INDA%ygU%Fz?{LW!RxIavkbS)Y!QL_SNB%*?f%2`BY%oe!>@7% z&6JQZZWGNJ@s9!|KPk`bN}AV@7o?Hn-mtWk!z}lbuXOMYlX=$6mUs0OOqri9iB0E~ zz91mO=D@*P)6Tl)%Dxp}x{leiiZR*-Fr{+mxKEfZ_Wazsvpi>dE?t&i-hEx^xO!#c zQLDJbsNl6Hs!gKkMu58T9%!FMB0$mOVn!a-Xd}Q66%+C_v zv!(xNql;Fb9MiR6(G~lZ!eYLb?vR`2|HgtR?0i5+XvVI_g2(;yA5V7`l#>%tj*nAI z;uPw0FfVz;*?qPC?+oVn`H$M>#a4WJ$XWB@yTE1s-o|&e2R2SRkd~kt+W3{>PucPV zP9I}Dxf~2sf*jjseEz8IwX3e}Ve>JmcG>(BdcM^suie_Qy@o}!-$zZbTXCs^P;c@3 zEviq%`JTP2KK1j<v_ ztJ{P#eb+w1yRg^mWMKx|(j) zo6^2&Ur(yIr_}ehZu;N7avS<~cJob`c9-|~t3Ub^&E~B-z{YU0SXI`2ue0=2gX5Jk zPJ3;aO{?+Y*mKhXWJn7H6y&3&30!r+izB+R0S679~*j7!sbt`I* zoqcAVx$g8^48IhPs3kCOXiUh^`5?Z4;f6!%is>nND_9N&OXSU#T^(<+DbxAUmy=>_ zU*Dgaoxk_+hD7I=_x4)1wzis>ZWqd2tE_J>eLK)+c8&DqW%e$WKUuh{zbx6{u5Xre z=g^6VwieefaDCYOT5F|SXhQL-hPg~Cldh{aJ!RN&>EGnN@Aq6wdU!MJuW9K1$_f74 z_nh7&{{O_A<@JZZcju>G7q_mgc_y2Zw&bE*r*ZPDxl>q+B6p?O{hV98a=rE8DId*^ zpWeH@Vs-8jNs!#T_zXuuq ziKW4tA#ZytFh7B9OpwLvX(%>%K*9W@tkKM^^% zc+-mruQsp9C#p>g)m6K`hOmBHZugAeMly7lS) z`_G*;Tc; z@+KEl^JpGeoRsogBy;h*#{t=lp_%f*56V7PeOG+&E%BQC{HZVO)5Ml-pHxv{@W9t{ zaa{V*BieuS9V^|~gf>i3S}5Ytc>T)9h{Bx9N3QS+Z9BZH_$-I!37_vmx!I3@9=_{& zW&WbS50;?u z@k^|FugjdiM)zfM3q_W1;^LIf>xfBP;;zJBclb)==^LBB8}!#sI=7kq*XAErW9PiJ zne}Sn_l;E_YUiALckqa9{-eIF4^N$N=2&^LU{l+db^Za7ca4{R={5Rl^wn&?{Ohca zGM$w&Lc2UuZ!-P6_g?OvtVw6+;bk8`?_<8X^ZKLX{?qS0yzHHqZM$u5!)Nyzmsw`YPv1=GuvU5eT;#PuNyl|>rKNwTE^Tor z&Ut6|iQjAA&pH0p(>HBAA5mw&ZPMrcRkMFr@ib+$e46DyBTvI<=>efhD_*La*G%mG z7o#(MKBG9F(Z00yMgAv!CiZMyIP-I5)pmwuuYYWof6r^zKjr=g)O7r5|F9p&!abrnhSO318TVnn5z4JIm3u8+-*zB%92S{^^R$QyX2 zOzzDIg>5U>X>4V=v37F(O6%H!r5OanGIcP|5b5SCFwp$oBaw-W{A2lqRj_Q~8f^QlW#+60Hs4Nzwhv zCSv`SZdaC;E81S`+My@K!ezqjo^j_wh_l}g!TEES{9k(ciP`+8>zeD64=($e;;Jur z)%M1$Hm3Fwt8T@_k;TOX51q}CiJbmEN=`=wu|EQRh;Nfb~9YRd*9(PVk%-Zx* zYrluC^v@N=nwuSjH-1ptY7kJi)S0X9yMIL*hmu6pqLR;mnIQYyaff`EpM;>1NBJ~?NgW0j6d*U!quST{C#ytPRx=`ai3KfUFc)osI0PW!b;C@#i>qKW_=S> zN+x$aeCV?LUd|iyKKi zn7iWQnkU}hinr_UKW1KDBJ$I<@pZ?^tvh{#M0XsjJFVi!TF}0Dw~5JS=WOk^X+4WI z+1)vG&N3Wkc4AiN46|8x^P2lRm*1;jK76*eRJ;CGTTn-W$BNdaue84Z5i{P^rFW&U z=q~HCmOUQFlq9&47@W#nJ9@6{=UX8*rC4q$AH)3Qjjcx;vqN>4oTz+WD;59mWVz6X b|EvWo*A_jP#H`A|z`)??>gTe~DWM4fDe85e literal 0 HcmV?d00001 diff --git a/rio-window/examples/pump_events.rs b/rio-window/examples/pump_events.rs new file mode 100644 index 00000000..0473b924 --- /dev/null +++ b/rio-window/examples/pump_events.rs @@ -0,0 +1,87 @@ +#![allow(clippy::single_match)] + +// Limit this example to only compatible platforms. +#[cfg(any( + windows_platform, + macos_platform, + x11_platform, + wayland_platform, + android_platform, +))] +fn main() -> std::process::ExitCode { + use std::process::ExitCode; + use std::thread::sleep; + use std::time::Duration; + + use rio_window::application::ApplicationHandler; + use rio_window::event::WindowEvent; + use rio_window::event_loop::{ActiveEventLoop, EventLoop}; + use rio_window::platform::pump_events::{EventLoopExtPumpEvents, PumpStatus}; + use rio_window::window::{Window, WindowId}; + + #[path = "util/fill.rs"] + mod fill; + + #[derive(Default)] + struct PumpDemo { + window: Option, + } + + impl ApplicationHandler for PumpDemo { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attributes = + Window::default_attributes().with_title("A fantastic window!"); + self.window = Some(event_loop.create_window(window_attributes).unwrap()); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + println!("{event:?}"); + + let window = match self.window.as_ref() { + Some(window) => window, + None => return, + }; + + match event { + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::RedrawRequested => { + fill::fill_window(window); + window.request_redraw(); + } + _ => (), + } + } + } + + let mut event_loop = EventLoop::new().unwrap(); + + tracing_subscriber::fmt::init(); + + let mut app = PumpDemo::default(); + + loop { + let timeout = Some(Duration::ZERO); + let status = event_loop.pump_app_events(timeout, &mut app); + + if let PumpStatus::Exit(exit_code) = status { + break ExitCode::from(exit_code as u8); + } + + // Sleep for 1/60 second to simulate application work + // + // Since `pump_events` doesn't block it will be important to + // throttle the loop in the app somehow. + println!("Update()"); + sleep(Duration::from_millis(16)); + } +} + +#[cfg(any(ios_platform, web_platform, orbital_platform))] +fn main() { + println!("This platform doesn't support pump_events."); +} diff --git a/rio-window/examples/run_on_demand.rs b/rio-window/examples/run_on_demand.rs new file mode 100644 index 00000000..4876de90 --- /dev/null +++ b/rio-window/examples/run_on_demand.rs @@ -0,0 +1,108 @@ +#![allow(clippy::single_match)] + +// Limit this example to only compatible platforms. +#[cfg(any(windows_platform, macos_platform, x11_platform, wayland_platform,))] +fn main() -> Result<(), Box> { + use std::time::Duration; + + use rio_window::application::ApplicationHandler; + use rio_window::event::WindowEvent; + use rio_window::event_loop::{ActiveEventLoop, EventLoop}; + use rio_window::platform::run_on_demand::EventLoopExtRunOnDemand; + use rio_window::window::{Window, WindowId}; + + #[path = "util/fill.rs"] + mod fill; + + #[derive(Default)] + struct App { + idx: usize, + window_id: Option, + window: Option, + } + + impl ApplicationHandler for App { + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + if let Some(window) = self.window.as_ref() { + window.request_redraw(); + } + } + + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attributes = Window::default_attributes() + .with_title("Fantastic window number one!") + .with_inner_size(rio_window::dpi::LogicalSize::new(128.0, 128.0)); + let window = event_loop.create_window(window_attributes).unwrap(); + self.window_id = Some(window.id()); + self.window = Some(window); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + if event == WindowEvent::Destroyed && self.window_id == Some(window_id) { + println!( + "--------------------------------------------------------- Window {} Destroyed", + self.idx + ); + self.window_id = None; + event_loop.exit(); + return; + } + + let window = match self.window.as_mut() { + Some(window) => window, + None => return, + }; + + match event { + WindowEvent::CloseRequested => { + println!( + "--------------------------------------------------------- Window {} \ + CloseRequested", + self.idx + ); + fill::cleanup_window(window); + self.window = None; + } + WindowEvent::RedrawRequested => { + fill::fill_window(window); + } + _ => (), + } + } + } + + tracing_subscriber::fmt::init(); + + let mut event_loop = EventLoop::new().unwrap(); + + let mut app = App { + idx: 1, + ..Default::default() + }; + event_loop.run_app_on_demand(&mut app)?; + + println!( + "--------------------------------------------------------- Finished first loop" + ); + println!( + "--------------------------------------------------------- Waiting 5 seconds" + ); + std::thread::sleep(Duration::from_secs(5)); + + app.idx += 1; + event_loop.run_app_on_demand(&mut app)?; + println!( + "--------------------------------------------------------- Finished second loop" + ); + Ok(()) +} + +#[cfg(not(any(windows_platform, macos_platform, x11_platform, wayland_platform,)))] +fn main() { + println!("This example is not supported on this platform"); +} diff --git a/rio-window/examples/util/fill.rs b/rio-window/examples/util/fill.rs new file mode 100644 index 00000000..6e5a45cf --- /dev/null +++ b/rio-window/examples/util/fill.rs @@ -0,0 +1,126 @@ +//! Fill the window buffer with a solid color. +//! +//! Launching a window without drawing to it has unpredictable results varying from platform to +//! platform. In order to have well-defined examples, this module provides an easy way to +//! fill the window buffer with a solid color. +//! +//! The `softbuffer` crate is used, largely because of its ease of use. `glutin` or `wgpu` could +//! also be used to fill the window buffer, but they are more complicated to use. + +#[allow(unused_imports)] +pub use platform::cleanup_window; +pub use platform::fill_window; + +#[cfg(all(feature = "rwh_06", not(any(target_os = "android", target_os = "ios"))))] +mod platform { + use std::cell::RefCell; + use std::collections::HashMap; + use std::mem; + use std::mem::ManuallyDrop; + use std::num::NonZeroU32; + + use rio_window::window::{Window, WindowId}; + use softbuffer::{Context, Surface}; + + thread_local! { + // NOTE: You should never do things like that, create context and drop it before + // you drop the event loop. We do this for brevity to not blow up examples. We use + // ManuallyDrop to prevent destructors from running. + // + // A static, thread-local map of graphics contexts to open windows. + static GC: ManuallyDrop>> = const { ManuallyDrop::new(RefCell::new(None)) }; + } + + /// The graphics context used to draw to a window. + struct GraphicsContext { + /// The global softbuffer context. + context: RefCell>, + + /// The hash map of window IDs to surfaces. + surfaces: HashMap>, + } + + impl GraphicsContext { + fn new(w: &Window) -> Self { + Self { + context: RefCell::new( + Context::new(unsafe { + mem::transmute::<&'_ Window, &'static Window>(w) + }) + .expect("Failed to create a softbuffer context"), + ), + surfaces: HashMap::new(), + } + } + + fn create_surface( + &mut self, + window: &Window, + ) -> &mut Surface<&'static Window, &'static Window> { + self.surfaces.entry(window.id()).or_insert_with(|| { + Surface::new(&self.context.borrow(), unsafe { + mem::transmute::<&'_ Window, &'static Window>(window) + }) + .expect("Failed to create a softbuffer surface") + }) + } + + fn destroy_surface(&mut self, window: &Window) { + self.surfaces.remove(&window.id()); + } + } + + pub fn fill_window(window: &Window) { + GC.with(|gc| { + let size = window.inner_size(); + let (Some(width), Some(height)) = + (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + else { + return; + }; + + // Either get the last context used or create a new one. + let mut gc = gc.borrow_mut(); + let surface = gc + .get_or_insert_with(|| GraphicsContext::new(window)) + .create_surface(window); + + // Fill a buffer with a solid color. + const DARK_GRAY: u32 = 0xff181818; + + surface + .resize(width, height) + .expect("Failed to resize the softbuffer surface"); + + let mut buffer = surface + .buffer_mut() + .expect("Failed to get the softbuffer buffer"); + buffer.fill(DARK_GRAY); + buffer + .present() + .expect("Failed to present the softbuffer buffer"); + }) + } + + #[allow(dead_code)] + pub fn cleanup_window(window: &Window) { + GC.with(|gc| { + let mut gc = gc.borrow_mut(); + if let Some(context) = gc.as_mut() { + context.destroy_surface(window); + } + }); + } +} + +#[cfg(not(all(feature = "rwh_06", not(any(target_os = "android", target_os = "ios")))))] +mod platform { + pub fn fill_window(_window: &rio_window::window::Window) { + // No-op on mobile platforms. + } + + #[allow(dead_code)] + pub fn cleanup_window(_window: &rio_window::window::Window) { + // No-op on mobile platforms. + } +} diff --git a/rio-window/examples/util/tracing.rs b/rio-window/examples/util/tracing.rs new file mode 100644 index 00000000..f5c8f374 --- /dev/null +++ b/rio-window/examples/util/tracing.rs @@ -0,0 +1,27 @@ +#[cfg(not(web_platform))] +pub fn init() { + use tracing_subscriber::filter::{EnvFilter, LevelFilter}; + + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .init(); +} + +#[cfg(web_platform)] +pub fn init() { + use tracing_subscriber::layer::SubscriberExt; + use tracing_subscriber::util::SubscriberInitExt; + + tracing_subscriber::registry() + .with( + tracing_subscriber::fmt::layer() + .with_ansi(false) + .without_time() + .with_writer(tracing_web::MakeWebConsoleWriter::new()), + ) + .init(); +} diff --git a/rio-window/examples/window.rs b/rio-window/examples/window.rs new file mode 100644 index 00000000..818200ea --- /dev/null +++ b/rio-window/examples/window.rs @@ -0,0 +1,1130 @@ +//! Simple winit application. + +use std::collections::HashMap; +use std::error::Error; +use std::fmt::Debug; +#[cfg(not(any(android_platform, ios_platform)))] +use std::num::NonZeroU32; +use std::sync::Arc; +use std::{fmt, mem}; + +use ::tracing::{error, info}; +use cursor_icon::CursorIcon; +#[cfg(not(any(android_platform, ios_platform)))] +use rwh_06::{DisplayHandle, HasDisplayHandle}; +#[cfg(not(any(android_platform, ios_platform)))] +use softbuffer::{Context, Surface}; + +use rio_window::application::ApplicationHandler; +use rio_window::dpi::{LogicalSize, PhysicalPosition, PhysicalSize}; +use rio_window::event::{ + DeviceEvent, DeviceId, Ime, MouseButton, MouseScrollDelta, WindowEvent, +}; +use rio_window::event_loop::{ActiveEventLoop, EventLoop}; +use rio_window::keyboard::{Key, ModifiersState}; +use rio_window::window::{ + Cursor, CursorGrabMode, CustomCursor, CustomCursorSource, Fullscreen, Icon, + ResizeDirection, Theme, Window, WindowId, +}; + +#[cfg(macos_platform)] +use rio_window::platform::macos::{ + OptionAsAlt, WindowAttributesExtMacOS, WindowExtMacOS, +}; +#[cfg(any(x11_platform, wayland_platform))] +use rio_window::platform::startup_notify::{ + self, EventLoopExtStartupNotify, WindowAttributesExtStartupNotify, + WindowExtStartupNotify, +}; + +#[path = "util/tracing.rs"] +mod tracing; + +/// The amount of points to around the window for drag resize direction calculations. +const BORDER_SIZE: f64 = 20.; + +fn main() -> Result<(), Box> { + #[cfg(web_platform)] + console_error_panic_hook::set_once(); + + tracing::init(); + + let event_loop = EventLoop::::with_user_event().build()?; + let _event_loop_proxy = event_loop.create_proxy(); + + // Wire the user event from another thread. + #[cfg(not(web_platform))] + std::thread::spawn(move || { + // Wake up the `event_loop` once every second and dispatch a custom event + // from a different thread. + info!("Starting to send user event every second"); + loop { + let _ = _event_loop_proxy.send_event(UserEvent::WakeUp); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + }); + + let mut state = Application::new(&event_loop); + + event_loop.run_app(&mut state).map_err(Into::into) +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy)] +enum UserEvent { + WakeUp, +} + +/// Application state and event handling. +struct Application { + /// Custom cursors assets. + custom_cursors: Vec, + /// Application icon. + icon: Icon, + windows: HashMap, + /// Drawing context. + /// + /// With OpenGL it could be EGLDisplay. + #[cfg(not(any(android_platform, ios_platform)))] + context: Option>>, +} + +impl Application { + fn new(event_loop: &EventLoop) -> Self { + // SAFETY: we drop the context right before the event loop is stopped, thus making it safe. + #[cfg(not(any(android_platform, ios_platform)))] + let context = Some( + Context::new(unsafe { + std::mem::transmute::, DisplayHandle<'static>>( + event_loop.display_handle().unwrap(), + ) + }) + .unwrap(), + ); + + // You'll have to choose an icon size at your own discretion. On X11, the desired size + // varies by WM, and on Windows, you still have to account for screen scaling. Here + // we use 32px, since it seems to work well enough in most cases. Be careful about + // going too high, or you'll be bitten by the low-quality downscaling built into the + // WM. + let icon = load_icon(include_bytes!("data/icon.png")); + + info!("Loading cursor assets"); + let custom_cursors = vec![ + event_loop + .create_custom_cursor(decode_cursor(include_bytes!("data/cross.png"))), + event_loop + .create_custom_cursor(decode_cursor(include_bytes!("data/cross2.png"))), + event_loop + .create_custom_cursor(decode_cursor(include_bytes!("data/gradient.png"))), + ]; + + Self { + #[cfg(not(any(android_platform, ios_platform)))] + context, + custom_cursors, + icon, + windows: Default::default(), + } + } + + fn create_window( + &mut self, + event_loop: &ActiveEventLoop, + _tab_id: Option, + ) -> Result> { + // TODO read-out activation token. + + #[allow(unused_mut)] + let mut window_attributes = Window::default_attributes() + .with_title("Winit window") + .with_transparent(true) + .with_window_icon(Some(self.icon.clone())); + + #[cfg(any(x11_platform, wayland_platform))] + if let Some(token) = event_loop.read_token_from_env() { + startup_notify::reset_activation_token_env(); + info!("Using token {:?} to activate a window", token); + window_attributes = window_attributes.with_activation_token(token); + } + + #[cfg(macos_platform)] + if let Some(tab_id) = _tab_id { + window_attributes = window_attributes.with_tabbing_identifier(&tab_id); + } + + #[cfg(web_platform)] + { + use rio_window::platform::web::WindowAttributesExtWebSys; + window_attributes = window_attributes.with_append(true); + } + + let window = event_loop.create_window(window_attributes)?; + + #[cfg(ios_platform)] + { + use rio_window::platform::ios::WindowExtIOS; + window.recognize_doubletap_gesture(true); + window.recognize_pinch_gesture(true); + window.recognize_rotation_gesture(true); + window.recognize_pan_gesture(true, 2, 2); + } + + let window_state = WindowState::new(self, window)?; + let window_id = window_state.window.id(); + info!("Created new window with id={window_id:?}"); + self.windows.insert(window_id, window_state); + Ok(window_id) + } + + fn handle_action( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + action: Action, + ) { + // let cursor_position = self.cursor_position; + let window = self.windows.get_mut(&window_id).unwrap(); + info!("Executing action: {action:?}"); + match action { + Action::CloseWindow => { + let _ = self.windows.remove(&window_id); + } + Action::CreateNewWindow => { + #[cfg(any(x11_platform, wayland_platform))] + if let Err(err) = window.window.request_activation_token() { + info!("Failed to get activation token: {err}"); + } else { + return; + } + + if let Err(err) = self.create_window(event_loop, None) { + error!("Error creating new window: {err}"); + } + } + Action::ToggleResizeIncrements => window.toggle_resize_increments(), + Action::ToggleCursorVisibility => window.toggle_cursor_visibility(), + Action::ToggleResizable => window.toggle_resizable(), + Action::ToggleDecorations => window.toggle_decorations(), + Action::ToggleFullscreen => window.toggle_fullscreen(), + Action::ToggleMaximize => window.toggle_maximize(), + Action::ToggleImeInput => window.toggle_ime(), + Action::Minimize => window.minimize(), + Action::NextCursor => window.next_cursor(), + Action::NextCustomCursor => window.next_custom_cursor(&self.custom_cursors), + #[cfg(web_platform)] + Action::UrlCustomCursor => window.url_custom_cursor(event_loop), + #[cfg(web_platform)] + Action::AnimationCustomCursor => { + window.animation_custom_cursor(event_loop, &self.custom_cursors) + } + Action::CycleCursorGrab => window.cycle_cursor_grab(), + Action::DragWindow => window.drag_window(), + Action::DragResizeWindow => window.drag_resize_window(), + Action::ShowWindowMenu => window.show_menu(), + Action::PrintHelp => self.print_help(), + #[cfg(macos_platform)] + Action::CycleOptionAsAlt => window.cycle_option_as_alt(), + #[cfg(macos_platform)] + Action::CreateNewTab => { + let tab_id = window.window.tabbing_identifier(); + if let Err(err) = self.create_window(event_loop, Some(tab_id)) { + error!("Error creating new window: {err}"); + } + } + Action::RequestResize => window.swap_dimensions(), + } + } + + fn dump_monitors(&self, event_loop: &ActiveEventLoop) { + info!("Monitors information"); + let primary_monitor = event_loop.primary_monitor(); + for monitor in event_loop.available_monitors() { + let intro = if primary_monitor.as_ref() == Some(&monitor) { + "Primary monitor" + } else { + "Monitor" + }; + + if let Some(name) = monitor.name() { + info!("{intro}: {name}"); + } else { + info!("{intro}: [no name]"); + } + + let PhysicalSize { width, height } = monitor.size(); + info!( + " Current mode: {width}x{height}{}", + if let Some(m_hz) = monitor.refresh_rate_millihertz() { + format!(" @ {}.{} Hz", m_hz / 1000, m_hz % 1000) + } else { + String::new() + } + ); + + let PhysicalPosition { x, y } = monitor.position(); + info!(" Position: {x},{y}"); + + info!(" Scale factor: {}", monitor.scale_factor()); + + info!(" Available modes (width x height x bit-depth):"); + for mode in monitor.video_modes() { + let PhysicalSize { width, height } = mode.size(); + let bits = mode.bit_depth(); + let m_hz = mode.refresh_rate_millihertz(); + info!( + " {width}x{height}x{bits} @ {}.{} Hz", + m_hz / 1000, + m_hz % 1000 + ); + } + } + } + + /// Process the key binding. + fn process_key_binding(key: &str, mods: &ModifiersState) -> Option { + KEY_BINDINGS.iter().find_map(|binding| { + binding + .is_triggered_by(&key, mods) + .then_some(binding.action) + }) + } + + /// Process mouse binding. + fn process_mouse_binding( + button: MouseButton, + mods: &ModifiersState, + ) -> Option { + MOUSE_BINDINGS.iter().find_map(|binding| { + binding + .is_triggered_by(&button, mods) + .then_some(binding.action) + }) + } + + fn print_help(&self) { + info!("Keyboard bindings:"); + for binding in KEY_BINDINGS { + info!( + "{}{:<10} - {} ({})", + modifiers_to_string(binding.mods), + binding.trigger, + binding.action, + binding.action.help(), + ); + } + info!("Mouse bindings:"); + for binding in MOUSE_BINDINGS { + info!( + "{}{:<10} - {} ({})", + modifiers_to_string(binding.mods), + mouse_button_to_string(binding.trigger), + binding.action, + binding.action.help(), + ); + } + } +} + +impl ApplicationHandler for Application { + fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) { + info!("User event: {event:?}"); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + let window = match self.windows.get_mut(&window_id) { + Some(window) => window, + None => return, + }; + + match event { + WindowEvent::Resized(size) => { + window.resize(size); + } + WindowEvent::Focused(focused) => { + if focused { + info!("Window={window_id:?} focused"); + } else { + info!("Window={window_id:?} unfocused"); + } + } + WindowEvent::ScaleFactorChanged { scale_factor, .. } => { + info!("Window={window_id:?} changed scale to {scale_factor}"); + } + WindowEvent::ThemeChanged(theme) => { + info!("Theme changed to {theme:?}"); + window.set_theme(theme); + } + WindowEvent::RedrawRequested => { + if let Err(err) = window.draw() { + error!("Error drawing window: {err}"); + } + } + WindowEvent::Occluded(occluded) => { + window.set_occluded(occluded); + } + WindowEvent::CloseRequested => { + info!("Closing Window={window_id:?}"); + self.windows.remove(&window_id); + } + WindowEvent::ModifiersChanged(modifiers) => { + window.modifiers = modifiers.state(); + info!("Modifiers changed to {:?}", window.modifiers); + } + WindowEvent::MouseWheel { delta, .. } => match delta { + MouseScrollDelta::LineDelta(x, y) => { + info!("Mouse wheel Line Delta: ({x},{y})"); + } + MouseScrollDelta::PixelDelta(px) => { + info!("Mouse wheel Pixel Delta: ({},{})", px.x, px.y); + } + }, + WindowEvent::KeyboardInput { + event, + is_synthetic: false, + .. + } => { + let mods = window.modifiers; + + // Dispatch actions only on press. + if event.state.is_pressed() { + let action = if let Key::Character(ch) = event.logical_key.as_ref() { + Self::process_key_binding(&ch.to_uppercase(), &mods) + } else { + None + }; + + if let Some(action) = action { + self.handle_action(event_loop, window_id, action); + } + } + } + WindowEvent::MouseInput { button, state, .. } => { + let mods = window.modifiers; + if let Some(action) = state + .is_pressed() + .then(|| Self::process_mouse_binding(button, &mods)) + .flatten() + { + self.handle_action(event_loop, window_id, action); + } + } + WindowEvent::CursorLeft { .. } => { + info!("Cursor left Window={window_id:?}"); + window.cursor_left(); + } + WindowEvent::CursorMoved { position, .. } => { + info!("Moved cursor to {position:?}"); + window.cursor_moved(position); + } + WindowEvent::ActivationTokenDone { token: _token, .. } => { + #[cfg(any(x11_platform, wayland_platform))] + { + startup_notify::set_activation_token_env(_token); + if let Err(err) = self.create_window(event_loop, None) { + error!("Error creating new window: {err}"); + } + } + } + WindowEvent::Ime(event) => match event { + Ime::Enabled => info!("IME enabled for Window={window_id:?}"), + Ime::Preedit(text, caret_pos) => { + info!("Preedit: {}, with caret at {:?}", text, caret_pos); + } + Ime::Commit(text) => { + info!("Committed: {}", text); + } + Ime::Disabled => info!("IME disabled for Window={window_id:?}"), + }, + WindowEvent::PinchGesture { delta, .. } => { + window.zoom += delta; + let zoom = window.zoom; + if delta > 0.0 { + info!("Zoomed in {delta:.5} (now: {zoom:.5})"); + } else { + info!("Zoomed out {delta:.5} (now: {zoom:.5})"); + } + } + WindowEvent::RotationGesture { delta, .. } => { + window.rotated += delta; + let rotated = window.rotated; + if delta > 0.0 { + info!("Rotated counterclockwise {delta:.5} (now: {rotated:.5})"); + } else { + info!("Rotated clockwise {delta:.5} (now: {rotated:.5})"); + } + } + WindowEvent::PanGesture { delta, phase, .. } => { + window.panned.x += delta.x; + window.panned.y += delta.y; + info!("Panned ({delta:?})) (now: {:?}), {phase:?}", window.panned); + } + WindowEvent::DoubleTapGesture { .. } => { + info!("Smart zoom"); + } + WindowEvent::TouchpadPressure { .. } + | WindowEvent::HoveredFileCancelled + | WindowEvent::KeyboardInput { .. } + | WindowEvent::CursorEntered { .. } + | WindowEvent::AxisMotion { .. } + | WindowEvent::DroppedFile(_) + | WindowEvent::HoveredFile(_) + | WindowEvent::Destroyed + | WindowEvent::Touch(_) + | WindowEvent::Moved(_) => (), + } + } + + fn device_event( + &mut self, + _event_loop: &ActiveEventLoop, + device_id: DeviceId, + event: DeviceEvent, + ) { + info!("Device {device_id:?} event: {event:?}"); + } + + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + info!("Resumed the event loop"); + self.dump_monitors(event_loop); + + // Create initial window. + self.create_window(event_loop, None) + .expect("failed to create initial window"); + + self.print_help(); + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if self.windows.is_empty() { + info!("No windows left, exiting..."); + event_loop.exit(); + } + } + + #[cfg(not(any(android_platform, ios_platform)))] + fn exiting(&mut self, _event_loop: &ActiveEventLoop) { + // We must drop the context here. + self.context = None; + } +} + +/// State of the window. +struct WindowState { + /// IME input. + ime: bool, + /// Render surface. + /// + /// NOTE: This surface must be dropped before the `Window`. + #[cfg(not(any(android_platform, ios_platform)))] + surface: Surface, Arc>, + /// The actual winit Window. + window: Arc, + /// The window theme we're drawing with. + theme: Theme, + /// Cursor position over the window. + cursor_position: Option>, + /// Window modifiers state. + modifiers: ModifiersState, + /// Occlusion state of the window. + occluded: bool, + /// Current cursor grab mode. + cursor_grab: CursorGrabMode, + /// The amount of zoom into window. + zoom: f64, + /// The amount of rotation of the window. + rotated: f32, + /// The amount of pan of the window. + panned: PhysicalPosition, + + #[cfg(macos_platform)] + option_as_alt: OptionAsAlt, + + // Cursor states. + named_idx: usize, + custom_idx: usize, + cursor_hidden: bool, +} + +impl WindowState { + fn new(app: &Application, window: Window) -> Result> { + let window = Arc::new(window); + + // SAFETY: the surface is dropped before the `window` which provided it with handle, thus + // it doesn't outlive it. + #[cfg(not(any(android_platform, ios_platform)))] + let surface = Surface::new(app.context.as_ref().unwrap(), Arc::clone(&window))?; + + let theme = window.theme().unwrap_or(Theme::Dark); + info!("Theme: {theme:?}"); + let named_idx = 0; + window.set_cursor(CURSORS[named_idx]); + + // Allow IME out of the box. + let ime = true; + window.set_ime_allowed(ime); + + let size = window.inner_size(); + let mut state = Self { + #[cfg(macos_platform)] + option_as_alt: window.option_as_alt(), + custom_idx: app.custom_cursors.len() - 1, + cursor_grab: CursorGrabMode::None, + named_idx, + #[cfg(not(any(android_platform, ios_platform)))] + surface, + window, + theme, + ime, + cursor_position: Default::default(), + cursor_hidden: Default::default(), + modifiers: Default::default(), + occluded: Default::default(), + rotated: Default::default(), + panned: Default::default(), + zoom: Default::default(), + }; + + state.resize(size); + Ok(state) + } + + pub fn toggle_ime(&mut self) { + self.ime = !self.ime; + self.window.set_ime_allowed(self.ime); + if let Some(position) = self.ime.then_some(self.cursor_position).flatten() { + self.window + .set_ime_cursor_area(position, PhysicalSize::new(20, 20)); + } + } + + pub fn minimize(&mut self) { + self.window.set_minimized(true); + } + + pub fn cursor_moved(&mut self, position: PhysicalPosition) { + self.cursor_position = Some(position); + if self.ime { + self.window + .set_ime_cursor_area(position, PhysicalSize::new(20, 20)); + } + } + + pub fn cursor_left(&mut self) { + self.cursor_position = None; + } + + /// Toggle maximized. + fn toggle_maximize(&self) { + let maximized = self.window.is_maximized(); + self.window.set_maximized(!maximized); + } + + /// Toggle window decorations. + fn toggle_decorations(&self) { + let decorated = self.window.is_decorated(); + self.window.set_decorations(!decorated); + } + + /// Toggle window resizable state. + fn toggle_resizable(&self) { + let resizable = self.window.is_resizable(); + self.window.set_resizable(!resizable); + } + + /// Toggle cursor visibility + fn toggle_cursor_visibility(&mut self) { + self.cursor_hidden = !self.cursor_hidden; + self.window.set_cursor_visible(!self.cursor_hidden); + } + + /// Toggle resize increments on a window. + fn toggle_resize_increments(&mut self) { + let new_increments = match self.window.resize_increments() { + Some(_) => None, + None => Some(LogicalSize::new(25.0, 25.0)), + }; + info!("Had increments: {}", new_increments.is_none()); + self.window.set_resize_increments(new_increments); + } + + /// Toggle fullscreen. + fn toggle_fullscreen(&self) { + let fullscreen = if self.window.fullscreen().is_some() { + None + } else { + Some(Fullscreen::Borderless(None)) + }; + + self.window.set_fullscreen(fullscreen); + } + + /// Cycle through the grab modes ignoring errors. + fn cycle_cursor_grab(&mut self) { + self.cursor_grab = match self.cursor_grab { + CursorGrabMode::None => CursorGrabMode::Confined, + CursorGrabMode::Confined => CursorGrabMode::Locked, + CursorGrabMode::Locked => CursorGrabMode::None, + }; + info!("Changing cursor grab mode to {:?}", self.cursor_grab); + if let Err(err) = self.window.set_cursor_grab(self.cursor_grab) { + error!("Error setting cursor grab: {err}"); + } + } + + #[cfg(macos_platform)] + fn cycle_option_as_alt(&mut self) { + self.option_as_alt = match self.option_as_alt { + OptionAsAlt::None => OptionAsAlt::OnlyLeft, + OptionAsAlt::OnlyLeft => OptionAsAlt::OnlyRight, + OptionAsAlt::OnlyRight => OptionAsAlt::Both, + OptionAsAlt::Both => OptionAsAlt::None, + }; + info!("Setting option as alt {:?}", self.option_as_alt); + self.window.set_option_as_alt(self.option_as_alt); + } + + /// Swap the window dimensions with `request_inner_size`. + fn swap_dimensions(&mut self) { + let old_inner_size = self.window.inner_size(); + let mut inner_size = old_inner_size; + + mem::swap(&mut inner_size.width, &mut inner_size.height); + info!("Requesting resize from {old_inner_size:?} to {inner_size:?}"); + + if let Some(new_inner_size) = self.window.request_inner_size(inner_size) { + if old_inner_size == new_inner_size { + info!("Inner size change got ignored"); + } else { + self.resize(new_inner_size); + } + } else { + info!("Request inner size is asynchronous"); + } + } + + /// Pick the next cursor. + fn next_cursor(&mut self) { + self.named_idx = (self.named_idx + 1) % CURSORS.len(); + info!("Setting cursor to \"{:?}\"", CURSORS[self.named_idx]); + self.window + .set_cursor(Cursor::Icon(CURSORS[self.named_idx])); + } + + /// Pick the next custom cursor. + fn next_custom_cursor(&mut self, custom_cursors: &[CustomCursor]) { + self.custom_idx = (self.custom_idx + 1) % custom_cursors.len(); + let cursor = Cursor::Custom(custom_cursors[self.custom_idx].clone()); + self.window.set_cursor(cursor); + } + + /// Custom cursor from an URL. + #[cfg(web_platform)] + fn url_custom_cursor(&mut self, event_loop: &ActiveEventLoop) { + let cursor = event_loop.create_custom_cursor(url_custom_cursor()); + + self.window.set_cursor(cursor); + } + + /// Custom cursor from a URL. + #[cfg(web_platform)] + fn animation_custom_cursor( + &mut self, + event_loop: &ActiveEventLoop, + custom_cursors: &[CustomCursor], + ) { + use rio_window::platform::web::CustomCursorExtWebSys; + use std::time::Duration; + + let cursors = vec![ + custom_cursors[0].clone(), + custom_cursors[1].clone(), + event_loop.create_custom_cursor(url_custom_cursor()), + ]; + let cursor = + CustomCursor::from_animation(Duration::from_secs(3), cursors).unwrap(); + let cursor = event_loop.create_custom_cursor(cursor); + + self.window.set_cursor(cursor); + } + + /// Resize the window to the new size. + fn resize(&mut self, size: PhysicalSize) { + info!("Resized to {size:?}"); + #[cfg(not(any(android_platform, ios_platform)))] + { + let (width, height) = + match (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) { + (Some(width), Some(height)) => (width, height), + _ => return, + }; + self.surface + .resize(width, height) + .expect("failed to resize inner buffer"); + } + self.window.request_redraw(); + } + + /// Change the theme. + fn set_theme(&mut self, theme: Theme) { + self.theme = theme; + self.window.request_redraw(); + } + + /// Show window menu. + fn show_menu(&self) { + if let Some(position) = self.cursor_position { + self.window.show_window_menu(position); + } + } + + /// Drag the window. + fn drag_window(&self) { + if let Err(err) = self.window.drag_window() { + info!("Error starting window drag: {err}"); + } else { + info!("Dragging window Window={:?}", self.window.id()); + } + } + + /// Drag-resize the window. + fn drag_resize_window(&self) { + let position = match self.cursor_position { + Some(position) => position, + None => { + info!("Drag-resize requires cursor to be inside the window"); + return; + } + }; + + let win_size = self.window.inner_size(); + let border_size = BORDER_SIZE * self.window.scale_factor(); + + let x_direction = if position.x < border_size { + ResizeDirection::West + } else if position.x > (win_size.width as f64 - border_size) { + ResizeDirection::East + } else { + // Use arbitrary direction instead of None for simplicity. + ResizeDirection::SouthEast + }; + + let y_direction = if position.y < border_size { + ResizeDirection::North + } else if position.y > (win_size.height as f64 - border_size) { + ResizeDirection::South + } else { + // Use arbitrary direction instead of None for simplicity. + ResizeDirection::SouthEast + }; + + let direction = match (x_direction, y_direction) { + (ResizeDirection::West, ResizeDirection::North) => ResizeDirection::NorthWest, + (ResizeDirection::West, ResizeDirection::South) => ResizeDirection::SouthWest, + (ResizeDirection::West, _) => ResizeDirection::West, + (ResizeDirection::East, ResizeDirection::North) => ResizeDirection::NorthEast, + (ResizeDirection::East, ResizeDirection::South) => ResizeDirection::SouthEast, + (ResizeDirection::East, _) => ResizeDirection::East, + (_, ResizeDirection::South) => ResizeDirection::South, + (_, ResizeDirection::North) => ResizeDirection::North, + _ => return, + }; + + if let Err(err) = self.window.drag_resize_window(direction) { + info!("Error starting window drag-resize: {err}"); + } else { + info!("Drag-resizing window Window={:?}", self.window.id()); + } + } + + /// Change window occlusion state. + fn set_occluded(&mut self, occluded: bool) { + self.occluded = occluded; + if !occluded { + self.window.request_redraw(); + } + } + + /// Draw the window contents. + #[cfg(not(any(android_platform, ios_platform)))] + fn draw(&mut self) -> Result<(), Box> { + if self.occluded { + info!("Skipping drawing occluded window={:?}", self.window.id()); + return Ok(()); + } + + const WHITE: u32 = 0xffffffff; + const DARK_GRAY: u32 = 0xff181818; + + let color = match self.theme { + Theme::Light => WHITE, + Theme::Dark => DARK_GRAY, + }; + + let mut buffer = self.surface.buffer_mut()?; + buffer.fill(color); + self.window.pre_present_notify(); + buffer.present()?; + Ok(()) + } + + #[cfg(any(android_platform, ios_platform))] + fn draw(&mut self) -> Result<(), Box> { + info!("Drawing but without rendering..."); + Ok(()) + } +} + +struct Binding { + trigger: T, + mods: ModifiersState, + action: Action, +} + +impl Binding { + const fn new(trigger: T, mods: ModifiersState, action: Action) -> Self { + Self { + trigger, + mods, + action, + } + } + + fn is_triggered_by(&self, trigger: &T, mods: &ModifiersState) -> bool { + &self.trigger == trigger && &self.mods == mods + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Action { + CloseWindow, + ToggleCursorVisibility, + CreateNewWindow, + ToggleResizeIncrements, + ToggleImeInput, + ToggleDecorations, + ToggleResizable, + ToggleFullscreen, + ToggleMaximize, + Minimize, + NextCursor, + NextCustomCursor, + #[cfg(web_platform)] + UrlCustomCursor, + #[cfg(web_platform)] + AnimationCustomCursor, + CycleCursorGrab, + PrintHelp, + DragWindow, + DragResizeWindow, + ShowWindowMenu, + #[cfg(macos_platform)] + CycleOptionAsAlt, + #[cfg(macos_platform)] + CreateNewTab, + RequestResize, +} + +impl Action { + fn help(&self) -> &'static str { + match self { + Action::CloseWindow => "Close window", + Action::ToggleCursorVisibility => "Hide cursor", + Action::CreateNewWindow => "Create new window", + Action::ToggleImeInput => "Toggle IME input", + Action::ToggleDecorations => "Toggle decorations", + Action::ToggleResizable => "Toggle window resizable state", + Action::ToggleFullscreen => "Toggle fullscreen", + Action::ToggleMaximize => "Maximize", + Action::Minimize => "Minimize", + Action::ToggleResizeIncrements => { + "Use resize increments when resizing window" + } + Action::NextCursor => "Advance the cursor to the next value", + Action::NextCustomCursor => "Advance custom cursor to the next value", + #[cfg(web_platform)] + Action::UrlCustomCursor => "Custom cursor from an URL", + #[cfg(web_platform)] + Action::AnimationCustomCursor => "Custom cursor from an animation", + Action::CycleCursorGrab => "Cycle through cursor grab mode", + Action::PrintHelp => "Print help", + Action::DragWindow => "Start window drag", + Action::DragResizeWindow => "Start window drag-resize", + Action::ShowWindowMenu => "Show window menu", + #[cfg(macos_platform)] + Action::CycleOptionAsAlt => "Cycle option as alt mode", + #[cfg(macos_platform)] + Action::CreateNewTab => "Create new tab", + Action::RequestResize => "Request a resize", + } + } +} + +impl fmt::Display for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Debug::fmt(&self, f) + } +} + +fn decode_cursor(bytes: &[u8]) -> CustomCursorSource { + let img = image::load_from_memory(bytes).unwrap().to_rgba8(); + let samples = img.into_flat_samples(); + let (_, w, h) = samples.extents(); + let (w, h) = (w as u16, h as u16); + CustomCursor::from_rgba(samples.samples, w, h, w / 2, h / 2).unwrap() +} + +#[cfg(web_platform)] +fn url_custom_cursor() -> CustomCursorSource { + use std::sync::atomic::{AtomicU64, Ordering}; + + use rio_window::platform::web::CustomCursorExtWebSys; + + static URL_COUNTER: AtomicU64 = AtomicU64::new(0); + + CustomCursor::from_url( + format!( + "https://picsum.photos/128?random={}", + URL_COUNTER.fetch_add(1, Ordering::Relaxed) + ), + 64, + 64, + ) +} + +fn load_icon(bytes: &[u8]) -> Icon { + let (icon_rgba, icon_width, icon_height) = { + let image = image::load_from_memory(bytes).unwrap().into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + (rgba, width, height) + }; + Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon") +} + +fn modifiers_to_string(mods: ModifiersState) -> String { + let mut mods_line = String::new(); + // Always add + since it's printed as a part of the bindings. + for (modifier, desc) in [ + (ModifiersState::SUPER, "Super+"), + (ModifiersState::ALT, "Alt+"), + (ModifiersState::CONTROL, "Ctrl+"), + (ModifiersState::SHIFT, "Shift+"), + ] { + if !mods.contains(modifier) { + continue; + } + + mods_line.push_str(desc); + } + mods_line +} + +fn mouse_button_to_string(button: MouseButton) -> &'static str { + match button { + MouseButton::Left => "LMB", + MouseButton::Right => "RMB", + MouseButton::Middle => "MMB", + MouseButton::Back => "Back", + MouseButton::Forward => "Forward", + MouseButton::Other(_) => "", + } +} + +/// Cursor list to cycle through. +const CURSORS: &[CursorIcon] = &[ + CursorIcon::Default, + CursorIcon::Crosshair, + CursorIcon::Pointer, + CursorIcon::Move, + CursorIcon::Text, + CursorIcon::Wait, + CursorIcon::Help, + CursorIcon::Progress, + CursorIcon::NotAllowed, + CursorIcon::ContextMenu, + CursorIcon::Cell, + CursorIcon::VerticalText, + CursorIcon::Alias, + CursorIcon::Copy, + CursorIcon::NoDrop, + CursorIcon::Grab, + CursorIcon::Grabbing, + CursorIcon::AllScroll, + CursorIcon::ZoomIn, + CursorIcon::ZoomOut, + CursorIcon::EResize, + CursorIcon::NResize, + CursorIcon::NeResize, + CursorIcon::NwResize, + CursorIcon::SResize, + CursorIcon::SeResize, + CursorIcon::SwResize, + CursorIcon::WResize, + CursorIcon::EwResize, + CursorIcon::NsResize, + CursorIcon::NeswResize, + CursorIcon::NwseResize, + CursorIcon::ColResize, + CursorIcon::RowResize, +]; + +const KEY_BINDINGS: &[Binding<&'static str>] = &[ + Binding::new("Q", ModifiersState::CONTROL, Action::CloseWindow), + Binding::new("H", ModifiersState::CONTROL, Action::PrintHelp), + Binding::new("F", ModifiersState::CONTROL, Action::ToggleFullscreen), + Binding::new("D", ModifiersState::CONTROL, Action::ToggleDecorations), + Binding::new("I", ModifiersState::CONTROL, Action::ToggleImeInput), + Binding::new("L", ModifiersState::CONTROL, Action::CycleCursorGrab), + Binding::new("P", ModifiersState::CONTROL, Action::ToggleResizeIncrements), + Binding::new("R", ModifiersState::CONTROL, Action::ToggleResizable), + Binding::new("R", ModifiersState::ALT, Action::RequestResize), + // M. + Binding::new("M", ModifiersState::CONTROL, Action::ToggleMaximize), + Binding::new("M", ModifiersState::ALT, Action::Minimize), + // N. + Binding::new("N", ModifiersState::CONTROL, Action::CreateNewWindow), + // C. + Binding::new("C", ModifiersState::CONTROL, Action::NextCursor), + Binding::new("C", ModifiersState::ALT, Action::NextCustomCursor), + #[cfg(web_platform)] + Binding::new( + "C", + ModifiersState::CONTROL.union(ModifiersState::SHIFT), + Action::UrlCustomCursor, + ), + #[cfg(web_platform)] + Binding::new( + "C", + ModifiersState::ALT.union(ModifiersState::SHIFT), + Action::AnimationCustomCursor, + ), + Binding::new("Z", ModifiersState::CONTROL, Action::ToggleCursorVisibility), + #[cfg(macos_platform)] + Binding::new("T", ModifiersState::SUPER, Action::CreateNewTab), + #[cfg(macos_platform)] + Binding::new("O", ModifiersState::CONTROL, Action::CycleOptionAsAlt), +]; + +const MOUSE_BINDINGS: &[Binding] = &[ + Binding::new( + MouseButton::Left, + ModifiersState::ALT, + Action::DragResizeWindow, + ), + Binding::new( + MouseButton::Left, + ModifiersState::CONTROL, + Action::DragWindow, + ), + Binding::new( + MouseButton::Right, + ModifiersState::CONTROL, + Action::ShowWindowMenu, + ), +]; diff --git a/rio-window/examples/x11_embed.rs b/rio-window/examples/x11_embed.rs new file mode 100644 index 00000000..e58b52b2 --- /dev/null +++ b/rio-window/examples/x11_embed.rs @@ -0,0 +1,72 @@ +//! A demonstration of embedding a winit window in an existing X11 application. +use std::error::Error; + +#[cfg(x11_platform)] +fn main() -> Result<(), Box> { + use rio_window::application::ApplicationHandler; + use rio_window::event::WindowEvent; + use rio_window::event_loop::{ActiveEventLoop, EventLoop}; + use rio_window::platform::x11::WindowAttributesExtX11; + use rio_window::window::{Window, WindowId}; + + #[path = "util/fill.rs"] + mod fill; + + pub struct XEmbedDemo { + parent_window_id: u32, + window: Option, + } + + impl ApplicationHandler for XEmbedDemo { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attributes = Window::default_attributes() + .with_title("An embedded window!") + .with_inner_size(rio_window::dpi::LogicalSize::new(128.0, 128.0)) + .with_embed_parent_window(self.parent_window_id); + + self.window = Some(event_loop.create_window(window_attributes).unwrap()); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + let window = self.window.as_ref().unwrap(); + match event { + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::RedrawRequested => { + window.pre_present_notify(); + fill::fill_window(window); + } + _ => (), + } + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + self.window.as_ref().unwrap().request_redraw(); + } + } + + // First argument should be a 32-bit X11 window ID. + let parent_window_id = std::env::args() + .nth(1) + .ok_or("Expected a 32-bit X11 window ID as the first argument.")? + .parse::()?; + + tracing_subscriber::fmt::init(); + let event_loop = EventLoop::new()?; + + let mut app = XEmbedDemo { + parent_window_id, + window: None, + }; + event_loop.run_app(&mut app).map_err(Into::into) +} + +#[cfg(not(x11_platform))] +fn main() -> Result<(), Box> { + println!("This example is only supported on X11 platforms."); + Ok(()) +} diff --git a/rio-window/src/application.rs b/rio-window/src/application.rs new file mode 100644 index 00000000..8533b408 --- /dev/null +++ b/rio-window/src/application.rs @@ -0,0 +1,229 @@ +//! End user application handling. + +use crate::event::{DeviceEvent, DeviceId, StartCause, WindowEvent}; +use crate::event_loop::ActiveEventLoop; +use crate::window::WindowId; + +/// The handler of the application events. +pub trait ApplicationHandler { + /// Emitted when new events arrive from the OS to be processed. + /// + /// This is a useful place to put code that should be done before you start processing + /// events, such as updating frame timing information for benchmarking or checking the + /// [`StartCause`] to see if a timer set by + /// [`ControlFlow::WaitUntil`][crate::event_loop::ControlFlow::WaitUntil] has elapsed. + fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { + let _ = (event_loop, cause); + } + + /// Emitted when the application has been resumed. + /// + /// For consistency, all platforms emit a `Resumed` event even if they don't themselves have a + /// formal suspend/resume lifecycle. For systems without a formal suspend/resume lifecycle + /// the `Resumed` event is always emitted after the + /// [`NewEvents(StartCause::Init)`][StartCause::Init] event. + /// + /// # Portability + /// + /// It's recommended that applications should only initialize their graphics context and create + /// a window after they have received their first `Resumed` event. Some systems + /// (specifically Android) won't allow applications to create a render surface until they are + /// resumed. + /// + /// Considering that the implementation of [`Suspended`] and `Resumed` events may be internally + /// driven by multiple platform-specific events, and that there may be subtle differences across + /// platforms with how these internal events are delivered, it's recommended that applications + /// be able to gracefully handle redundant (i.e. back-to-back) [`Suspended`] or `Resumed` + /// events. + /// + /// Also see [`Suspended`] notes. + /// + /// ## Android + /// + /// On Android, the `Resumed` event is sent when a new [`SurfaceView`] has been created. This is + /// expected to closely correlate with the [`onResume`] lifecycle event but there may + /// technically be a discrepancy. + /// + /// [`onResume`]: https://developer.android.com/reference/android/app/Activity#onResume() + /// + /// Applications that need to run on Android must wait until they have been `Resumed` + /// before they will be able to create a render surface (such as an `EGLSurface`, + /// [`VkSurfaceKHR`] or [`wgpu::Surface`]) which depend on having a + /// [`SurfaceView`]. Applications must also assume that if they are [`Suspended`], then their + /// render surfaces are invalid and should be dropped. + /// + /// Also see [`Suspended`] notes. + /// + /// [`SurfaceView`]: https://developer.android.com/reference/android/view/SurfaceView + /// [Activity lifecycle]: https://developer.android.com/guide/components/activities/activity-lifecycle + /// [`VkSurfaceKHR`]: https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkSurfaceKHR.html + /// [`wgpu::Surface`]: https://docs.rs/wgpu/latest/wgpu/struct.Surface.html + /// + /// ## iOS + /// + /// On iOS, the `Resumed` event is emitted in response to an [`applicationDidBecomeActive`] + /// callback which means the application is "active" (according to the + /// [iOS application lifecycle]). + /// + /// [`applicationDidBecomeActive`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622956-applicationdidbecomeactive + /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle + /// + /// ## Web + /// + /// On Web, the `Resumed` event is emitted in response to a [`pageshow`] event + /// with the property [`persisted`] being true, which means that the page is being + /// restored from the [`bfcache`] (back/forward cache) - an in-memory cache that + /// stores a complete snapshot of a page (including the JavaScript heap) as the + /// user is navigating away. + /// + /// [`pageshow`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/pageshow_event + /// [`persisted`]: https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent/persisted + /// [`bfcache`]: https://web.dev/bfcache/ + /// [`Suspended`]: Self::suspended + fn resumed(&mut self, event_loop: &ActiveEventLoop); + + /// Emitted when an event is sent from [`EventLoopProxy::send_event`]. + /// + /// [`EventLoopProxy::send_event`]: crate::event_loop::EventLoopProxy::send_event + fn user_event(&mut self, event_loop: &ActiveEventLoop, event: T) { + let _ = (event_loop, event); + } + + /// Emitted when the OS sends an event to a winit window. + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ); + + /// Emitted when the OS sends an event to a device. + fn device_event( + &mut self, + event_loop: &ActiveEventLoop, + device_id: DeviceId, + event: DeviceEvent, + ) { + let _ = (event_loop, device_id, event); + } + + /// Emitted when the event loop is about to block and wait for new events. + /// + /// Most applications shouldn't need to hook into this event since there is no real relationship + /// between how often the event loop needs to wake up and the dispatching of any specific + /// events. + /// + /// High frequency event sources, such as input devices could potentially lead to lots of wake + /// ups and also lots of corresponding `AboutToWait` events. + /// + /// This is not an ideal event to drive application rendering from and instead applications + /// should render in response to [`WindowEvent::RedrawRequested`] events. + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + let _ = event_loop; + } + + /// Emitted when the application has been suspended. + /// + /// # Portability + /// + /// Not all platforms support the notion of suspending applications, and there may be no + /// technical way to guarantee being able to emit a `Suspended` event if the OS has + /// no formal application lifecycle (currently only Android, iOS, and Web do). For this reason, + /// Winit does not currently try to emit pseudo `Suspended` events before the application + /// quits on platforms without an application lifecycle. + /// + /// Considering that the implementation of `Suspended` and [`Resumed`] events may be internally + /// driven by multiple platform-specific events, and that there may be subtle differences across + /// platforms with how these internal events are delivered, it's recommended that applications + /// be able to gracefully handle redundant (i.e. back-to-back) `Suspended` or [`Resumed`] + /// events. + /// + /// Also see [`Resumed`] notes. + /// + /// ## Android + /// + /// On Android, the `Suspended` event is only sent when the application's associated + /// [`SurfaceView`] is destroyed. This is expected to closely correlate with the [`onPause`] + /// lifecycle event but there may technically be a discrepancy. + /// + /// [`onPause`]: https://developer.android.com/reference/android/app/Activity#onPause() + /// + /// Applications that need to run on Android should assume their [`SurfaceView`] has been + /// destroyed, which indirectly invalidates any existing render surfaces that may have been + /// created outside of Winit (such as an `EGLSurface`, [`VkSurfaceKHR`] or [`wgpu::Surface`]). + /// + /// After being `Suspended` on Android applications must drop all render surfaces before + /// the event callback completes, which may be re-created when the application is next + /// [`Resumed`]. + /// + /// [`SurfaceView`]: https://developer.android.com/reference/android/view/SurfaceView + /// [Activity lifecycle]: https://developer.android.com/guide/components/activities/activity-lifecycle + /// [`VkSurfaceKHR`]: https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkSurfaceKHR.html + /// [`wgpu::Surface`]: https://docs.rs/wgpu/latest/wgpu/struct.Surface.html + /// + /// ## iOS + /// + /// On iOS, the `Suspended` event is currently emitted in response to an + /// [`applicationWillResignActive`] callback which means that the application is + /// about to transition from the active to inactive state (according to the + /// [iOS application lifecycle]). + /// + /// [`applicationWillResignActive`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622950-applicationwillresignactive + /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle + /// + /// ## Web + /// + /// On Web, the `Suspended` event is emitted in response to a [`pagehide`] event + /// with the property [`persisted`] being true, which means that the page is being + /// put in the [`bfcache`] (back/forward cache) - an in-memory cache that stores a + /// complete snapshot of a page (including the JavaScript heap) as the user is + /// navigating away. + /// + /// [`pagehide`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event + /// [`persisted`]: https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent/persisted + /// [`bfcache`]: https://web.dev/bfcache/ + /// [`Resumed`]: Self::resumed + fn suspended(&mut self, event_loop: &ActiveEventLoop) { + let _ = event_loop; + } + + /// Emitted when the event loop is being shut down. + /// + /// This is irreversible - if this method is called, it is guaranteed that the event loop + /// will exit right after. + fn exiting(&mut self, event_loop: &ActiveEventLoop) { + let _ = event_loop; + } + + /// Emitted when the application has received a memory warning. + /// + /// ## Platform-specific + /// + /// ### Android + /// + /// On Android, the `MemoryWarning` event is sent when [`onLowMemory`] was called. The + /// application must [release memory] or risk being killed. + /// + /// [`onLowMemory`]: https://developer.android.com/reference/android/app/Application.html#onLowMemory() + /// [release memory]: https://developer.android.com/topic/performance/memory#release + /// + /// ### iOS + /// + /// On iOS, the `MemoryWarning` event is emitted in response to an + /// [`applicationDidReceiveMemoryWarning`] callback. The application must free as much + /// memory as possible or risk being terminated, see [how to respond to memory warnings]. + /// + /// [`applicationDidReceiveMemoryWarning`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623063-applicationdidreceivememorywarni + /// [how to respond to memory warnings]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle/responding_to_memory_warnings + /// + /// ### Others + /// + /// - **macOS / Orbital / Wayland / Web / Windows:** Unsupported. + fn memory_warning(&mut self, event_loop: &ActiveEventLoop) { + let _ = event_loop; + } + + fn open_urls(&mut self, event_loop: &ActiveEventLoop, urls: Vec) { + let _ = (event_loop, urls); + } +} diff --git a/rio-window/src/cursor.rs b/rio-window/src/cursor.rs new file mode 100644 index 00000000..08f09778 --- /dev/null +++ b/rio-window/src/cursor.rs @@ -0,0 +1,291 @@ +use core::fmt; +use std::error::Error; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use cursor_icon::CursorIcon; + +use crate::platform_impl::{PlatformCustomCursor, PlatformCustomCursorSource}; + +/// The maximum width and height for a cursor when using [`CustomCursor::from_rgba`]. +pub const MAX_CURSOR_SIZE: u16 = 2048; + +const PIXEL_SIZE: usize = 4; + +/// See [`Window::set_cursor()`][crate::window::Window::set_cursor] for more details. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum Cursor { + Icon(CursorIcon), + Custom(CustomCursor), +} + +impl Default for Cursor { + fn default() -> Self { + Self::Icon(CursorIcon::default()) + } +} + +impl From for Cursor { + fn from(icon: CursorIcon) -> Self { + Self::Icon(icon) + } +} + +impl From for Cursor { + fn from(custom: CustomCursor) -> Self { + Self::Custom(custom) + } +} + +/// Use a custom image as a cursor (mouse pointer). +/// +/// Is guaranteed to be cheap to clone. +/// +/// ## Platform-specific +/// +/// **Web**: Some browsers have limits on cursor sizes usually at 128x128. +/// +/// # Example +/// +/// ```no_run +/// # use rio_window::event_loop::ActiveEventLoop; +/// # use rio_window::window::Window; +/// # fn scope(event_loop: &ActiveEventLoop, window: &Window) { +/// use rio_window::window::CustomCursor; +/// +/// let w = 10; +/// let h = 10; +/// let rgba = vec![255; (w * h * 4) as usize]; +/// +/// #[cfg(not(target_family = "wasm"))] +/// let source = CustomCursor::from_rgba(rgba, w, h, w / 2, h / 2).unwrap(); +/// +/// #[cfg(target_family = "wasm")] +/// let source = { +/// use rio_window::platform::web::CustomCursorExtWebSys; +/// CustomCursor::from_url(String::from("http://localhost:3000/cursor.png"), 0, 0) +/// }; +/// +/// let custom_cursor = event_loop.create_custom_cursor(source); +/// +/// window.set_cursor(custom_cursor.clone()); +/// # } +/// ``` +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct CustomCursor { + /// Platforms should make sure this is cheap to clone. + pub(crate) inner: PlatformCustomCursor, +} + +impl CustomCursor { + /// Creates a new cursor from an rgba buffer. + /// + /// The alpha channel is assumed to be **not** premultiplied. + pub fn from_rgba( + rgba: impl Into>, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + let _span = tracing::debug_span!( + "rio_window::Cursor::from_rgba", + width, + height, + hotspot_x, + hotspot_y + ) + .entered(); + + Ok(CustomCursorSource { + inner: PlatformCustomCursorSource::from_rgba( + rgba.into(), + width, + height, + hotspot_x, + hotspot_y, + )?, + }) + } +} + +/// Source for [`CustomCursor`]. +/// +/// See [`CustomCursor`] for more details. +#[derive(Debug)] +pub struct CustomCursorSource { + pub(crate) inner: PlatformCustomCursorSource, +} + +/// An error produced when using [`CustomCursor::from_rgba`] with invalid arguments. +#[derive(Debug, Clone)] +pub enum BadImage { + /// Produced when the image dimensions are larger than [`MAX_CURSOR_SIZE`]. This doesn't + /// guarantee that the cursor will work, but should avoid many platform and device specific + /// limits. + TooLarge { width: u16, height: u16 }, + /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be + /// safely interpreted as 32bpp RGBA pixels. + ByteCountNotDivisibleBy4 { byte_count: usize }, + /// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`. + /// At least one of your arguments is incorrect. + DimensionsVsPixelCount { + width: u16, + height: u16, + width_x_height: u64, + pixel_count: u64, + }, + /// Produced when the hotspot is outside the image bounds + HotspotOutOfBounds { + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + }, +} + +impl fmt::Display for BadImage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BadImage::TooLarge { width, height } => write!( + f, + "The specified dimensions ({width:?}x{height:?}) are too large. The maximum is \ + {MAX_CURSOR_SIZE:?}x{MAX_CURSOR_SIZE:?}.", + ), + BadImage::ByteCountNotDivisibleBy4 { byte_count } => write!( + f, + "The length of the `rgba` argument ({byte_count:?}) isn't divisible by 4, making \ + it impossible to interpret as 32bpp RGBA pixels.", + ), + BadImage::DimensionsVsPixelCount { width, height, width_x_height, pixel_count } => { + write!( + f, + "The specified dimensions ({width:?}x{height:?}) don't match the number of \ + pixels supplied by the `rgba` argument ({pixel_count:?}). For those \ + dimensions, the expected pixel count is {width_x_height:?}.", + ) + }, + BadImage::HotspotOutOfBounds { width, height, hotspot_x, hotspot_y } => write!( + f, + "The specified hotspot ({hotspot_x:?}, {hotspot_y:?}) is outside the image bounds \ + ({width:?}x{height:?}).", + ), + } + } +} + +impl Error for BadImage {} + +/// Platforms export this directly as `PlatformCustomCursorSource` if they need to only work with +/// images. +#[allow(dead_code)] +#[derive(Debug)] +pub(crate) struct OnlyCursorImageSource(pub(crate) CursorImage); + +#[allow(dead_code)] +impl OnlyCursorImageSource { + pub(crate) fn from_rgba( + rgba: Vec, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y).map(Self) + } +} + +/// Platforms export this directly as `PlatformCustomCursor` if they don't implement caching. +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub(crate) struct OnlyCursorImage(pub(crate) Arc); + +impl Hash for OnlyCursorImage { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.0).hash(state); + } +} + +impl PartialEq for OnlyCursorImage { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl Eq for OnlyCursorImage {} + +#[derive(Debug)] +#[allow(dead_code)] +pub(crate) struct CursorImage { + pub(crate) rgba: Vec, + pub(crate) width: u16, + pub(crate) height: u16, + pub(crate) hotspot_x: u16, + pub(crate) hotspot_y: u16, +} + +impl CursorImage { + pub(crate) fn from_rgba( + rgba: Vec, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + if width > MAX_CURSOR_SIZE || height > MAX_CURSOR_SIZE { + return Err(BadImage::TooLarge { width, height }); + } + + if rgba.len() % PIXEL_SIZE != 0 { + return Err(BadImage::ByteCountNotDivisibleBy4 { + byte_count: rgba.len(), + }); + } + + let pixel_count = (rgba.len() / PIXEL_SIZE) as u64; + let width_x_height = width as u64 * height as u64; + if pixel_count != width_x_height { + return Err(BadImage::DimensionsVsPixelCount { + width, + height, + width_x_height, + pixel_count, + }); + } + + if hotspot_x >= width || hotspot_y >= height { + return Err(BadImage::HotspotOutOfBounds { + width, + height, + hotspot_x, + hotspot_y, + }); + } + + Ok(CursorImage { + rgba, + width, + height, + hotspot_x, + hotspot_y, + }) + } +} + +// Platforms that don't support cursors will export this as `PlatformCustomCursor`. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(crate) struct NoCustomCursor; + +#[allow(dead_code)] +impl NoCustomCursor { + pub(crate) fn from_rgba( + rgba: Vec, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y)?; + Ok(Self) + } +} diff --git a/rio-window/src/error.rs b/rio-window/src/error.rs new file mode 100644 index 00000000..93d7d3c1 --- /dev/null +++ b/rio-window/src/error.rs @@ -0,0 +1,144 @@ +use std::{error, fmt}; + +use crate::platform_impl; + +// TODO: Rename +/// An error that may be generated when requesting Winit state +#[derive(Debug)] +pub enum ExternalError { + /// The operation is not supported by the backend. + NotSupported(NotSupportedError), + /// The operation was ignored. + Ignored, + /// The OS cannot perform the operation. + Os(OsError), +} + +/// The error type for when the requested operation is not supported by the backend. +#[derive(Clone)] +pub struct NotSupportedError { + _marker: (), +} + +/// The error type for when the OS cannot perform the requested operation. +#[derive(Debug)] +pub struct OsError { + line: u32, + file: &'static str, + error: platform_impl::OsError, +} + +/// A general error that may occur while running the Winit event loop +#[derive(Debug)] +pub enum EventLoopError { + /// The operation is not supported by the backend. + NotSupported(NotSupportedError), + /// The OS cannot perform the operation. + Os(OsError), + /// The event loop can't be re-created. + RecreationAttempt, + /// Application has exit with an error status. + ExitFailure(i32), +} + +impl From for EventLoopError { + fn from(value: OsError) -> Self { + Self::Os(value) + } +} + +impl NotSupportedError { + #[inline] + #[allow(dead_code)] + pub(crate) fn new() -> NotSupportedError { + NotSupportedError { _marker: () } + } +} + +impl OsError { + #[allow(dead_code)] + pub(crate) fn new( + line: u32, + file: &'static str, + error: platform_impl::OsError, + ) -> OsError { + OsError { line, file, error } + } +} + +#[allow(unused_macros)] +macro_rules! os_error { + ($error:expr) => {{ + crate::error::OsError::new(line!(), file!(), $error) + }}; +} + +impl fmt::Display for OsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.pad(&format!( + "os error at {}:{}: {}", + self.file, self.line, self.error + )) + } +} + +impl fmt::Display for ExternalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + ExternalError::NotSupported(e) => e.fmt(f), + ExternalError::Ignored => write!(f, "Operation was ignored"), + ExternalError::Os(e) => e.fmt(f), + } + } +} + +impl fmt::Debug for NotSupportedError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.debug_struct("NotSupportedError").finish() + } +} + +impl fmt::Display for NotSupportedError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.pad("the requested operation is not supported by Winit") + } +} + +impl fmt::Display for EventLoopError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + EventLoopError::RecreationAttempt => { + write!(f, "EventLoop can't be recreated") + } + EventLoopError::NotSupported(e) => e.fmt(f), + EventLoopError::Os(e) => e.fmt(f), + EventLoopError::ExitFailure(status) => write!(f, "Exit Failure: {status}"), + } + } +} + +impl error::Error for OsError {} +impl error::Error for ExternalError {} +impl error::Error for NotSupportedError {} +impl error::Error for EventLoopError {} + +#[cfg(test)] +#[allow(clippy::redundant_clone)] +mod tests { + use super::*; + + // Eat attributes for testing + #[test] + fn ensure_fmt_does_not_panic() { + let _ = format!( + "{:?}, {}", + NotSupportedError::new(), + NotSupportedError::new().clone() + ); + let _ = format!( + "{:?}, {}", + ExternalError::NotSupported(NotSupportedError::new()), + ExternalError::NotSupported(NotSupportedError::new()) + ); + } +} diff --git a/rio-window/src/event.rs b/rio-window/src/event.rs new file mode 100644 index 00000000..e990a1f7 --- /dev/null +++ b/rio-window/src/event.rs @@ -0,0 +1,1254 @@ +//! The [`Event`] enum and assorted supporting types. +//! +//! These are sent to the closure given to [`EventLoop::run_app(...)`], where they get +//! processed and used to modify the program state. For more details, see the root-level +//! documentation. +//! +//! Some of these events represent different "parts" of a traditional event-handling loop. You could +//! approximate the basic ordering loop of [`EventLoop::run_app(...)`] like this: +//! +//! ```rust,ignore +//! let mut start_cause = StartCause::Init; +//! +//! while !elwt.exiting() { +//! app.new_events(event_loop, start_cause); +//! +//! for event in (window events, user events, device events) { +//! // This will pick the right method on the application based on the event. +//! app.handle_event(event_loop, event); +//! } +//! +//! for window_id in (redraw windows) { +//! app.window_event(event_loop, window_id, RedrawRequested); +//! } +//! +//! app.about_to_wait(event_loop); +//! start_cause = wait_if_necessary(); +//! } +//! +//! app.exiting(event_loop); +//! ``` +//! +//! This leaves out timing details like [`ControlFlow::WaitUntil`] but hopefully +//! describes what happens in what order. +//! +//! [`EventLoop::run_app(...)`]: crate::event_loop::EventLoop::run_app +//! [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil +use std::path::PathBuf; +use std::sync::{Mutex, Weak}; +#[cfg(not(web_platform))] +use std::time::Instant; + +use smol_str::SmolStr; +#[cfg(web_platform)] +use web_time::Instant; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::error::ExternalError; +use crate::event_loop::AsyncRequestSerial; +use crate::keyboard::{self, ModifiersKeyState, ModifiersKeys, ModifiersState}; +use crate::platform_impl; +#[cfg(doc)] +use crate::window::Window; +use crate::window::{ActivationToken, Theme, WindowId}; + +/// Describes a generic event. +/// +/// See the module-level docs for more information on the event loop manages each event. +#[derive(Debug, Clone, PartialEq)] +pub enum Event { + /// See [`ApplicationHandler::new_events`] for details. + /// + /// [`ApplicationHandler::new_events`]: crate::application::ApplicationHandler::new_events + NewEvents(StartCause), + + /// See [`ApplicationHandler::window_event`] for details. + /// + /// [`ApplicationHandler::window_event`]: crate::application::ApplicationHandler::window_event + WindowEvent { + window_id: WindowId, + event: WindowEvent, + }, + + /// See [`ApplicationHandler::device_event`] for details. + /// + /// [`ApplicationHandler::device_event`]: crate::application::ApplicationHandler::device_event + DeviceEvent { + device_id: DeviceId, + event: DeviceEvent, + }, + + /// See [`ApplicationHandler::user_event`] for details. + /// + /// [`ApplicationHandler::user_event`]: crate::application::ApplicationHandler::user_event + UserEvent(T), + + /// See [`ApplicationHandler::suspended`] for details. + /// + /// [`ApplicationHandler::suspended`]: crate::application::ApplicationHandler::suspended + Suspended, + + /// See [`ApplicationHandler::resumed`] for details. + /// + /// [`ApplicationHandler::resumed`]: crate::application::ApplicationHandler::resumed + Resumed, + + /// See [`ApplicationHandler::about_to_wait`] for details. + /// + /// [`ApplicationHandler::about_to_wait`]: crate::application::ApplicationHandler::about_to_wait + AboutToWait, + + /// See [`ApplicationHandler::exiting`] for details. + /// + /// [`ApplicationHandler::exiting`]: crate::application::ApplicationHandler::exiting + LoopExiting, + + /// See [`ApplicationHandler::memory_warning`] for details. + /// + /// [`ApplicationHandler::memory_warning`]: crate::application::ApplicationHandler::memory_warning + MemoryWarning, + + Opened { + urls: Vec, + }, +} + +impl Event { + #[allow(clippy::result_large_err)] + pub fn map_nonuser_event(self) -> Result, Event> { + use self::Event::*; + match self { + UserEvent(_) => Err(self), + WindowEvent { window_id, event } => Ok(WindowEvent { window_id, event }), + DeviceEvent { device_id, event } => Ok(DeviceEvent { device_id, event }), + NewEvents(cause) => Ok(NewEvents(cause)), + AboutToWait => Ok(AboutToWait), + LoopExiting => Ok(LoopExiting), + Suspended => Ok(Suspended), + Resumed => Ok(Resumed), + MemoryWarning => Ok(MemoryWarning), + Opened { urls } => Ok(Opened { urls }), + } + } +} + +/// Describes the reason the event loop is resuming. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StartCause { + /// Sent if the time specified by [`ControlFlow::WaitUntil`] has been reached. Contains the + /// moment the timeout was requested and the requested resume time. The actual resume time is + /// guaranteed to be equal to or after the requested resume time. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + ResumeTimeReached { + start: Instant, + requested_resume: Instant, + }, + + /// Sent if the OS has new events to send to the window, after a wait was requested. Contains + /// the moment the wait was requested and the resume time, if requested. + WaitCancelled { + start: Instant, + requested_resume: Option, + }, + + /// Sent if the event loop is being resumed after the loop's control flow was set to + /// [`ControlFlow::Poll`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + Poll, + + /// Sent once, immediately after `run` is called. Indicates that the loop was just initialized. + Init, +} + +/// Describes an event from a [`Window`]. +#[derive(Debug, Clone, PartialEq)] +pub enum WindowEvent { + /// The activation token was delivered back and now could be used. + #[cfg_attr( + not(any(x11_platform, wayland_platform)), + allow(rustdoc::broken_intra_doc_links) + )] + /// Delivered in response to [`request_activation_token`]. + /// + /// [`request_activation_token`]: crate::platform::startup_notify::WindowExtStartupNotify::request_activation_token + ActivationTokenDone { + serial: AsyncRequestSerial, + token: ActivationToken, + }, + + /// The size of the window has changed. Contains the client area's new dimensions. + Resized(PhysicalSize), + + /// The position of the window has changed. Contains the window's new position. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Wayland:** Unsupported. + Moved(PhysicalPosition), + + /// The window has been requested to close. + CloseRequested, + + /// The window has been destroyed. + Destroyed, + + /// A file has been dropped into the window. + /// + /// When the user drops multiple files at once, this event will be emitted for each file + /// separately. + DroppedFile(PathBuf), + + /// A file is being hovered over the window. + /// + /// When the user hovers multiple files at once, this event will be emitted for each file + /// separately. + HoveredFile(PathBuf), + + /// A file was hovered, but has exited the window. + /// + /// There will be a single `HoveredFileCancelled` event triggered even if multiple files were + /// hovered. + HoveredFileCancelled, + + /// The window gained or lost focus. + /// + /// The parameter is true if the window has gained focus, and false if it has lost focus. + Focused(bool), + + /// An event from the keyboard has been received. + /// + /// ## Platform-specific + /// - **Windows:** The shift key overrides NumLock. In other words, while shift is held down, + /// numpad keys act as if NumLock wasn't active. When this is used, the OS sends fake key + /// events which are not marked as `is_synthetic`. + KeyboardInput { + device_id: DeviceId, + event: KeyEvent, + + /// If `true`, the event was generated synthetically by winit + /// in one of the following circumstances: + /// + /// * Synthetic key press events are generated for all keys pressed when a window gains + /// focus. Likewise, synthetic key release events are generated for all keys pressed when + /// a window goes out of focus. ***Currently, this is only functional on X11 and + /// Windows*** + /// + /// Otherwise, this value is always `false`. + is_synthetic: bool, + }, + + /// The keyboard modifiers have changed. + ModifiersChanged(Modifiers), + + /// An event from an input method. + /// + /// **Note:** You have to explicitly enable this event using [`Window::set_ime_allowed`]. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Orbital:** Unsupported. + Ime(Ime), + + /// The cursor has moved on the window. + /// + /// ## Platform-specific + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + CursorMoved { + device_id: DeviceId, + + /// (x,y) coords in pixels relative to the top-left corner of the window. Because the range + /// of this data is limited by the display area and it may have been transformed by + /// the OS to implement effects such as cursor acceleration, it should not be used + /// to implement non-cursor-like interactions such as 3D camera control. + position: PhysicalPosition, + }, + + /// The cursor has entered the window. + /// + /// ## Platform-specific + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + CursorEntered { device_id: DeviceId }, + + /// The cursor has left the window. + /// + /// ## Platform-specific + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + CursorLeft { device_id: DeviceId }, + + /// A mouse wheel movement or touchpad scroll occurred. + MouseWheel { + device_id: DeviceId, + delta: MouseScrollDelta, + phase: TouchPhase, + }, + + /// An mouse button press has been received. + MouseInput { + device_id: DeviceId, + state: ElementState, + button: MouseButton, + }, + + /// Two-finger pinch gesture, often used for magnification. + /// + /// ## Platform-specific + /// + /// - Only available on **macOS** and **iOS**. + /// - On iOS, not recognized by default. It must be enabled when needed. + PinchGesture { + device_id: DeviceId, + /// Positive values indicate magnification (zooming in) and negative + /// values indicate shrinking (zooming out). + /// + /// This value may be NaN. + delta: f64, + phase: TouchPhase, + }, + + /// N-finger pan gesture + /// + /// ## Platform-specific + /// + /// - Only available on **iOS**. + /// - On iOS, not recognized by default. It must be enabled when needed. + PanGesture { + device_id: DeviceId, + /// Change in pixels of pan gesture from last update. + delta: PhysicalPosition, + phase: TouchPhase, + }, + + /// Double tap gesture. + /// + /// On a Mac, smart magnification is triggered by a double tap with two fingers + /// on the trackpad and is commonly used to zoom on a certain object + /// (e.g. a paragraph of a PDF) or (sort of like a toggle) to reset any zoom. + /// The gesture is also supported in Safari, Pages, etc. + /// + /// The event is general enough that its generating gesture is allowed to vary + /// across platforms. It could also be generated by another device. + /// + /// Unfortunately, neither [Windows](https://support.microsoft.com/en-us/windows/touch-gestures-for-windows-a9d28305-4818-a5df-4e2b-e5590f850741) + /// nor [Wayland](https://wayland.freedesktop.org/libinput/doc/latest/gestures.html) + /// support this gesture or any other gesture with the same effect. + /// + /// ## Platform-specific + /// + /// - Only available on **macOS 10.8** and later, and **iOS**. + /// - On iOS, not recognized by default. It must be enabled when needed. + DoubleTapGesture { device_id: DeviceId }, + + /// Two-finger rotation gesture. + /// + /// Positive delta values indicate rotation counterclockwise and + /// negative delta values indicate rotation clockwise. + /// + /// ## Platform-specific + /// + /// - Only available on **macOS** and **iOS**. + /// - On iOS, not recognized by default. It must be enabled when needed. + RotationGesture { + device_id: DeviceId, + /// change in rotation in degrees + delta: f32, + phase: TouchPhase, + }, + + /// Touchpad pressure event. + /// + /// At the moment, only supported on Apple forcetouch-capable macbooks. + /// The parameters are: pressure level (value between 0 and 1 representing how hard the + /// touchpad is being pressed) and stage (integer representing the click level). + TouchpadPressure { + device_id: DeviceId, + pressure: f32, + stage: i64, + }, + + /// Motion on some analog axis. May report data redundant to other, more specific events. + AxisMotion { + device_id: DeviceId, + axis: AxisId, + value: f64, + }, + + /// Touch event has been received + /// + /// ## Platform-specific + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// - **macOS:** Unsupported. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + Touch(Touch), + + /// The window's scale factor has changed. + /// + /// The following user actions can cause DPI changes: + /// + /// * Changing the display's resolution. + /// * Changing the display's scale factor (e.g. in Control Panel on Windows). + /// * Moving the window to a display with a different scale factor. + /// + /// To update the window size, use the provided [`InnerSizeWriter`] handle. By default, the + /// window is resized to the value suggested by the OS, but it can be changed to any value. + /// + /// For more information about DPI in general, see the [`dpi`] crate. + ScaleFactorChanged { + scale_factor: f64, + /// Handle to update inner size during scale changes. + /// + /// See [`InnerSizeWriter`] docs for more details. + inner_size_writer: InnerSizeWriter, + }, + + /// The system window theme has changed. + /// + /// Applications might wish to react to this to change the theme of the content of the window + /// when the system changes the window theme. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / X11 / Wayland / Orbital:** Unsupported. + ThemeChanged(Theme), + + /// The window has been occluded (completely hidden from view). + /// + /// This is different to window visibility as it depends on whether the window is closed, + /// minimised, set invisible, or fully occluded by another window. + /// + /// ## Platform-specific + /// + /// ### iOS + /// + /// On iOS, the `Occluded(false)` event is emitted in response to an + /// [`applicationWillEnterForeground`] callback which means the application should start + /// preparing its data. The `Occluded(true)` event is emitted in response to an + /// [`applicationDidEnterBackground`] callback which means the application should free + /// resources (according to the [iOS application lifecycle]). + /// + /// [`applicationWillEnterForeground`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623076-applicationwillenterforeground + /// [`applicationDidEnterBackground`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622997-applicationdidenterbackground + /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle + /// + /// ### Others + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// - **Android / Wayland / Windows / Orbital:** Unsupported. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + Occluded(bool), + + /// Emitted when a window should be redrawn. + /// + /// This gets triggered in two scenarios: + /// - The OS has performed an operation that's invalidated the window's contents (such as + /// resizing the window). + /// - The application has explicitly requested a redraw via [`Window::request_redraw`]. + /// + /// Winit will aggregate duplicate redraw requests into a single event, to + /// help avoid duplicating rendering work. + RedrawRequested, +} + +/// Identifier of an input device. +/// +/// Whenever you receive an event arising from a particular input device, this event contains a +/// `DeviceId` which identifies its origin. Note that devices may be virtual (representing an +/// on-screen cursor and keyboard focus) or physical. Virtual devices typically aggregate inputs +/// from multiple physical devices. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(pub(crate) platform_impl::DeviceId); + +impl DeviceId { + /// Returns a dummy id, useful for unit testing. + /// + /// # Safety + /// + /// The only guarantee made about the return value of this function is that + /// it will always be equal to itself and to future values returned by this function. + /// No other guarantees are made. This may be equal to a real `DeviceId`. + /// + /// **Passing this into a winit function will result in undefined behavior.** + pub const unsafe fn dummy() -> Self { + #[allow(unused_unsafe)] + DeviceId(unsafe { platform_impl::DeviceId::dummy() }) + } +} + +/// Represents raw hardware events that are not associated with any particular window. +/// +/// Useful for interactions that diverge significantly from a conventional 2D GUI, such as 3D camera +/// or first-person game controls. Many physical actions, such as mouse movement, can produce both +/// device and window events. Because window events typically arise from virtual devices +/// (corresponding to GUI cursors and keyboard focus) the device IDs may not match. +/// +/// Note that these events are delivered regardless of input focus. +#[derive(Clone, Debug, PartialEq)] +pub enum DeviceEvent { + Added, + Removed, + + /// Change in physical position of a pointing device. + /// + /// This represents raw, unfiltered physical motion. Not to be confused with + /// [`WindowEvent::CursorMoved`]. + MouseMotion { + /// (x, y) change in position in unspecified units. + /// + /// Different devices may use different units. + delta: (f64, f64), + }, + + /// Physical scroll event + MouseWheel { + delta: MouseScrollDelta, + }, + + /// Motion on some analog axis. This event will be reported for all arbitrary input devices + /// that winit supports on this platform, including mouse devices. If the device is a mouse + /// device then this will be reported alongside the MouseMotion event. + Motion { + axis: AxisId, + value: f64, + }, + + Button { + button: ButtonId, + state: ElementState, + }, + + Key(RawKeyEvent), +} + +/// Describes a keyboard input as a raw device event. +/// +/// Note that holding down a key may produce repeated `RawKeyEvent`s. The +/// operating system doesn't provide information whether such an event is a +/// repeat or the initial keypress. An application may emulate this by, for +/// example keeping a Map/Set of pressed keys and determining whether a keypress +/// corresponds to an already pressed key. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct RawKeyEvent { + pub physical_key: keyboard::PhysicalKey, + pub state: ElementState, +} + +/// Describes a keyboard input targeting a window. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEvent { + /// Represents the position of a key independent of the currently active layout. + /// + /// It also uniquely identifies the physical key (i.e. it's mostly synonymous with a scancode). + /// The most prevalent use case for this is games. For example the default keys for the player + /// to move around might be the W, A, S, and D keys on a US layout. The position of these keys + /// is more important than their label, so they should map to Z, Q, S, and D on an "AZERTY" + /// layout. (This value is `KeyCode::KeyW` for the Z key on an AZERTY layout.) + /// + /// ## Caveats + /// + /// - Certain niche hardware will shuffle around physical key positions, e.g. a keyboard that + /// implements DVORAK in hardware (or firmware) + /// - Your application will likely have to handle keyboards which are missing keys that your + /// own keyboard has. + /// - Certain `KeyCode`s will move between a couple of different positions depending on what + /// layout the keyboard was manufactured to support. + /// + /// **Because of these caveats, it is important that you provide users with a way to configure + /// most (if not all) keybinds in your application.** + /// + /// ## `Fn` and `FnLock` + /// + /// `Fn` and `FnLock` key events are *exceedingly unlikely* to be emitted by Winit. These keys + /// are usually handled at the hardware or OS level, and aren't surfaced to applications. If + /// you somehow see this in the wild, we'd like to know :) + pub physical_key: keyboard::PhysicalKey, + + // Allowing `broken_intra_doc_links` for `logical_key`, because + // `key_without_modifiers` is not available on all platforms + #[cfg_attr( + not(any(windows_platform, macos_platform, x11_platform, wayland_platform)), + allow(rustdoc::broken_intra_doc_links) + )] + /// This value is affected by all modifiers except Ctrl. + /// + /// This has two use cases: + /// - Allows querying whether the current input is a Dead key. + /// - Allows handling key-bindings on platforms which don't + /// support [`key_without_modifiers`]. + /// + /// If you use this field (or [`key_without_modifiers`] for that matter) for keyboard + /// shortcuts, **it is important that you provide users with a way to configure your + /// application's shortcuts so you don't render your application unusable for users with an + /// incompatible keyboard layout.** + /// + /// ## Platform-specific + /// - **Web:** Dead keys might be reported as the real key instead + /// of `Dead` depending on the browser/OS. + /// + /// [`key_without_modifiers`]: crate::platform::modifier_supplement::KeyEventExtModifierSupplement::key_without_modifiers + pub logical_key: keyboard::Key, + + /// Contains the text produced by this keypress. + /// + /// In most cases this is identical to the content + /// of the `Character` variant of `logical_key`. + /// However, on Windows when a dead key was pressed earlier + /// but cannot be combined with the character from this + /// keypress, the produced text will consist of two characters: + /// the dead-key-character followed by the character resulting + /// from this keypress. + /// + /// An additional difference from `logical_key` is that + /// this field stores the text representation of any key + /// that has such a representation. For example when + /// `logical_key` is `Key::Named(NamedKey::Enter)`, this field is `Some("\r")`. + /// + /// This is `None` if the current keypress cannot + /// be interpreted as text. + /// + /// See also: `text_with_all_modifiers()` + pub text: Option, + + /// Contains the location of this key on the keyboard. + /// + /// Certain keys on the keyboard may appear in more than once place. For example, the "Shift" + /// key appears on the left side of the QWERTY keyboard as well as the right side. However, + /// both keys have the same symbolic value. Another example of this phenomenon is the "1" + /// key, which appears both above the "Q" key and as the "Keypad 1" key. + /// + /// This field allows the user to differentiate between keys like this that have the same + /// symbolic value but different locations on the keyboard. + /// + /// See the [`KeyLocation`] type for more details. + /// + /// [`KeyLocation`]: crate::keyboard::KeyLocation + pub location: keyboard::KeyLocation, + + /// Whether the key is being pressed or released. + /// + /// See the [`ElementState`] type for more details. + pub state: ElementState, + + /// Whether or not this key is a key repeat event. + /// + /// On some systems, holding down a key for some period of time causes that key to be repeated + /// as though it were being pressed and released repeatedly. This field is `true` if and only + /// if this event is the result of one of those repeats. + /// + /// # Example + /// + /// In games, you often want to ignore repated key events - this can be + /// done by ignoring events where this property is set. + /// + /// ``` + /// use rio_window::event::{ElementState, KeyEvent, WindowEvent}; + /// use rio_window::keyboard::{KeyCode, PhysicalKey}; + /// # let window_event = WindowEvent::RedrawRequested; // To make the example compile + /// match window_event { + /// WindowEvent::KeyboardInput { + /// event: + /// KeyEvent { + /// physical_key: PhysicalKey::Code(KeyCode::KeyW), + /// state: ElementState::Pressed, + /// repeat: false, + /// .. + /// }, + /// .. + /// } => { + /// // The physical key `W` was pressed, and it was not a repeat + /// }, + /// _ => {}, // Handle other events + /// } + /// ``` + pub repeat: bool, + + /// Platform-specific key event information. + /// + /// On Windows, Linux and macOS, this type contains the key without modifiers and the text with + /// all modifiers applied. + /// + /// On Android, iOS, Redox and Web, this type is a no-op. + pub(crate) platform_specific: platform_impl::KeyEventExtra, +} + +/// Describes keyboard modifiers event. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Modifiers { + pub(crate) state: ModifiersState, + + // NOTE: Currently pressed modifiers keys. + // + // The field providing a metadata, it shouldn't be used as a source of truth. + pub(crate) pressed_mods: ModifiersKeys, +} + +impl Modifiers { + /// The state of the modifiers. + pub fn state(&self) -> ModifiersState { + self.state + } + + /// The state of the left shift key. + pub fn lshift_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::LSHIFT) + } + + /// The state of the right shift key. + pub fn rshift_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::RSHIFT) + } + + /// The state of the left alt key. + pub fn lalt_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::LALT) + } + + /// The state of the right alt key. + pub fn ralt_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::RALT) + } + + /// The state of the left control key. + pub fn lcontrol_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::LCONTROL) + } + + /// The state of the right control key. + pub fn rcontrol_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::RCONTROL) + } + + /// The state of the left super key. + pub fn lsuper_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::LSUPER) + } + + /// The state of the right super key. + pub fn rsuper_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::RSUPER) + } + + fn mod_state(&self, modifier: ModifiersKeys) -> ModifiersKeyState { + if self.pressed_mods.contains(modifier) { + ModifiersKeyState::Pressed + } else { + ModifiersKeyState::Unknown + } + } +} + +impl From for Modifiers { + fn from(value: ModifiersState) -> Self { + Self { + state: value, + pressed_mods: Default::default(), + } + } +} + +/// Describes [input method](https://en.wikipedia.org/wiki/Input_method) events. +/// +/// This is also called a "composition event". +/// +/// Most keypresses using a latin-like keyboard layout simply generate a +/// [`WindowEvent::KeyboardInput`]. However, one couldn't possibly have a key for every single +/// unicode character that the user might want to type +/// - so the solution operating systems employ is to allow the user to type these using _a sequence +/// of keypresses_ instead. +/// +/// A prominent example of this is accents - many keyboard layouts allow you to first click the +/// "accent key", and then the character you want to apply the accent to. In this case, some +/// platforms will generate the following event sequence: +/// +/// ```ignore +/// // Press "`" key +/// Ime::Preedit("`", Some((0, 0))) +/// // Press "E" key +/// Ime::Preedit("", None) // Synthetic event generated by winit to clear preedit. +/// Ime::Commit("é") +/// ``` +/// +/// Additionally, certain input devices are configured to display a candidate box that allow the +/// user to select the desired character interactively. (To properly position this box, you must use +/// [`Window::set_ime_cursor_area`].) +/// +/// An example of a keyboard layout which uses candidate boxes is pinyin. On a latin keyboard the +/// following event sequence could be obtained: +/// +/// ```ignore +/// // Press "A" key +/// Ime::Preedit("a", Some((1, 1))) +/// // Press "B" key +/// Ime::Preedit("a b", Some((3, 3))) +/// // Press left arrow key +/// Ime::Preedit("a b", Some((1, 1))) +/// // Press space key +/// Ime::Preedit("啊b", Some((3, 3))) +/// // Press space key +/// Ime::Preedit("", None) // Synthetic event generated by winit to clear preedit. +/// Ime::Commit("啊不") +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Ime { + /// Notifies when the IME was enabled. + /// + /// After getting this event you could receive [`Preedit`][Self::Preedit] and + /// [`Commit`][Self::Commit] events. You should also start performing IME related requests + /// like [`Window::set_ime_cursor_area`]. + Enabled, + + /// Notifies when a new composing text should be set at the cursor position. + /// + /// The value represents a pair of the preedit string and the cursor begin position and end + /// position. When it's `None`, the cursor should be hidden. When `String` is an empty string + /// this indicates that preedit was cleared. + /// + /// The cursor position is byte-wise indexed. + Preedit(String, Option<(usize, usize)>), + + /// Notifies when text should be inserted into the editor widget. + /// + /// Right before this event winit will send empty [`Self::Preedit`] event. + Commit(String), + + /// Notifies when the IME was disabled. + /// + /// After receiving this event you won't get any more [`Preedit`][Self::Preedit] or + /// [`Commit`][Self::Commit] events until the next [`Enabled`][Self::Enabled] event. You should + /// also stop issuing IME related requests like [`Window::set_ime_cursor_area`] and clear + /// pending preedit text. + Disabled, +} + +/// Describes touch-screen input state. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub enum TouchPhase { + Started, + Moved, + Ended, + Cancelled, +} + +/// Represents a touch event +/// +/// Every time the user touches the screen, a new [`TouchPhase::Started`] event with an unique +/// identifier for the finger is generated. When the finger is lifted, an [`TouchPhase::Ended`] +/// event is generated with the same finger id. +/// +/// After a `Started` event has been emitted, there may be zero or more `Move` +/// events when the finger is moved or the touch pressure changes. +/// +/// The finger id may be reused by the system after an `Ended` event. The user +/// should assume that a new `Started` event received with the same id has nothing +/// to do with the old finger and is a new finger. +/// +/// A [`TouchPhase::Cancelled`] event is emitted when the system has canceled tracking this +/// touch, such as when the window loses focus, or on iOS if the user moves the +/// device against their face. +/// +/// ## Platform-specific +/// +/// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. +/// - **macOS:** Unsupported. +/// +/// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border +/// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding +/// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Touch { + pub device_id: DeviceId, + pub phase: TouchPhase, + pub location: PhysicalPosition, + /// Describes how hard the screen was pressed. May be `None` if the platform + /// does not support pressure sensitivity. + /// + /// ## Platform-specific + /// + /// - Only available on **iOS** 9.0+, **Windows** 8+, **Web**, and **Android**. + pub force: Option, + /// Unique identifier of a finger. + pub id: u64, +} + +/// Describes the force of a touch event +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Force { + /// On iOS, the force is calibrated so that the same number corresponds to + /// roughly the same amount of pressure on the screen regardless of the + /// device. + Calibrated { + /// The force of the touch, where a value of 1.0 represents the force of + /// an average touch (predetermined by the system, not user-specific). + /// + /// The force reported by Apple Pencil is measured along the axis of the + /// pencil. If you want a force perpendicular to the device, you need to + /// calculate this value using the `altitude_angle` value. + force: f64, + /// The maximum possible force for a touch. + /// + /// The value of this field is sufficiently high to provide a wide + /// dynamic range for values of the `force` field. + max_possible_force: f64, + /// The altitude (in radians) of the stylus. + /// + /// A value of 0 radians indicates that the stylus is parallel to the + /// surface. The value of this property is Pi/2 when the stylus is + /// perpendicular to the surface. + altitude_angle: Option, + }, + /// If the platform reports the force as normalized, we have no way of + /// knowing how much pressure 1.0 corresponds to – we know it's the maximum + /// amount of force, but as to how much force, you might either have to + /// press really really hard, or not hard at all, depending on the device. + Normalized(f64), +} + +impl Force { + /// Returns the force normalized to the range between 0.0 and 1.0 inclusive. + /// + /// Instead of normalizing the force, you should prefer to handle + /// [`Force::Calibrated`] so that the amount of force the user has to apply is + /// consistent across devices. + pub fn normalized(&self) -> f64 { + match self { + Force::Calibrated { + force, + max_possible_force, + altitude_angle, + } => { + let force = match altitude_angle { + Some(altitude_angle) => force / altitude_angle.sin(), + None => *force, + }; + force / max_possible_force + } + Force::Normalized(force) => *force, + } + } +} + +/// Identifier for a specific analog axis on some device. +pub type AxisId = u32; + +/// Identifier for a specific button on some device. +pub type ButtonId = u32; + +/// Describes the input state of a key. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub enum ElementState { + Pressed, + Released, +} + +impl ElementState { + /// True if `self == Pressed`. + pub fn is_pressed(self) -> bool { + self == ElementState::Pressed + } +} + +/// Describes a button of a mouse controller. +/// +/// ## Platform-specific +/// +/// **macOS:** `Back` and `Forward` might not work with all hardware. +/// **Orbital:** `Back` and `Forward` are unsupported due to orbital not supporting them. +#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +pub enum MouseButton { + Left, + Right, + Middle, + Back, + Forward, + Other(u16), +} + +/// Describes a difference in the mouse scroll wheel state. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum MouseScrollDelta { + /// Amount in lines or rows to scroll in the horizontal + /// and vertical directions. + /// + /// Positive values indicate that the content that is being scrolled should move + /// right and down (revealing more content left and up). + LineDelta(f32, f32), + + /// Amount in pixels to scroll in the horizontal and + /// vertical direction. + /// + /// Scroll events are expressed as a `PixelDelta` if + /// supported by the device (eg. a touchpad) and + /// platform. + /// + /// Positive values indicate that the content being scrolled should + /// move right/down. + /// + /// For a 'natural scrolling' touch pad (that acts like a touch screen) + /// this means moving your fingers right and down should give positive values, + /// and move the content right and down (to reveal more things left and up). + PixelDelta(PhysicalPosition), +} + +/// Handle to synchronously change the size of the window from the +/// [`WindowEvent`]. +#[derive(Debug, Clone)] +pub struct InnerSizeWriter { + pub(crate) new_inner_size: Weak>>, +} + +impl InnerSizeWriter { + #[cfg(not(orbital_platform))] + pub(crate) fn new(new_inner_size: Weak>>) -> Self { + Self { new_inner_size } + } + + /// Try to request inner size which will be set synchronously on the window. + pub fn request_inner_size( + &mut self, + new_inner_size: PhysicalSize, + ) -> Result<(), ExternalError> { + if let Some(inner) = self.new_inner_size.upgrade() { + *inner.lock().unwrap() = new_inner_size; + Ok(()) + } else { + Err(ExternalError::Ignored) + } + } +} + +impl PartialEq for InnerSizeWriter { + fn eq(&self, other: &Self) -> bool { + self.new_inner_size.as_ptr() == other.new_inner_size.as_ptr() + } +} + +#[cfg(test)] +mod tests { + use crate::dpi::PhysicalPosition; + use crate::event; + use std::collections::{BTreeSet, HashSet}; + + macro_rules! foreach_event { + ($closure:expr) => {{ + #[allow(unused_mut)] + let mut x = $closure; + let did = unsafe { event::DeviceId::dummy() }; + + #[allow(deprecated)] + { + use crate::event::Event::*; + use crate::event::Ime::Enabled; + use crate::event::WindowEvent::*; + use crate::window::WindowId; + + // Mainline events. + let wid = unsafe { WindowId::dummy() }; + x(UserEvent(())); + x(NewEvents(event::StartCause::Init)); + x(AboutToWait); + x(LoopExiting); + x(Suspended); + x(Resumed); + + // Window events. + let with_window_event = |wev| { + x(WindowEvent { + window_id: wid, + event: wev, + }) + }; + + with_window_event(CloseRequested); + with_window_event(Destroyed); + with_window_event(Focused(true)); + with_window_event(Moved((0, 0).into())); + with_window_event(Resized((0, 0).into())); + with_window_event(DroppedFile("x.txt".into())); + with_window_event(HoveredFile("x.txt".into())); + with_window_event(HoveredFileCancelled); + with_window_event(Ime(Enabled)); + with_window_event(CursorMoved { + device_id: did, + position: (0, 0).into(), + }); + with_window_event(ModifiersChanged(event::Modifiers::default())); + with_window_event(CursorEntered { device_id: did }); + with_window_event(CursorLeft { device_id: did }); + with_window_event(MouseWheel { + device_id: did, + delta: event::MouseScrollDelta::LineDelta(0.0, 0.0), + phase: event::TouchPhase::Started, + }); + with_window_event(MouseInput { + device_id: did, + state: event::ElementState::Pressed, + button: event::MouseButton::Other(0), + }); + with_window_event(PinchGesture { + device_id: did, + delta: 0.0, + phase: event::TouchPhase::Started, + }); + with_window_event(DoubleTapGesture { device_id: did }); + with_window_event(RotationGesture { + device_id: did, + delta: 0.0, + phase: event::TouchPhase::Started, + }); + with_window_event(PanGesture { + device_id: did, + delta: PhysicalPosition::::new(0.0, 0.0), + phase: event::TouchPhase::Started, + }); + with_window_event(TouchpadPressure { + device_id: did, + pressure: 0.0, + stage: 0, + }); + with_window_event(AxisMotion { + device_id: did, + axis: 0, + value: 0.0, + }); + with_window_event(Touch(event::Touch { + device_id: did, + phase: event::TouchPhase::Started, + location: (0.0, 0.0).into(), + id: 0, + force: Some(event::Force::Normalized(0.0)), + })); + with_window_event(ThemeChanged(crate::window::Theme::Light)); + with_window_event(Occluded(true)); + } + + #[allow(deprecated)] + { + use event::DeviceEvent::*; + + let with_device_event = |dev_ev| { + x(event::Event::DeviceEvent { + device_id: did, + event: dev_ev, + }) + }; + + with_device_event(Added); + with_device_event(Removed); + with_device_event(MouseMotion { + delta: (0.0, 0.0).into(), + }); + with_device_event(MouseWheel { + delta: event::MouseScrollDelta::LineDelta(0.0, 0.0), + }); + with_device_event(Motion { + axis: 0, + value: 0.0, + }); + with_device_event(Button { + button: 0, + state: event::ElementState::Pressed, + }); + } + }}; + } + + #[allow(clippy::redundant_clone)] + #[test] + fn test_event_clone() { + foreach_event!(|event: event::Event<()>| { + let event2 = event.clone(); + assert_eq!(event, event2); + }) + } + + #[test] + fn test_map_nonuser_event() { + foreach_event!(|event: event::Event<()>| { + let is_user = matches!(event, event::Event::UserEvent(())); + let event2 = event.map_nonuser_event::<()>(); + if is_user { + assert_eq!(event2, Err(event::Event::UserEvent(()))); + } else { + assert!(event2.is_ok()); + } + }) + } + + #[test] + fn test_force_normalize() { + let force = event::Force::Normalized(0.0); + assert_eq!(force.normalized(), 0.0); + + let force2 = event::Force::Calibrated { + force: 5.0, + max_possible_force: 2.5, + altitude_angle: None, + }; + assert_eq!(force2.normalized(), 2.0); + + let force3 = event::Force::Calibrated { + force: 5.0, + max_possible_force: 2.5, + altitude_angle: Some(std::f64::consts::PI / 2.0), + }; + assert_eq!(force3.normalized(), 2.0); + } + + #[allow(clippy::clone_on_copy)] + #[test] + fn ensure_attrs_do_not_panic() { + foreach_event!(|event: event::Event<()>| { + let _ = format!("{:?}", event); + }); + let _ = event::StartCause::Init.clone(); + + let did = unsafe { crate::event::DeviceId::dummy() }.clone(); + HashSet::new().insert(did); + let mut set = [did, did, did]; + set.sort_unstable(); + let mut set2 = BTreeSet::new(); + set2.insert(did); + set2.insert(did); + + HashSet::new().insert(event::TouchPhase::Started.clone()); + HashSet::new().insert(event::MouseButton::Left.clone()); + HashSet::new().insert(event::Ime::Enabled); + + let _ = event::Touch { + device_id: did, + phase: event::TouchPhase::Started, + location: (0.0, 0.0).into(), + id: 0, + force: Some(event::Force::Normalized(0.0)), + } + .clone(); + let _ = event::Force::Calibrated { + force: 0.0, + max_possible_force: 0.0, + altitude_angle: None, + } + .clone(); + } +} diff --git a/rio-window/src/event_loop.rs b/rio-window/src/event_loop.rs new file mode 100644 index 00000000..cad3c46c --- /dev/null +++ b/rio-window/src/event_loop.rs @@ -0,0 +1,667 @@ +//! The [`EventLoop`] struct and assorted supporting types, including +//! [`ControlFlow`]. +//! +//! If you want to send custom events to the event loop, use +//! [`EventLoop::create_proxy`] to acquire an [`EventLoopProxy`] and call its +//! [`send_event`][EventLoopProxy::send_event] method. +//! +//! See the root-level documentation for information on how to create and use an event loop to +//! handle events. +use std::marker::PhantomData; +#[cfg(any(x11_platform, wayland_platform))] +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::{error, fmt}; + +#[cfg(not(web_platform))] +use std::time::{Duration, Instant}; +#[cfg(web_platform)] +use web_time::{Duration, Instant}; + +use crate::application::ApplicationHandler; +use crate::error::{EventLoopError, OsError}; +use crate::event::Event; +use crate::monitor::MonitorHandle; +use crate::platform_impl; +use crate::window::{CustomCursor, CustomCursorSource, Window, WindowAttributes}; + +/// Provides a way to retrieve events from the system and from the windows that were registered to +/// the events loop. +/// +/// An `EventLoop` can be seen more or less as a "context". Calling [`EventLoop::new`] +/// initializes everything that will be required to create windows. For example on Linux creating +/// an event loop opens a connection to the X or Wayland server. +/// +/// To wake up an `EventLoop` from a another thread, see the [`EventLoopProxy`] docs. +/// +/// Note that this cannot be shared across threads (due to platform-dependant logic +/// forbidding it), as such it is neither [`Send`] nor [`Sync`]. If you need cross-thread access, +/// the [`Window`] created from this _can_ be sent to an other thread, and the +/// [`EventLoopProxy`] allows you to wake up an `EventLoop` from another thread. +/// +/// [`Window`]: crate::window::Window +pub struct EventLoop { + pub(crate) event_loop: platform_impl::EventLoop, + pub(crate) _marker: PhantomData<*mut ()>, // Not Send nor Sync +} + +/// Target that associates windows with an [`EventLoop`]. +/// +/// This type exists to allow you to create new windows while Winit executes +/// your callback. +pub struct ActiveEventLoop { + pub(crate) p: platform_impl::ActiveEventLoop, + pub(crate) _marker: PhantomData<*mut ()>, // Not Send nor Sync +} + +/// Object that allows building the event loop. +/// +/// This is used to make specifying options that affect the whole application +/// easier. But note that constructing multiple event loops is not supported. +/// +/// This can be created using [`EventLoop::new`] or [`EventLoop::with_user_event`]. +#[derive(Default)] +pub struct EventLoopBuilder { + pub(crate) platform_specific: platform_impl::PlatformSpecificEventLoopAttributes, + _p: PhantomData, +} + +static EVENT_LOOP_CREATED: AtomicBool = AtomicBool::new(false); + +impl EventLoopBuilder<()> { + /// Start building a new event loop. + #[inline] + #[deprecated = "use `EventLoop::builder` instead"] + pub fn new() -> Self { + EventLoop::builder() + } +} + +impl EventLoopBuilder { + /// Builds a new event loop. + /// + /// ***For cross-platform compatibility, the [`EventLoop`] must be created on the main thread, + /// and only once per application.*** + /// + /// Calling this function will result in display backend initialisation. + /// + /// ## Panics + /// + /// Attempting to create the event loop off the main thread will panic. This + /// restriction isn't strictly necessary on all platforms, but is imposed to + /// eliminate any nasty surprises when porting to platforms that require it. + /// `EventLoopBuilderExt::any_thread` functions are exposed in the relevant + /// [`platform`] module if the target platform supports creating an event + /// loop on any thread. + /// + /// ## Platform-specific + /// + /// - **Wayland/X11:** to prevent running under `Wayland` or `X11` unset `WAYLAND_DISPLAY` or + /// `DISPLAY` respectively when building the event loop. + /// - **Android:** must be configured with an `AndroidApp` from `android_main()` by calling + /// [`.with_android_app(app)`] before calling `.build()`, otherwise it'll panic. + /// + /// [`platform`]: crate::platform + #[cfg_attr( + android_platform, + doc = "[`.with_android_app(app)`]: \ + crate::platform::android::EventLoopBuilderExtAndroid::with_android_app" + )] + #[cfg_attr( + not(android_platform), + doc = "[`.with_android_app(app)`]: #only-available-on-android" + )] + #[inline] + pub fn build(&mut self) -> Result, EventLoopError> { + let _span = tracing::debug_span!("winit::EventLoopBuilder::build").entered(); + + if EVENT_LOOP_CREATED.swap(true, Ordering::Relaxed) { + return Err(EventLoopError::RecreationAttempt); + } + + // Certain platforms accept a mutable reference in their API. + #[allow(clippy::unnecessary_mut_passed)] + Ok(EventLoop { + event_loop: platform_impl::EventLoop::new(&mut self.platform_specific)?, + _marker: PhantomData, + }) + } + + #[cfg(web_platform)] + pub(crate) fn allow_event_loop_recreation() { + EVENT_LOOP_CREATED.store(false, Ordering::Relaxed); + } +} + +impl fmt::Debug for EventLoop { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("EventLoop { .. }") + } +} + +impl fmt::Debug for ActiveEventLoop { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("ActiveEventLoop { .. }") + } +} + +/// Set through [`ActiveEventLoop::set_control_flow()`]. +/// +/// Indicates the desired behavior of the event loop after [`Event::AboutToWait`] is emitted. +/// +/// Defaults to [`Wait`]. +/// +/// [`Wait`]: Self::Wait +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum ControlFlow { + /// When the current loop iteration finishes, immediately begin a new iteration regardless of + /// whether or not new events are available to process. + Poll, + + /// When the current loop iteration finishes, suspend the thread until another event arrives. + #[default] + Wait, + + /// When the current loop iteration finishes, suspend the thread until either another event + /// arrives or the given time is reached. + /// + /// Useful for implementing efficient timers. Applications which want to render at the + /// display's native refresh rate should instead use [`Poll`] and the VSync functionality + /// of a graphics API to reduce odds of missed frames. + /// + /// [`Poll`]: Self::Poll + WaitUntil(Instant), +} + +impl ControlFlow { + /// Creates a [`ControlFlow`] that waits until a timeout has expired. + /// + /// In most cases, this is set to [`WaitUntil`]. However, if the timeout overflows, it is + /// instead set to [`Wait`]. + /// + /// [`WaitUntil`]: Self::WaitUntil + /// [`Wait`]: Self::Wait + pub fn wait_duration(timeout: Duration) -> Self { + match Instant::now().checked_add(timeout) { + Some(instant) => Self::WaitUntil(instant), + None => Self::Wait, + } + } +} + +impl EventLoop<()> { + /// Create the event loop. + /// + /// This is an alias of `EventLoop::builder().build()`. + #[inline] + pub fn new() -> Result, EventLoopError> { + Self::builder().build() + } + + /// Start building a new event loop. + /// + /// This returns an [`EventLoopBuilder`], to allow configuring the event loop before creation. + /// + /// To get the actual event loop, call [`build`][EventLoopBuilder::build] on that. + #[inline] + pub fn builder() -> EventLoopBuilder<()> { + Self::with_user_event() + } +} + +impl EventLoop { + /// Start building a new event loop, with the given type as the user event + /// type. + pub fn with_user_event() -> EventLoopBuilder { + EventLoopBuilder { + platform_specific: Default::default(), + _p: PhantomData, + } + } + + /// See [`run_app`]. + /// + /// [`run_app`]: Self::run_app + #[inline] + #[deprecated = "use `EventLoop::run_app` instead"] + #[cfg(not(all(web_platform, target_feature = "exception-handling")))] + pub fn run(self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &ActiveEventLoop), + { + let _span = tracing::debug_span!("winit::EventLoop::run").entered(); + + self.event_loop.run(event_handler) + } + + /// Run the application with the event loop on the calling thread. + /// + /// See the [`set_control_flow()`] docs on how to change the event loop's behavior. + /// + /// ## Platform-specific + /// + /// - **iOS:** Will never return to the caller and so values not passed to this function will + /// *not* be dropped before the process exits. + /// - **Web:** Will _act_ as if it never returns to the caller by throwing a Javascript + /// exception (that Rust doesn't see) that will also mean that the rest of the function is + /// never executed and any values not passed to this function will *not* be dropped. + /// + /// Web applications are recommended to use + #[cfg_attr( + web_platform, + doc = "[`EventLoopExtWebSys::spawn_app()`][crate::platform::web::EventLoopExtWebSys::spawn_app()]" + )] + #[cfg_attr(not(web_platform), doc = "`EventLoopExtWebSys::spawn()`")] + /// [^1] instead of [`run_app()`] to avoid the need + /// for the Javascript exception trick, and to make it clearer that the event loop runs + /// asynchronously (via the browser's own, internal, event loop) and doesn't block the + /// current thread of execution like it does on other platforms. + /// + /// This function won't be available with `target_feature = "exception-handling"`. + /// + /// [`set_control_flow()`]: ActiveEventLoop::set_control_flow() + /// [`run_app()`]: Self::run_app() + /// [^1]: `EventLoopExtWebSys::spawn_app()` is only available on Web. + #[inline] + #[cfg(not(all(web_platform, target_feature = "exception-handling")))] + pub fn run_app>( + self, + app: &mut A, + ) -> Result<(), EventLoopError> { + self.event_loop + .run(|event, event_loop| dispatch_event_for_app(app, event_loop, event)) + } + + /// Creates an [`EventLoopProxy`] that can be used to dispatch user events + /// to the main event loop, possibly from another thread. + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + event_loop_proxy: self.event_loop.create_proxy(), + } + } + + /// Gets a persistent reference to the underlying platform display. + /// + /// See the [`OwnedDisplayHandle`] type for more information. + pub fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle { + platform: self.event_loop.window_target().p.owned_display_handle(), + } + } + + /// Change if or when [`DeviceEvent`]s are captured. + /// + /// See [`ActiveEventLoop::listen_device_events`] for details. + /// + /// [`DeviceEvent`]: crate::event::DeviceEvent + pub fn listen_device_events(&self, allowed: DeviceEvents) { + let _span = tracing::debug_span!( + "winit::EventLoop::listen_device_events", + allowed = ?allowed + ) + .entered(); + + self.event_loop + .window_target() + .p + .listen_device_events(allowed); + } + + /// Sets the [`ControlFlow`]. + pub fn set_control_flow(&self, control_flow: ControlFlow) { + self.event_loop + .window_target() + .p + .set_control_flow(control_flow) + } + + /// Create a window. + /// + /// Creating window without event loop running often leads to improper window creation; + /// use [`ActiveEventLoop::create_window`] instead. + #[deprecated = "use `ActiveEventLoop::create_window` instead"] + #[inline] + pub fn create_window( + &self, + window_attributes: WindowAttributes, + ) -> Result { + let _span = tracing::debug_span!( + "winit::EventLoop::create_window", + window_attributes = ?window_attributes + ) + .entered(); + + let window = platform_impl::Window::new( + &self.event_loop.window_target().p, + window_attributes, + )?; + Ok(Window { window }) + } + + /// Create custom cursor. + pub fn create_custom_cursor( + &self, + custom_cursor: CustomCursorSource, + ) -> CustomCursor { + self.event_loop + .window_target() + .p + .create_custom_cursor(custom_cursor) + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for EventLoop { + fn display_handle(&self) -> Result, rwh_06::HandleError> { + rwh_06::HasDisplayHandle::display_handle(self.event_loop.window_target()) + } +} + +#[cfg(any(x11_platform, wayland_platform))] +impl AsFd for EventLoop { + /// Get the underlying [EventLoop]'s `fd` which you can register + /// into other event loop, like [`calloop`] or [`mio`]. When doing so, the + /// loop must be polled with the [`pump_app_events`] API. + /// + /// [`calloop`]: https://crates.io/crates/calloop + /// [`mio`]: https://crates.io/crates/mio + /// [`pump_app_events`]: crate::platform::pump_events::EventLoopExtPumpEvents::pump_app_events + fn as_fd(&self) -> BorrowedFd<'_> { + self.event_loop.as_fd() + } +} + +#[cfg(any(x11_platform, wayland_platform))] +impl AsRawFd for EventLoop { + /// Get the underlying [EventLoop]'s raw `fd` which you can register + /// into other event loop, like [`calloop`] or [`mio`]. When doing so, the + /// loop must be polled with the [`pump_app_events`] API. + /// + /// [`calloop`]: https://crates.io/crates/calloop + /// [`mio`]: https://crates.io/crates/mio + /// [`pump_app_events`]: crate::platform::pump_events::EventLoopExtPumpEvents::pump_app_events + fn as_raw_fd(&self) -> RawFd { + self.event_loop.as_raw_fd() + } +} + +impl ActiveEventLoop { + /// Create the window. + /// + /// Possible causes of error include denied permission, incompatible system, and lack of memory. + /// + /// ## Platform-specific + /// + /// - **Web:** The window is created but not inserted into the web page automatically. Please + /// see the web platform module for more information. + #[inline] + pub fn create_window( + &self, + window_attributes: WindowAttributes, + ) -> Result { + let _span = tracing::debug_span!( + "winit::ActiveEventLoop::create_window", + window_attributes = ?window_attributes + ) + .entered(); + + let window = platform_impl::Window::new(&self.p, window_attributes)?; + Ok(Window { window }) + } + + /// Create custom cursor. + pub fn create_custom_cursor( + &self, + custom_cursor: CustomCursorSource, + ) -> CustomCursor { + let _span = tracing::debug_span!("winit::ActiveEventLoop::create_custom_cursor",) + .entered(); + + self.p.create_custom_cursor(custom_cursor) + } + + /// Returns the list of all the monitors available on the system. + #[inline] + pub fn available_monitors(&self) -> impl Iterator { + let _span = + tracing::debug_span!("winit::ActiveEventLoop::available_monitors",).entered(); + + #[allow(clippy::useless_conversion)] // false positive on some platforms + self.p + .available_monitors() + .into_iter() + .map(|inner| MonitorHandle { inner }) + } + + /// Returns the primary monitor of the system. + /// + /// Returns `None` if it can't identify any monitor as a primary one. + /// + /// ## Platform-specific + /// + /// **Wayland / Web:** Always returns `None`. + #[inline] + pub fn primary_monitor(&self) -> Option { + let _span = + tracing::debug_span!("winit::ActiveEventLoop::primary_monitor",).entered(); + + self.p + .primary_monitor() + .map(|inner| MonitorHandle { inner }) + } + + /// Change if or when [`DeviceEvent`]s are captured. + /// + /// Since the [`DeviceEvent`] capture can lead to high CPU usage for unfocused windows, winit + /// will ignore them by default for unfocused windows on Linux/BSD. This method allows changing + /// this at runtime to explicitly capture them again. + /// + /// ## Platform-specific + /// + /// - **Wayland / macOS / iOS / Android / Orbital:** Unsupported. + /// + /// [`DeviceEvent`]: crate::event::DeviceEvent + pub fn listen_device_events(&self, allowed: DeviceEvents) { + let _span = tracing::debug_span!( + "winit::ActiveEventLoop::listen_device_events", + allowed = ?allowed + ) + .entered(); + + self.p.listen_device_events(allowed); + } + + /// Sets the [`ControlFlow`]. + pub fn set_control_flow(&self, control_flow: ControlFlow) { + self.p.set_control_flow(control_flow) + } + + /// Gets the current [`ControlFlow`]. + pub fn control_flow(&self) -> ControlFlow { + self.p.control_flow() + } + + /// This exits the event loop. + /// + /// See [`LoopExiting`][Event::LoopExiting]. + pub fn exit(&self) { + let _span = tracing::debug_span!("winit::ActiveEventLoop::exit",).entered(); + + self.p.exit() + } + + /// Returns if the [`EventLoop`] is about to stop. + /// + /// See [`exit()`][Self::exit]. + pub fn exiting(&self) -> bool { + self.p.exiting() + } + + /// Gets a persistent reference to the underlying platform display. + /// + /// See the [`OwnedDisplayHandle`] type for more information. + pub fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle { + platform: self.p.owned_display_handle(), + } + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for ActiveEventLoop { + fn display_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self.p.raw_display_handle_rwh_06()?; + // SAFETY: The display will never be deallocated while the event loop is alive. + Ok(unsafe { rwh_06::DisplayHandle::borrow_raw(raw) }) + } +} + +/// A proxy for the underlying display handle. +/// +/// The purpose of this type is to provide a cheaply clonable handle to the underlying +/// display handle. This is often used by graphics APIs to connect to the underlying APIs. +/// It is difficult to keep a handle to the [`EventLoop`] type or the [`ActiveEventLoop`] +/// type. In contrast, this type involves no lifetimes and can be persisted for as long as +/// needed. +/// +/// For all platforms, this is one of the following: +/// +/// - A zero-sized type that is likely optimized out. +/// - A reference-counted pointer to the underlying type. +#[derive(Clone)] +pub struct OwnedDisplayHandle { + #[cfg_attr(not(feature = "rwh_06"), allow(dead_code))] + platform: platform_impl::OwnedDisplayHandle, +} + +impl fmt::Debug for OwnedDisplayHandle { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OwnedDisplayHandle").finish_non_exhaustive() + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for OwnedDisplayHandle { + #[inline] + fn display_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self.platform.raw_display_handle_rwh_06()?; + + // SAFETY: The underlying display handle should be safe. + let handle = unsafe { rwh_06::DisplayHandle::borrow_raw(raw) }; + + Ok(handle) + } +} + +/// Used to send custom events to [`EventLoop`]. +pub struct EventLoopProxy { + event_loop_proxy: platform_impl::EventLoopProxy, +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + Self { + event_loop_proxy: self.event_loop_proxy.clone(), + } + } +} + +impl EventLoopProxy { + /// Send an event to the [`EventLoop`] from which this proxy was created. This emits a + /// `UserEvent(event)` event in the event loop, where `event` is the value passed to this + /// function. + /// + /// Returns an `Err` if the associated [`EventLoop`] no longer exists. + /// + /// [`UserEvent(event)`]: Event::UserEvent + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + let _span = tracing::debug_span!("winit::EventLoopProxy::send_event",).entered(); + + self.event_loop_proxy.send_event(event) + } +} + +impl fmt::Debug for EventLoopProxy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("EventLoopProxy { .. }") + } +} + +/// The error that is returned when an [`EventLoopProxy`] attempts to wake up an [`EventLoop`] that +/// no longer exists. +/// +/// Contains the original event given to [`EventLoopProxy::send_event`]. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct EventLoopClosed(pub T); + +impl fmt::Display for EventLoopClosed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Tried to wake up a closed `EventLoop`") + } +} + +impl error::Error for EventLoopClosed {} + +/// Control when device events are captured. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] +pub enum DeviceEvents { + /// Report device events regardless of window focus. + Always, + /// Only capture device events while the window is focused. + #[default] + WhenFocused, + /// Never capture device events. + Never, +} + +/// A unique identifier of the winit's async request. +/// +/// This could be used to identify the async request once it's done +/// and a specific action must be taken. +/// +/// One of the handling scenarios could be to maintain a working list +/// containing [`AsyncRequestSerial`] and some closure associated with it. +/// Then once event is arriving the working list is being traversed and a job +/// executed and removed from the list. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AsyncRequestSerial { + serial: usize, +} + +impl AsyncRequestSerial { + // TODO(kchibisov): Remove `cfg` when the clipboard will be added. + #[allow(dead_code)] + pub(crate) fn get() -> Self { + static CURRENT_SERIAL: AtomicUsize = AtomicUsize::new(0); + // NOTE: We rely on wrap around here, while the user may just request + // in the loop usize::MAX times that's issue is considered on them. + let serial = CURRENT_SERIAL.fetch_add(1, Ordering::Relaxed); + Self { serial } + } +} + +/// Shim for various run APIs. +#[inline(always)] +pub(crate) fn dispatch_event_for_app>( + app: &mut A, + event_loop: &ActiveEventLoop, + event: Event, +) { + match event { + Event::NewEvents(cause) => app.new_events(event_loop, cause), + Event::WindowEvent { window_id, event } => { + app.window_event(event_loop, window_id, event) + } + Event::DeviceEvent { device_id, event } => { + app.device_event(event_loop, device_id, event) + } + Event::UserEvent(event) => app.user_event(event_loop, event), + Event::Suspended => app.suspended(event_loop), + Event::Resumed => app.resumed(event_loop), + Event::AboutToWait => app.about_to_wait(event_loop), + Event::LoopExiting => app.exiting(event_loop), + Event::MemoryWarning => app.memory_warning(event_loop), + Event::Opened { urls } => app.open_urls(event_loop, urls), + } +} diff --git a/rio-window/src/icon.rs b/rio-window/src/icon.rs new file mode 100644 index 00000000..0a2a2191 --- /dev/null +++ b/rio-window/src/icon.rs @@ -0,0 +1,139 @@ +use crate::platform_impl::PlatformIcon; +use std::error::Error; +use std::{fmt, io, mem}; + +#[repr(C)] +#[derive(Debug)] +pub(crate) struct Pixel { + pub(crate) r: u8, + pub(crate) g: u8, + pub(crate) b: u8, + pub(crate) a: u8, +} + +pub(crate) const PIXEL_SIZE: usize = mem::size_of::(); + +#[derive(Debug)] +/// An error produced when using [`Icon::from_rgba`] with invalid arguments. +pub enum BadIcon { + /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be + /// safely interpreted as 32bpp RGBA pixels. + ByteCountNotDivisibleBy4 { byte_count: usize }, + /// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`. + /// At least one of your arguments is incorrect. + DimensionsVsPixelCount { + width: u32, + height: u32, + width_x_height: usize, + pixel_count: usize, + }, + /// Produced when underlying OS functionality failed to create the icon + OsError(io::Error), +} + +impl fmt::Display for BadIcon { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BadIcon::ByteCountNotDivisibleBy4 { byte_count } => write!( + f, + "The length of the `rgba` argument ({byte_count:?}) isn't divisible by 4, making \ + it impossible to interpret as 32bpp RGBA pixels.", + ), + BadIcon::DimensionsVsPixelCount { width, height, width_x_height, pixel_count } => { + write!( + f, + "The specified dimensions ({width:?}x{height:?}) don't match the number of \ + pixels supplied by the `rgba` argument ({pixel_count:?}). For those \ + dimensions, the expected pixel count is {width_x_height:?}.", + ) + }, + BadIcon::OsError(e) => write!(f, "OS error when instantiating the icon: {e:?}"), + } + } +} + +impl Error for BadIcon {} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct RgbaIcon { + pub(crate) rgba: Vec, + pub(crate) width: u32, + pub(crate) height: u32, +} + +/// For platforms which don't have window icons (e.g. web) +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NoIcon; + +#[allow(dead_code)] // These are not used on every platform +mod constructors { + use super::*; + + impl RgbaIcon { + pub fn from_rgba( + rgba: Vec, + width: u32, + height: u32, + ) -> Result { + if rgba.len() % PIXEL_SIZE != 0 { + return Err(BadIcon::ByteCountNotDivisibleBy4 { + byte_count: rgba.len(), + }); + } + let pixel_count = rgba.len() / PIXEL_SIZE; + if pixel_count != (width * height) as usize { + Err(BadIcon::DimensionsVsPixelCount { + width, + height, + width_x_height: (width * height) as usize, + pixel_count, + }) + } else { + Ok(RgbaIcon { + rgba, + width, + height, + }) + } + } + } + + impl NoIcon { + pub fn from_rgba( + rgba: Vec, + width: u32, + height: u32, + ) -> Result { + // Create the rgba icon anyway to validate the input + let _ = RgbaIcon::from_rgba(rgba, width, height)?; + Ok(NoIcon) + } + } +} + +/// An icon used for the window titlebar, taskbar, etc. +#[derive(Clone)] +pub struct Icon { + pub(crate) inner: PlatformIcon, +} + +impl fmt::Debug for Icon { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + fmt::Debug::fmt(&self.inner, formatter) + } +} + +impl Icon { + /// Creates an icon from 32bpp RGBA data. + /// + /// The length of `rgba` must be divisible by 4, and `width * height` must equal + /// `rgba.len() / 4`. Otherwise, this will return a `BadIcon` error. + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + let _span = + tracing::debug_span!("winit::Icon::from_rgba", width, height).entered(); + + Ok(Icon { + inner: PlatformIcon::from_rgba(rgba, width, height)?, + }) + } +} diff --git a/rio-window/src/keyboard.rs b/rio-window/src/keyboard.rs new file mode 100644 index 00000000..0cd211f4 --- /dev/null +++ b/rio-window/src/keyboard.rs @@ -0,0 +1,1748 @@ +//! Types related to the keyboard. + +// This file contains a substantial portion of the UI Events Specification by the W3C. In +// particular, the variant names within `Key` and `KeyCode` and their documentation are modified +// versions of contents of the aforementioned specification. +// +// The original documents are: +// +// ### For `Key` +// UI Events KeyboardEvent key Values +// https://www.w3.org/TR/2017/CR-uievents-key-20170601/ +// Copyright © 2017 W3C® (MIT, ERCIM, Keio, Beihang). +// +// ### For `KeyCode` +// UI Events KeyboardEvent code Values +// https://www.w3.org/TR/2017/CR-uievents-code-20170601/ +// Copyright © 2017 W3C® (MIT, ERCIM, Keio, Beihang). +// +// These documents were used under the terms of the following license. This W3C license as well as +// the W3C short notice apply to the `Key` and `KeyCode` enums and their variants and the +// documentation attached to their variants. + +// --------- BEGINNING OF W3C LICENSE -------------------------------------------------------------- +// +// License +// +// By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, +// and will comply with the following terms and conditions. +// +// Permission to copy, modify, and distribute this work, with or without modification, for any +// purpose and without fee or royalty is hereby granted, provided that you include the following on +// ALL copies of the work or portions thereof, including modifications: +// +// - The full text of this NOTICE in a location viewable to users of the redistributed or derivative +// work. +// - Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none +// exist, the W3C Software and Document Short Notice should be included. +// - Notice of any changes or modifications, through a copyright statement on the new code or +// document such as "This software or document includes material copied from or derived from +// [title and URI of the W3C document]. Copyright © [YEAR] W3C® (MIT, ERCIM, Keio, Beihang)." +// +// Disclaimers +// +// THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR +// ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD +// PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. +// +// COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES +// ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. +// +// The name and trademarks of copyright holders may NOT be used in advertising or publicity +// pertaining to the work without specific, written prior permission. Title to copyright in this +// work will at all times remain with copyright holders. +// +// --------- END OF W3C LICENSE -------------------------------------------------------------------- + +// --------- BEGINNING OF W3C SHORT NOTICE --------------------------------------------------------- +// +// winit: https://github.com/rust-windowing/winit +// +// Copyright © 2021 World Wide Web Consortium, (Massachusetts Institute of Technology, European +// Research Consortium for Informatics and Mathematics, Keio University, Beihang). All Rights +// Reserved. This work is distributed under the W3C® Software License [1] in the hope that it will +// be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// [1] http://www.w3.org/Consortium/Legal/copyright-software +// +// --------- END OF W3C SHORT NOTICE --------------------------------------------------------------- + +use bitflags::bitflags; +pub use smol_str::SmolStr; + +/// Contains the platform-native physical key identifier +/// +/// The exact values vary from platform to platform (which is part of why this is a per-platform +/// enum), but the values are primarily tied to the key's physical location on the keyboard. +/// +/// This enum is primarily used to store raw keycodes when Winit doesn't map a given native +/// physical key identifier to a meaningful [`KeyCode`] variant. In the presence of identifiers we +/// haven't mapped for you yet, this lets you use use [`KeyCode`] to: +/// +/// - Correctly match key press and release events. +/// - On non-web platforms, support assigning keybinds to virtually any key through a UI. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum NativeKeyCode { + Unidentified, + /// An Android "scancode". + Android(u32), + /// A macOS "scancode". + MacOS(u16), + /// A Windows "scancode". + Windows(u16), + /// An XKB "keycode". + Xkb(u32), +} + +impl std::fmt::Debug for NativeKeyCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use NativeKeyCode::{Android, MacOS, Unidentified, Windows, Xkb}; + let mut debug_tuple; + match self { + Unidentified => { + debug_tuple = f.debug_tuple("Unidentified"); + } + Android(code) => { + debug_tuple = f.debug_tuple("Android"); + debug_tuple.field(&format_args!("0x{code:04X}")); + } + MacOS(code) => { + debug_tuple = f.debug_tuple("MacOS"); + debug_tuple.field(&format_args!("0x{code:04X}")); + } + Windows(code) => { + debug_tuple = f.debug_tuple("Windows"); + debug_tuple.field(&format_args!("0x{code:04X}")); + } + Xkb(code) => { + debug_tuple = f.debug_tuple("Xkb"); + debug_tuple.field(&format_args!("0x{code:04X}")); + } + } + debug_tuple.finish() + } +} + +/// Contains the platform-native logical key identifier +/// +/// Exactly what that means differs from platform to platform, but the values are to some degree +/// tied to the currently active keyboard layout. The same key on the same keyboard may also report +/// different values on different platforms, which is one of the reasons this is a per-platform +/// enum. +/// +/// This enum is primarily used to store raw keysym when Winit doesn't map a given native logical +/// key identifier to a meaningful [`Key`] variant. This lets you use [`Key`], and let the user +/// define keybinds which work in the presence of identifiers we haven't mapped for you yet. +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum NativeKey { + Unidentified, + /// An Android "keycode", which is similar to a "virtual-key code" on Windows. + Android(u32), + /// A macOS "scancode". There does not appear to be any direct analogue to either keysyms or + /// "virtual-key" codes in macOS, so we report the scancode instead. + MacOS(u16), + /// A Windows "virtual-key code". + Windows(u16), + /// An XKB "keysym". + Xkb(u32), + /// A "key value string". + Web(SmolStr), +} + +impl std::fmt::Debug for NativeKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use NativeKey::{Android, MacOS, Unidentified, Web, Windows, Xkb}; + let mut debug_tuple; + match self { + Unidentified => { + debug_tuple = f.debug_tuple("Unidentified"); + } + Android(code) => { + debug_tuple = f.debug_tuple("Android"); + debug_tuple.field(&format_args!("0x{code:04X}")); + } + MacOS(code) => { + debug_tuple = f.debug_tuple("MacOS"); + debug_tuple.field(&format_args!("0x{code:04X}")); + } + Windows(code) => { + debug_tuple = f.debug_tuple("Windows"); + debug_tuple.field(&format_args!("0x{code:04X}")); + } + Xkb(code) => { + debug_tuple = f.debug_tuple("Xkb"); + debug_tuple.field(&format_args!("0x{code:04X}")); + } + Web(code) => { + debug_tuple = f.debug_tuple("Web"); + debug_tuple.field(code); + } + } + debug_tuple.finish() + } +} + +impl From for NativeKey { + #[inline] + fn from(code: NativeKeyCode) -> Self { + match code { + NativeKeyCode::Unidentified => NativeKey::Unidentified, + NativeKeyCode::Android(x) => NativeKey::Android(x), + NativeKeyCode::MacOS(x) => NativeKey::MacOS(x), + NativeKeyCode::Windows(x) => NativeKey::Windows(x), + NativeKeyCode::Xkb(x) => NativeKey::Xkb(x), + } + } +} + +impl PartialEq for NativeKeyCode { + #[allow(clippy::cmp_owned)] // uses less code than direct match; target is stack allocated + #[inline] + fn eq(&self, rhs: &NativeKey) -> bool { + NativeKey::from(*self) == *rhs + } +} + +impl PartialEq for NativeKey { + #[inline] + fn eq(&self, rhs: &NativeKeyCode) -> bool { + rhs == self + } +} + +/// Represents the location of a physical key. +/// +/// This type is a superset of [`KeyCode`], including an [`Unidentified`][Self::Unidentified] +/// variant. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PhysicalKey { + /// A known key code + Code(KeyCode), + /// This variant is used when the key cannot be translated to a [`KeyCode`] + /// + /// The native keycode is provided (if available) so you're able to more reliably match + /// key-press and key-release events by hashing the [`PhysicalKey`]. It is also possible to use + /// this for keybinds for non-standard keys, but such keybinds are tied to a given platform. + Unidentified(NativeKeyCode), +} + +impl From for PhysicalKey { + #[inline] + fn from(code: KeyCode) -> Self { + PhysicalKey::Code(code) + } +} + +impl From for PhysicalKey { + #[inline] + fn from(code: NativeKeyCode) -> Self { + PhysicalKey::Unidentified(code) + } +} + +impl PartialEq for PhysicalKey { + #[inline] + fn eq(&self, rhs: &KeyCode) -> bool { + match self { + PhysicalKey::Code(ref code) => code == rhs, + _ => false, + } + } +} + +impl PartialEq for KeyCode { + #[inline] + fn eq(&self, rhs: &PhysicalKey) -> bool { + rhs == self + } +} + +impl PartialEq for PhysicalKey { + #[inline] + fn eq(&self, rhs: &NativeKeyCode) -> bool { + match self { + PhysicalKey::Unidentified(ref code) => code == rhs, + _ => false, + } + } +} + +impl PartialEq for NativeKeyCode { + #[inline] + fn eq(&self, rhs: &PhysicalKey) -> bool { + rhs == self + } +} + +/// Code representing the location of a physical key +/// +/// This mostly conforms to the UI Events Specification's [`KeyboardEvent.code`] with a few +/// exceptions: +/// - The keys that the specification calls "MetaLeft" and "MetaRight" are named "SuperLeft" and +/// "SuperRight" here. +/// - The key that the specification calls "Super" is reported as `Unidentified` here. +/// +/// [`KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum KeyCode { + /// ` on a US keyboard. This is also called a backtick or grave. + /// This is the 半角/全角/漢字 + /// (hankaku/zenkaku/kanji) key on Japanese keyboards + Backquote, + /// Used for both the US \\ (on the 101-key layout) and also for the key + /// located between the " and Enter keys on row C of the 102-, + /// 104- and 106-key layouts. + /// Labeled # on a UK (102) keyboard. + Backslash, + /// [ on a US keyboard. + BracketLeft, + /// ] on a US keyboard. + BracketRight, + /// , on a US keyboard. + Comma, + /// 0 on a US keyboard. + Digit0, + /// 1 on a US keyboard. + Digit1, + /// 2 on a US keyboard. + Digit2, + /// 3 on a US keyboard. + Digit3, + /// 4 on a US keyboard. + Digit4, + /// 5 on a US keyboard. + Digit5, + /// 6 on a US keyboard. + Digit6, + /// 7 on a US keyboard. + Digit7, + /// 8 on a US keyboard. + Digit8, + /// 9 on a US keyboard. + Digit9, + /// = on a US keyboard. + Equal, + /// Located between the left Shift and Z keys. + /// Labeled \\ on a UK keyboard. + IntlBackslash, + /// Located between the / and right Shift keys. + /// Labeled \\ (ro) on a Japanese keyboard. + IntlRo, + /// Located between the = and Backspace keys. + /// Labeled ¥ (yen) on a Japanese keyboard. \\ on a + /// Russian keyboard. + IntlYen, + /// a on a US keyboard. + /// Labeled q on an AZERTY (e.g., French) keyboard. + KeyA, + /// b on a US keyboard. + KeyB, + /// c on a US keyboard. + KeyC, + /// d on a US keyboard. + KeyD, + /// e on a US keyboard. + KeyE, + /// f on a US keyboard. + KeyF, + /// g on a US keyboard. + KeyG, + /// h on a US keyboard. + KeyH, + /// i on a US keyboard. + KeyI, + /// j on a US keyboard. + KeyJ, + /// k on a US keyboard. + KeyK, + /// l on a US keyboard. + KeyL, + /// m on a US keyboard. + KeyM, + /// n on a US keyboard. + KeyN, + /// o on a US keyboard. + KeyO, + /// p on a US keyboard. + KeyP, + /// q on a US keyboard. + /// Labeled a on an AZERTY (e.g., French) keyboard. + KeyQ, + /// r on a US keyboard. + KeyR, + /// s on a US keyboard. + KeyS, + /// t on a US keyboard. + KeyT, + /// u on a US keyboard. + KeyU, + /// v on a US keyboard. + KeyV, + /// w on a US keyboard. + /// Labeled z on an AZERTY (e.g., French) keyboard. + KeyW, + /// x on a US keyboard. + KeyX, + /// y on a US keyboard. + /// Labeled z on a QWERTZ (e.g., German) keyboard. + KeyY, + /// z on a US keyboard. + /// Labeled w on an AZERTY (e.g., French) keyboard, and y on a + /// QWERTZ (e.g., German) keyboard. + KeyZ, + /// - on a US keyboard. + Minus, + /// . on a US keyboard. + Period, + /// ' on a US keyboard. + Quote, + /// ; on a US keyboard. + Semicolon, + /// / on a US keyboard. + Slash, + /// Alt, Option, or ⌥. + AltLeft, + /// Alt, Option, or ⌥. + /// This is labeled AltGr on many keyboard layouts. + AltRight, + /// Backspace or ⌫. + /// Labeled Delete on Apple keyboards. + Backspace, + /// CapsLock or ⇪ + CapsLock, + /// The application context menu key, which is typically found between the right + /// Super key and the right Control key. + ContextMenu, + /// Control or ⌃ + ControlLeft, + /// Control or ⌃ + ControlRight, + /// Enter or ↵. Labeled Return on Apple keyboards. + Enter, + /// The Windows, ⌘, Command, or other OS symbol key. + SuperLeft, + /// The Windows, ⌘, Command, or other OS symbol key. + SuperRight, + /// Shift or ⇧ + ShiftLeft, + /// Shift or ⇧ + ShiftRight, + ///   (space) + Space, + /// Tab or ⇥ + Tab, + /// Japanese: 変 (henkan) + Convert, + /// Japanese: カタカナ/ひらがな/ローマ字 + /// (katakana/hiragana/romaji) + KanaMode, + /// Korean: HangulMode 한/영 (han/yeong) + /// + /// Japanese (Mac keyboard): か (kana) + Lang1, + /// Korean: Hanja 한 (hanja) + /// + /// Japanese (Mac keyboard): 英 (eisu) + Lang2, + /// Japanese (word-processing keyboard): Katakana + Lang3, + /// Japanese (word-processing keyboard): Hiragana + Lang4, + /// Japanese (word-processing keyboard): Zenkaku/Hankaku + Lang5, + /// Japanese: 無変換 (muhenkan) + NonConvert, + /// ⌦. The forward delete key. + /// Note that on Apple keyboards, the key labelled Delete on the main part of + /// the keyboard is encoded as [`Backspace`]. + /// + /// [`Backspace`]: Self::Backspace + Delete, + /// Page Down, End, or ↘ + End, + /// Help. Not present on standard PC keyboards. + Help, + /// Home or ↖ + Home, + /// Insert or Ins. Not present on Apple keyboards. + Insert, + /// Page Down, PgDn, or ⇟ + PageDown, + /// Page Up, PgUp, or ⇞ + PageUp, + /// ↓ + ArrowDown, + /// ← + ArrowLeft, + /// → + ArrowRight, + /// ↑ + ArrowUp, + /// On the Mac, this is used for the numpad Clear key. + NumLock, + /// 0 Ins on a keyboard. 0 on a phone or remote control + Numpad0, + /// 1 End on a keyboard. 1 or 1 QZ on a phone or remote + /// control + Numpad1, + /// 2 ↓ on a keyboard. 2 ABC on a phone or remote control + Numpad2, + /// 3 PgDn on a keyboard. 3 DEF on a phone or remote control + Numpad3, + /// 4 ← on a keyboard. 4 GHI on a phone or remote control + Numpad4, + /// 5 on a keyboard. 5 JKL on a phone or remote control + Numpad5, + /// 6 → on a keyboard. 6 MNO on a phone or remote control + Numpad6, + /// 7 Home on a keyboard. 7 PQRS or 7 PRS on a phone + /// or remote control + Numpad7, + /// 8 ↑ on a keyboard. 8 TUV on a phone or remote control + Numpad8, + /// 9 PgUp on a keyboard. 9 WXYZ or 9 WXY on a phone + /// or remote control + Numpad9, + /// + + NumpadAdd, + /// Found on the Microsoft Natural Keyboard. + NumpadBackspace, + /// C or A (All Clear). Also for use with numpads that have a + /// Clear key that is separate from the NumLock key. On the Mac, the + /// numpad Clear key is encoded as [`NumLock`]. + /// + /// [`NumLock`]: Self::NumLock + NumpadClear, + /// C (Clear Entry) + NumpadClearEntry, + /// , (thousands separator). For locales where the thousands separator + /// is a "." (e.g., Brazil), this key may generate a .. + NumpadComma, + /// . Del. For locales where the decimal separator is "," (e.g., + /// Brazil), this key may generate a ,. + NumpadDecimal, + /// / + NumpadDivide, + NumpadEnter, + /// = + NumpadEqual, + /// # on a phone or remote control device. This key is typically found + /// below the 9 key and to the right of the 0 key. + NumpadHash, + /// M Add current entry to the value stored in memory. + NumpadMemoryAdd, + /// M Clear the value stored in memory. + NumpadMemoryClear, + /// M Replace the current entry with the value stored in memory. + NumpadMemoryRecall, + /// M Replace the value stored in memory with the current entry. + NumpadMemoryStore, + /// M Subtract current entry from the value stored in memory. + NumpadMemorySubtract, + /// * on a keyboard. For use with numpads that provide mathematical + /// operations (+, - * and /). + /// + /// Use `NumpadStar` for the * key on phones and remote controls. + NumpadMultiply, + /// ( Found on the Microsoft Natural Keyboard. + NumpadParenLeft, + /// ) Found on the Microsoft Natural Keyboard. + NumpadParenRight, + /// * on a phone or remote control device. + /// + /// This key is typically found below the 7 key and to the left of + /// the 0 key. + /// + /// Use "NumpadMultiply" for the * key on + /// numeric keypads. + NumpadStar, + /// - + NumpadSubtract, + /// Esc or ⎋ + Escape, + /// Fn This is typically a hardware key that does not generate a separate code. + Fn, + /// FLock or FnLock. Function Lock key. Found on the Microsoft + /// Natural Keyboard. + FnLock, + /// PrtScr SysRq or Print Screen + PrintScreen, + /// Scroll Lock + ScrollLock, + /// Pause Break + Pause, + /// Some laptops place this key to the left of the ↑ key. + /// + /// This also the "back" button (triangle) on Android. + BrowserBack, + BrowserFavorites, + /// Some laptops place this key to the right of the ↑ key. + BrowserForward, + /// The "home" button on Android. + BrowserHome, + BrowserRefresh, + BrowserSearch, + BrowserStop, + /// Eject or ⏏. This key is placed in the function section on some Apple + /// keyboards. + Eject, + /// Sometimes labelled My Computer on the keyboard + LaunchApp1, + /// Sometimes labelled Calculator on the keyboard + LaunchApp2, + LaunchMail, + MediaPlayPause, + MediaSelect, + MediaStop, + MediaTrackNext, + MediaTrackPrevious, + /// This key is placed in the function section on some Apple keyboards, replacing the + /// Eject key. + Power, + Sleep, + AudioVolumeDown, + AudioVolumeMute, + AudioVolumeUp, + WakeUp, + // Legacy modifier key. Also called "Super" in certain places. + Meta, + // Legacy modifier key. + Hyper, + Turbo, + Abort, + Resume, + Suspend, + /// Found on Sun’s USB keyboard. + Again, + /// Found on Sun’s USB keyboard. + Copy, + /// Found on Sun’s USB keyboard. + Cut, + /// Found on Sun’s USB keyboard. + Find, + /// Found on Sun’s USB keyboard. + Open, + /// Found on Sun’s USB keyboard. + Paste, + /// Found on Sun’s USB keyboard. + Props, + /// Found on Sun’s USB keyboard. + Select, + /// Found on Sun’s USB keyboard. + Undo, + /// Use for dedicated ひらがな key found on some Japanese word processing keyboards. + Hiragana, + /// Use for dedicated カタカナ key found on some Japanese word processing keyboards. + Katakana, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F1, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F2, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F3, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F4, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F5, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F6, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F7, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F8, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F9, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F10, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F11, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F12, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F13, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F14, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F15, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F16, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F17, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F18, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F19, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F20, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F21, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F22, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F23, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F24, + /// General-purpose function key. + F25, + /// General-purpose function key. + F26, + /// General-purpose function key. + F27, + /// General-purpose function key. + F28, + /// General-purpose function key. + F29, + /// General-purpose function key. + F30, + /// General-purpose function key. + F31, + /// General-purpose function key. + F32, + /// General-purpose function key. + F33, + /// General-purpose function key. + F34, + /// General-purpose function key. + F35, +} + +/// A [`Key::Named`] value +/// +/// This mostly conforms to the UI Events Specification's [`KeyboardEvent.key`] with a few +/// exceptions: +/// - The `Super` variant here, is named `Meta` in the aforementioned specification. (There's +/// another key which the specification calls `Super`. That does not exist here.) +/// - The `Space` variant here, can be identified by the character it generates in the +/// specification. +/// +/// [`KeyboardEvent.key`]: https://w3c.github.io/uievents-key/ +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum NamedKey { + /// The `Alt` (Alternative) key. + /// + /// This key enables the alternate modifier function for interpreting concurrent or subsequent + /// keyboard input. This key value is also used for the Apple Option key. + Alt, + /// The Alternate Graphics (AltGr or AltGraph) key. + /// + /// This key is used enable the ISO Level 3 shift modifier (the standard `Shift` key is the + /// level 2 modifier). + AltGraph, + /// The `Caps Lock` (Capital) key. + /// + /// Toggle capital character lock function for interpreting subsequent keyboard input event. + CapsLock, + /// The `Control` or `Ctrl` key. + /// + /// Used to enable control modifier function for interpreting concurrent or subsequent keyboard + /// input. + Control, + /// The Function switch `Fn` key. Activating this key simultaneously with another key changes + /// that key’s value to an alternate character or function. This key is often handled directly + /// in the keyboard hardware and does not usually generate key events. + Fn, + /// The Function-Lock (`FnLock` or `F-Lock`) key. Activating this key switches the mode of the + /// keyboard to changes some keys' values to an alternate character or function. This key is + /// often handled directly in the keyboard hardware and does not usually generate key events. + FnLock, + /// The `NumLock` or Number Lock key. Used to toggle numpad mode function for interpreting + /// subsequent keyboard input. + NumLock, + /// Toggle between scrolling and cursor movement modes. + ScrollLock, + /// Used to enable shift modifier function for interpreting concurrent or subsequent keyboard + /// input. + Shift, + /// The Symbol modifier key (used on some virtual keyboards). + Symbol, + SymbolLock, + // Legacy modifier key. Also called "Super" in certain places. + Meta, + // Legacy modifier key. + Hyper, + /// Used to enable "super" modifier function for interpreting concurrent or subsequent keyboard + /// input. This key value is used for the "Windows Logo" key and the Apple `Command` or `⌘` + /// key. + /// + /// Note: In some contexts (e.g. the Web) this is referred to as the "Meta" key. + Super, + /// The `Enter` or `↵` key. Used to activate current selection or accept current input. This + /// key value is also used for the `Return` (Macintosh numpad) key. This key value is also + /// used for the Android `KEYCODE_DPAD_CENTER`. + Enter, + /// The Horizontal Tabulation `Tab` key. + Tab, + /// Used in text to insert a space between words. Usually located below the character keys. + Space, + /// Navigate or traverse downward. (`KEYCODE_DPAD_DOWN`) + ArrowDown, + /// Navigate or traverse leftward. (`KEYCODE_DPAD_LEFT`) + ArrowLeft, + /// Navigate or traverse rightward. (`KEYCODE_DPAD_RIGHT`) + ArrowRight, + /// Navigate or traverse upward. (`KEYCODE_DPAD_UP`) + ArrowUp, + /// The End key, used with keyboard entry to go to the end of content (`KEYCODE_MOVE_END`). + End, + /// The Home key, used with keyboard entry, to go to start of content (`KEYCODE_MOVE_HOME`). + /// For the mobile phone `Home` key (which goes to the phone’s main screen), use [`GoHome`]. + /// + /// [`GoHome`]: Self::GoHome + Home, + /// Scroll down or display next page of content. + PageDown, + /// Scroll up or display previous page of content. + PageUp, + /// Used to remove the character to the left of the cursor. This key value is also used for + /// the key labeled `Delete` on MacOS keyboards. + Backspace, + /// Remove the currently selected input. + Clear, + /// Copy the current selection. (`APPCOMMAND_COPY`) + Copy, + /// The Cursor Select key. + CrSel, + /// Cut the current selection. (`APPCOMMAND_CUT`) + Cut, + /// Used to delete the character to the right of the cursor. This key value is also used for + /// the key labeled `Delete` on MacOS keyboards when `Fn` is active. + Delete, + /// The Erase to End of Field key. This key deletes all characters from the current cursor + /// position to the end of the current field. + EraseEof, + /// The Extend Selection (Exsel) key. + ExSel, + /// Toggle between text modes for insertion or overtyping. + /// (`KEYCODE_INSERT`) + Insert, + /// The Paste key. (`APPCOMMAND_PASTE`) + Paste, + /// Redo the last action. (`APPCOMMAND_REDO`) + Redo, + /// Undo the last action. (`APPCOMMAND_UNDO`) + Undo, + /// The Accept (Commit, OK) key. Accept current option or input method sequence conversion. + Accept, + /// Redo or repeat an action. + Again, + /// The Attention (Attn) key. + Attn, + Cancel, + /// Show the application’s context menu. + /// This key is commonly found between the right `Super` key and the right `Control` key. + ContextMenu, + /// The `Esc` key. This key was originally used to initiate an escape sequence, but is + /// now more generally used to exit or "escape" the current context, such as closing a dialog + /// or exiting full screen mode. + Escape, + Execute, + /// Open the Find dialog. (`APPCOMMAND_FIND`) + Find, + /// Open a help dialog or toggle display of help information. (`APPCOMMAND_HELP`, + /// `KEYCODE_HELP`) + Help, + /// Pause the current state or application (as appropriate). + /// + /// Note: Do not use this value for the `Pause` button on media controllers. Use `"MediaPause"` + /// instead. + Pause, + /// Play or resume the current state or application (as appropriate). + /// + /// Note: Do not use this value for the `Play` button on media controllers. Use `"MediaPlay"` + /// instead. + Play, + /// The properties (Props) key. + Props, + Select, + /// The ZoomIn key. (`KEYCODE_ZOOM_IN`) + ZoomIn, + /// The ZoomOut key. (`KEYCODE_ZOOM_OUT`) + ZoomOut, + /// The Brightness Down key. Typically controls the display brightness. + /// (`KEYCODE_BRIGHTNESS_DOWN`) + BrightnessDown, + /// The Brightness Up key. Typically controls the display brightness. (`KEYCODE_BRIGHTNESS_UP`) + BrightnessUp, + /// Toggle removable media to eject (open) and insert (close) state. (`KEYCODE_MEDIA_EJECT`) + Eject, + LogOff, + /// Toggle power state. (`KEYCODE_POWER`) + /// Note: Note: Some devices might not expose this key to the operating environment. + Power, + /// The `PowerOff` key. Sometime called `PowerDown`. + PowerOff, + /// Initiate print-screen function. + PrintScreen, + /// The Hibernate key. This key saves the current state of the computer to disk so that it can + /// be restored. The computer will then shutdown. + Hibernate, + /// The Standby key. This key turns off the display and places the computer into a low-power + /// mode without completely shutting down. It is sometimes labelled `Suspend` or `Sleep` key. + /// (`KEYCODE_SLEEP`) + Standby, + /// The WakeUp key. (`KEYCODE_WAKEUP`) + WakeUp, + /// Initiate the multi-candidate mode. + AllCandidates, + Alphanumeric, + /// Initiate the Code Input mode to allow characters to be entered by + /// their code points. + CodeInput, + /// The Compose key, also known as "Multi_key" on the X Window System. This key acts in a + /// manner similar to a dead key, triggering a mode where subsequent key presses are combined + /// to produce a different character. + Compose, + /// Convert the current input method sequence. + Convert, + /// The Final Mode `Final` key used on some Asian keyboards, to enable the final mode for IMEs. + FinalMode, + /// Switch to the first character group. (ISO/IEC 9995) + GroupFirst, + /// Switch to the last character group. (ISO/IEC 9995) + GroupLast, + /// Switch to the next character group. (ISO/IEC 9995) + GroupNext, + /// Switch to the previous character group. (ISO/IEC 9995) + GroupPrevious, + /// Toggle between or cycle through input modes of IMEs. + ModeChange, + NextCandidate, + /// Accept current input method sequence without + /// conversion in IMEs. + NonConvert, + PreviousCandidate, + Process, + SingleCandidate, + /// Toggle between Hangul and English modes. + HangulMode, + HanjaMode, + JunjaMode, + /// The Eisu key. This key may close the IME, but its purpose is defined by the current IME. + /// (`KEYCODE_EISU`) + Eisu, + /// The (Half-Width) Characters key. + Hankaku, + /// The Hiragana (Japanese Kana characters) key. + Hiragana, + /// The Hiragana/Katakana toggle key. (`KEYCODE_KATAKANA_HIRAGANA`) + HiraganaKatakana, + /// The Kana Mode (Kana Lock) key. This key is used to enter hiragana mode (typically from + /// romaji mode). + KanaMode, + /// The Kanji (Japanese name for ideographic characters of Chinese origin) Mode key. This key + /// is typically used to switch to a hiragana keyboard for the purpose of converting input + /// into kanji. (`KEYCODE_KANA`) + KanjiMode, + /// The Katakana (Japanese Kana characters) key. + Katakana, + /// The Roman characters function key. + Romaji, + /// The Zenkaku (Full-Width) Characters key. + Zenkaku, + /// The Zenkaku/Hankaku (full-width/half-width) toggle key. (`KEYCODE_ZENKAKU_HANKAKU`) + ZenkakuHankaku, + /// General purpose virtual function key, as index 1. + Soft1, + /// General purpose virtual function key, as index 2. + Soft2, + /// General purpose virtual function key, as index 3. + Soft3, + /// General purpose virtual function key, as index 4. + Soft4, + /// Select next (numerically or logically) lower channel. (`APPCOMMAND_MEDIA_CHANNEL_DOWN`, + /// `KEYCODE_CHANNEL_DOWN`) + ChannelDown, + /// Select next (numerically or logically) higher channel. (`APPCOMMAND_MEDIA_CHANNEL_UP`, + /// `KEYCODE_CHANNEL_UP`) + ChannelUp, + /// Close the current document or message (Note: This doesn’t close the application). + /// (`APPCOMMAND_CLOSE`) + Close, + /// Open an editor to forward the current message. (`APPCOMMAND_FORWARD_MAIL`) + MailForward, + /// Open an editor to reply to the current message. (`APPCOMMAND_REPLY_TO_MAIL`) + MailReply, + /// Send the current message. (`APPCOMMAND_SEND_MAIL`) + MailSend, + /// Close the current media, for example to close a CD or DVD tray. (`KEYCODE_MEDIA_CLOSE`) + MediaClose, + /// Initiate or continue forward playback at faster than normal speed, or increase speed if + /// already fast forwarding. (`APPCOMMAND_MEDIA_FAST_FORWARD`, `KEYCODE_MEDIA_FAST_FORWARD`) + MediaFastForward, + /// Pause the currently playing media. (`APPCOMMAND_MEDIA_PAUSE`, `KEYCODE_MEDIA_PAUSE`) + /// + /// Note: Media controller devices should use this value rather than `"Pause"` for their pause + /// keys. + MediaPause, + /// Initiate or continue media playback at normal speed, if not currently playing at normal + /// speed. (`APPCOMMAND_MEDIA_PLAY`, `KEYCODE_MEDIA_PLAY`) + MediaPlay, + /// Toggle media between play and pause states. (`APPCOMMAND_MEDIA_PLAY_PAUSE`, + /// `KEYCODE_MEDIA_PLAY_PAUSE`) + MediaPlayPause, + /// Initiate or resume recording of currently selected media. (`APPCOMMAND_MEDIA_RECORD`, + /// `KEYCODE_MEDIA_RECORD`) + MediaRecord, + /// Initiate or continue reverse playback at faster than normal speed, or increase speed if + /// already rewinding. (`APPCOMMAND_MEDIA_REWIND`, `KEYCODE_MEDIA_REWIND`) + MediaRewind, + /// Stop media playing, pausing, forwarding, rewinding, or recording, if not already stopped. + /// (`APPCOMMAND_MEDIA_STOP`, `KEYCODE_MEDIA_STOP`) + MediaStop, + /// Seek to next media or program track. (`APPCOMMAND_MEDIA_NEXTTRACK`, `KEYCODE_MEDIA_NEXT`) + MediaTrackNext, + /// Seek to previous media or program track. (`APPCOMMAND_MEDIA_PREVIOUSTRACK`, + /// `KEYCODE_MEDIA_PREVIOUS`) + MediaTrackPrevious, + /// Open a new document or message. (`APPCOMMAND_NEW`) + New, + /// Open an existing document or message. (`APPCOMMAND_OPEN`) + Open, + /// Print the current document or message. (`APPCOMMAND_PRINT`) + Print, + /// Save the current document or message. (`APPCOMMAND_SAVE`) + Save, + /// Spellcheck the current document or selection. (`APPCOMMAND_SPELL_CHECK`) + SpellCheck, + /// The `11` key found on media numpads that + /// have buttons from `1` ... `12`. + Key11, + /// The `12` key found on media numpads that + /// have buttons from `1` ... `12`. + Key12, + /// Adjust audio balance leftward. (`VK_AUDIO_BALANCE_LEFT`) + AudioBalanceLeft, + /// Adjust audio balance rightward. (`VK_AUDIO_BALANCE_RIGHT`) + AudioBalanceRight, + /// Decrease audio bass boost or cycle down through bass boost states. (`APPCOMMAND_BASS_DOWN`, + /// `VK_BASS_BOOST_DOWN`) + AudioBassBoostDown, + /// Toggle bass boost on/off. (`APPCOMMAND_BASS_BOOST`) + AudioBassBoostToggle, + /// Increase audio bass boost or cycle up through bass boost states. (`APPCOMMAND_BASS_UP`, + /// `VK_BASS_BOOST_UP`) + AudioBassBoostUp, + /// Adjust audio fader towards front. (`VK_FADER_FRONT`) + AudioFaderFront, + /// Adjust audio fader towards rear. (`VK_FADER_REAR`) + AudioFaderRear, + /// Advance surround audio mode to next available mode. (`VK_SURROUND_MODE_NEXT`) + AudioSurroundModeNext, + /// Decrease treble. (`APPCOMMAND_TREBLE_DOWN`) + AudioTrebleDown, + /// Increase treble. (`APPCOMMAND_TREBLE_UP`) + AudioTrebleUp, + /// Decrease audio volume. (`APPCOMMAND_VOLUME_DOWN`, `KEYCODE_VOLUME_DOWN`) + AudioVolumeDown, + /// Increase audio volume. (`APPCOMMAND_VOLUME_UP`, `KEYCODE_VOLUME_UP`) + AudioVolumeUp, + /// Toggle between muted state and prior volume level. (`APPCOMMAND_VOLUME_MUTE`, + /// `KEYCODE_VOLUME_MUTE`) + AudioVolumeMute, + /// Toggle the microphone on/off. (`APPCOMMAND_MIC_ON_OFF_TOGGLE`) + MicrophoneToggle, + /// Decrease microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_DOWN`) + MicrophoneVolumeDown, + /// Increase microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_UP`) + MicrophoneVolumeUp, + /// Mute the microphone. (`APPCOMMAND_MICROPHONE_VOLUME_MUTE`, `KEYCODE_MUTE`) + MicrophoneVolumeMute, + /// Show correction list when a word is incorrectly identified. (`APPCOMMAND_CORRECTION_LIST`) + SpeechCorrectionList, + /// Toggle between dictation mode and command/control mode. + /// (`APPCOMMAND_DICTATE_OR_COMMAND_CONTROL_TOGGLE`) + SpeechInputToggle, + /// The first generic "LaunchApplication" key. This is commonly associated with launching "My + /// Computer", and may have a computer symbol on the key. (`APPCOMMAND_LAUNCH_APP1`) + LaunchApplication1, + /// The second generic "LaunchApplication" key. This is commonly associated with launching + /// "Calculator", and may have a calculator symbol on the key. (`APPCOMMAND_LAUNCH_APP2`, + /// `KEYCODE_CALCULATOR`) + LaunchApplication2, + /// The "Calendar" key. (`KEYCODE_CALENDAR`) + LaunchCalendar, + /// The "Contacts" key. (`KEYCODE_CONTACTS`) + LaunchContacts, + /// The "Mail" key. (`APPCOMMAND_LAUNCH_MAIL`) + LaunchMail, + /// The "Media Player" key. (`APPCOMMAND_LAUNCH_MEDIA_SELECT`) + LaunchMediaPlayer, + LaunchMusicPlayer, + LaunchPhone, + LaunchScreenSaver, + LaunchSpreadsheet, + LaunchWebBrowser, + LaunchWebCam, + LaunchWordProcessor, + /// Navigate to previous content or page in current history. (`APPCOMMAND_BROWSER_BACKWARD`) + BrowserBack, + /// Open the list of browser favorites. (`APPCOMMAND_BROWSER_FAVORITES`) + BrowserFavorites, + /// Navigate to next content or page in current history. (`APPCOMMAND_BROWSER_FORWARD`) + BrowserForward, + /// Go to the user’s preferred home page. (`APPCOMMAND_BROWSER_HOME`) + BrowserHome, + /// Refresh the current page or content. (`APPCOMMAND_BROWSER_REFRESH`) + BrowserRefresh, + /// Call up the user’s preferred search page. (`APPCOMMAND_BROWSER_SEARCH`) + BrowserSearch, + /// Stop loading the current page or content. (`APPCOMMAND_BROWSER_STOP`) + BrowserStop, + /// The Application switch key, which provides a list of recent apps to switch between. + /// (`KEYCODE_APP_SWITCH`) + AppSwitch, + /// The Call key. (`KEYCODE_CALL`) + Call, + /// The Camera key. (`KEYCODE_CAMERA`) + Camera, + /// The Camera focus key. (`KEYCODE_FOCUS`) + CameraFocus, + /// The End Call key. (`KEYCODE_ENDCALL`) + EndCall, + /// The Back key. (`KEYCODE_BACK`) + GoBack, + /// The Home key, which goes to the phone’s main screen. (`KEYCODE_HOME`) + GoHome, + /// The Headset Hook key. (`KEYCODE_HEADSETHOOK`) + HeadsetHook, + LastNumberRedial, + /// The Notification key. (`KEYCODE_NOTIFICATION`) + Notification, + /// Toggle between manner mode state: silent, vibrate, ring, ... (`KEYCODE_MANNER_MODE`) + MannerMode, + VoiceDial, + /// Switch to viewing TV. (`KEYCODE_TV`) + TV, + /// TV 3D Mode. (`KEYCODE_3D_MODE`) + TV3DMode, + /// Toggle between antenna and cable input. (`KEYCODE_TV_ANTENNA_CABLE`) + TVAntennaCable, + /// Audio description. (`KEYCODE_TV_AUDIO_DESCRIPTION`) + TVAudioDescription, + /// Audio description mixing volume down. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN`) + TVAudioDescriptionMixDown, + /// Audio description mixing volume up. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP`) + TVAudioDescriptionMixUp, + /// Contents menu. (`KEYCODE_TV_CONTENTS_MENU`) + TVContentsMenu, + /// Contents menu. (`KEYCODE_TV_DATA_SERVICE`) + TVDataService, + /// Switch the input mode on an external TV. (`KEYCODE_TV_INPUT`) + TVInput, + /// Switch to component input #1. (`KEYCODE_TV_INPUT_COMPONENT_1`) + TVInputComponent1, + /// Switch to component input #2. (`KEYCODE_TV_INPUT_COMPONENT_2`) + TVInputComponent2, + /// Switch to composite input #1. (`KEYCODE_TV_INPUT_COMPOSITE_1`) + TVInputComposite1, + /// Switch to composite input #2. (`KEYCODE_TV_INPUT_COMPOSITE_2`) + TVInputComposite2, + /// Switch to HDMI input #1. (`KEYCODE_TV_INPUT_HDMI_1`) + TVInputHDMI1, + /// Switch to HDMI input #2. (`KEYCODE_TV_INPUT_HDMI_2`) + TVInputHDMI2, + /// Switch to HDMI input #3. (`KEYCODE_TV_INPUT_HDMI_3`) + TVInputHDMI3, + /// Switch to HDMI input #4. (`KEYCODE_TV_INPUT_HDMI_4`) + TVInputHDMI4, + /// Switch to VGA input #1. (`KEYCODE_TV_INPUT_VGA_1`) + TVInputVGA1, + /// Media context menu. (`KEYCODE_TV_MEDIA_CONTEXT_MENU`) + TVMediaContext, + /// Toggle network. (`KEYCODE_TV_NETWORK`) + TVNetwork, + /// Number entry. (`KEYCODE_TV_NUMBER_ENTRY`) + TVNumberEntry, + /// Toggle the power on an external TV. (`KEYCODE_TV_POWER`) + TVPower, + /// Radio. (`KEYCODE_TV_RADIO_SERVICE`) + TVRadioService, + /// Satellite. (`KEYCODE_TV_SATELLITE`) + TVSatellite, + /// Broadcast Satellite. (`KEYCODE_TV_SATELLITE_BS`) + TVSatelliteBS, + /// Communication Satellite. (`KEYCODE_TV_SATELLITE_CS`) + TVSatelliteCS, + /// Toggle between available satellites. (`KEYCODE_TV_SATELLITE_SERVICE`) + TVSatelliteToggle, + /// Analog Terrestrial. (`KEYCODE_TV_TERRESTRIAL_ANALOG`) + TVTerrestrialAnalog, + /// Digital Terrestrial. (`KEYCODE_TV_TERRESTRIAL_DIGITAL`) + TVTerrestrialDigital, + /// Timer programming. (`KEYCODE_TV_TIMER_PROGRAMMING`) + TVTimer, + /// Switch the input mode on an external AVR (audio/video receiver). (`KEYCODE_AVR_INPUT`) + AVRInput, + /// Toggle the power on an external AVR (audio/video receiver). (`KEYCODE_AVR_POWER`) + AVRPower, + /// General purpose color-coded media function key, as index 0 (red). (`VK_COLORED_KEY_0`, + /// `KEYCODE_PROG_RED`) + ColorF0Red, + /// General purpose color-coded media function key, as index 1 (green). (`VK_COLORED_KEY_1`, + /// `KEYCODE_PROG_GREEN`) + ColorF1Green, + /// General purpose color-coded media function key, as index 2 (yellow). (`VK_COLORED_KEY_2`, + /// `KEYCODE_PROG_YELLOW`) + ColorF2Yellow, + /// General purpose color-coded media function key, as index 3 (blue). (`VK_COLORED_KEY_3`, + /// `KEYCODE_PROG_BLUE`) + ColorF3Blue, + /// General purpose color-coded media function key, as index 4 (grey). (`VK_COLORED_KEY_4`) + ColorF4Grey, + /// General purpose color-coded media function key, as index 5 (brown). (`VK_COLORED_KEY_5`) + ColorF5Brown, + /// Toggle the display of Closed Captions. (`VK_CC`, `KEYCODE_CAPTIONS`) + ClosedCaptionToggle, + /// Adjust brightness of device, by toggling between or cycling through states. (`VK_DIMMER`) + Dimmer, + /// Swap video sources. (`VK_DISPLAY_SWAP`) + DisplaySwap, + /// Select Digital Video Rrecorder. (`KEYCODE_DVR`) + DVR, + /// Exit the current application. (`VK_EXIT`) + Exit, + /// Clear program or content stored as favorite 0. (`VK_CLEAR_FAVORITE_0`) + FavoriteClear0, + /// Clear program or content stored as favorite 1. (`VK_CLEAR_FAVORITE_1`) + FavoriteClear1, + /// Clear program or content stored as favorite 2. (`VK_CLEAR_FAVORITE_2`) + FavoriteClear2, + /// Clear program or content stored as favorite 3. (`VK_CLEAR_FAVORITE_3`) + FavoriteClear3, + /// Select (recall) program or content stored as favorite 0. (`VK_RECALL_FAVORITE_0`) + FavoriteRecall0, + /// Select (recall) program or content stored as favorite 1. (`VK_RECALL_FAVORITE_1`) + FavoriteRecall1, + /// Select (recall) program or content stored as favorite 2. (`VK_RECALL_FAVORITE_2`) + FavoriteRecall2, + /// Select (recall) program or content stored as favorite 3. (`VK_RECALL_FAVORITE_3`) + FavoriteRecall3, + /// Store current program or content as favorite 0. (`VK_STORE_FAVORITE_0`) + FavoriteStore0, + /// Store current program or content as favorite 1. (`VK_STORE_FAVORITE_1`) + FavoriteStore1, + /// Store current program or content as favorite 2. (`VK_STORE_FAVORITE_2`) + FavoriteStore2, + /// Store current program or content as favorite 3. (`VK_STORE_FAVORITE_3`) + FavoriteStore3, + /// Toggle display of program or content guide. (`VK_GUIDE`, `KEYCODE_GUIDE`) + Guide, + /// If guide is active and displayed, then display next day’s content. (`VK_NEXT_DAY`) + GuideNextDay, + /// If guide is active and displayed, then display previous day’s content. (`VK_PREV_DAY`) + GuidePreviousDay, + /// Toggle display of information about currently selected context or media. (`VK_INFO`, + /// `KEYCODE_INFO`) + Info, + /// Toggle instant replay. (`VK_INSTANT_REPLAY`) + InstantReplay, + /// Launch linked content, if available and appropriate. (`VK_LINK`) + Link, + /// List the current program. (`VK_LIST`) + ListProgram, + /// Toggle display listing of currently available live content or programs. (`VK_LIVE`) + LiveContent, + /// Lock or unlock current content or program. (`VK_LOCK`) + Lock, + /// Show a list of media applications: audio/video players and image viewers. (`VK_APPS`) + /// + /// Note: Do not confuse this key value with the Windows' `VK_APPS` / `VK_CONTEXT_MENU` key, + /// which is encoded as `"ContextMenu"`. + MediaApps, + /// Audio track key. (`KEYCODE_MEDIA_AUDIO_TRACK`) + MediaAudioTrack, + /// Select previously selected channel or media. (`VK_LAST`, `KEYCODE_LAST_CHANNEL`) + MediaLast, + /// Skip backward to next content or program. (`KEYCODE_MEDIA_SKIP_BACKWARD`) + MediaSkipBackward, + /// Skip forward to next content or program. (`VK_SKIP`, `KEYCODE_MEDIA_SKIP_FORWARD`) + MediaSkipForward, + /// Step backward to next content or program. (`KEYCODE_MEDIA_STEP_BACKWARD`) + MediaStepBackward, + /// Step forward to next content or program. (`KEYCODE_MEDIA_STEP_FORWARD`) + MediaStepForward, + /// Media top menu. (`KEYCODE_MEDIA_TOP_MENU`) + MediaTopMenu, + /// Navigate in. (`KEYCODE_NAVIGATE_IN`) + NavigateIn, + /// Navigate to next key. (`KEYCODE_NAVIGATE_NEXT`) + NavigateNext, + /// Navigate out. (`KEYCODE_NAVIGATE_OUT`) + NavigateOut, + /// Navigate to previous key. (`KEYCODE_NAVIGATE_PREVIOUS`) + NavigatePrevious, + /// Cycle to next favorite channel (in favorites list). (`VK_NEXT_FAVORITE_CHANNEL`) + NextFavoriteChannel, + /// Cycle to next user profile (if there are multiple user profiles). (`VK_USER`) + NextUserProfile, + /// Access on-demand content or programs. (`VK_ON_DEMAND`) + OnDemand, + /// Pairing key to pair devices. (`KEYCODE_PAIRING`) + Pairing, + /// Move picture-in-picture window down. (`VK_PINP_DOWN`) + PinPDown, + /// Move picture-in-picture window. (`VK_PINP_MOVE`) + PinPMove, + /// Toggle display of picture-in-picture window. (`VK_PINP_TOGGLE`) + PinPToggle, + /// Move picture-in-picture window up. (`VK_PINP_UP`) + PinPUp, + /// Decrease media playback speed. (`VK_PLAY_SPEED_DOWN`) + PlaySpeedDown, + /// Reset playback to normal speed. (`VK_PLAY_SPEED_RESET`) + PlaySpeedReset, + /// Increase media playback speed. (`VK_PLAY_SPEED_UP`) + PlaySpeedUp, + /// Toggle random media or content shuffle mode. (`VK_RANDOM_TOGGLE`) + RandomToggle, + /// Not a physical key, but this key code is sent when the remote control battery is low. + /// (`VK_RC_LOW_BATTERY`) + RcLowBattery, + /// Toggle or cycle between media recording speeds. (`VK_RECORD_SPEED_NEXT`) + RecordSpeedNext, + /// Toggle RF (radio frequency) input bypass mode (pass RF input directly to the RF output). + /// (`VK_RF_BYPASS`) + RfBypass, + /// Toggle scan channels mode. (`VK_SCAN_CHANNELS_TOGGLE`) + ScanChannelsToggle, + /// Advance display screen mode to next available mode. (`VK_SCREEN_MODE_NEXT`) + ScreenModeNext, + /// Toggle display of device settings screen. (`VK_SETTINGS`, `KEYCODE_SETTINGS`) + Settings, + /// Toggle split screen mode. (`VK_SPLIT_SCREEN_TOGGLE`) + SplitScreenToggle, + /// Switch the input mode on an external STB (set top box). (`KEYCODE_STB_INPUT`) + STBInput, + /// Toggle the power on an external STB (set top box). (`KEYCODE_STB_POWER`) + STBPower, + /// Toggle display of subtitles, if available. (`VK_SUBTITLE`) + Subtitle, + /// Toggle display of teletext, if available (`VK_TELETEXT`, `KEYCODE_TV_TELETEXT`). + Teletext, + /// Advance video mode to next available mode. (`VK_VIDEO_MODE_NEXT`) + VideoModeNext, + /// Cause device to identify itself in some manner, e.g., audibly or visibly. (`VK_WINK`) + Wink, + /// Toggle between full-screen and scaled content, or alter magnification level. (`VK_ZOOM`, + /// `KEYCODE_TV_ZOOM_MODE`) + ZoomToggle, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F1, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F2, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F3, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F4, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F5, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F6, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F7, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F8, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F9, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F10, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F11, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F12, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F13, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F14, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F15, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F16, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F17, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F18, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F19, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F20, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F21, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F22, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F23, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F24, + /// General-purpose function key. + F25, + /// General-purpose function key. + F26, + /// General-purpose function key. + F27, + /// General-purpose function key. + F28, + /// General-purpose function key. + F29, + /// General-purpose function key. + F30, + /// General-purpose function key. + F31, + /// General-purpose function key. + F32, + /// General-purpose function key. + F33, + /// General-purpose function key. + F34, + /// General-purpose function key. + F35, +} + +/// Key represents the meaning of a keypress. +/// +/// This is a superset of the UI Events Specification's [`KeyboardEvent.key`] with +/// additions: +/// - All simple variants are wrapped under the `Named` variant +/// - The `Unidentified` variant here, can still identify a key through it's `NativeKeyCode`. +/// - The `Dead` variant here, can specify the character which is inserted when pressing the +/// dead-key twice. +/// +/// [`KeyboardEvent.key`]: https://w3c.github.io/uievents-key/ +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum Key { + /// A simple (unparameterised) action + Named(NamedKey), + + /// A key string that corresponds to the character typed by the user, taking into account the + /// user’s current locale setting, and any system-level keyboard mapping overrides that are in + /// effect. + Character(Str), + + /// This variant is used when the key cannot be translated to any other variant. + /// + /// The native key is provided (if available) in order to allow the user to specify keybindings + /// for keys which are not defined by this API, mainly through some sort of UI. + Unidentified(NativeKey), + + /// Contains the text representation of the dead-key when available. + /// + /// ## Platform-specific + /// - **Web:** Always contains `None` + Dead(Option), +} + +impl From for Key { + #[inline] + fn from(action: NamedKey) -> Self { + Key::Named(action) + } +} + +impl From for Key { + #[inline] + fn from(code: NativeKey) -> Self { + Key::Unidentified(code) + } +} + +impl PartialEq for Key { + #[inline] + fn eq(&self, rhs: &NamedKey) -> bool { + match self { + Key::Named(ref a) => a == rhs, + _ => false, + } + } +} + +impl> PartialEq for Key { + #[inline] + fn eq(&self, rhs: &str) -> bool { + match self { + Key::Character(ref s) => s == rhs, + _ => false, + } + } +} + +impl> PartialEq<&str> for Key { + #[inline] + fn eq(&self, rhs: &&str) -> bool { + self == *rhs + } +} + +impl PartialEq for Key { + #[inline] + fn eq(&self, rhs: &NativeKey) -> bool { + match self { + Key::Unidentified(ref code) => code == rhs, + _ => false, + } + } +} + +impl PartialEq> for NativeKey { + #[inline] + fn eq(&self, rhs: &Key) -> bool { + rhs == self + } +} + +impl Key { + /// Convert `Key::Character(SmolStr)` to `Key::Character(&str)` so you can more easily match on + /// `Key`. All other variants remain unchanged. + pub fn as_ref(&self) -> Key<&str> { + match self { + Key::Named(a) => Key::Named(*a), + Key::Character(ch) => Key::Character(ch.as_str()), + Key::Dead(d) => Key::Dead(*d), + Key::Unidentified(u) => Key::Unidentified(u.clone()), + } + } +} + +impl NamedKey { + /// Convert an action to its approximate textual equivalent. + /// + /// # Examples + /// + /// ``` + /// use rio_window::keyboard::NamedKey; + /// + /// assert_eq!(NamedKey::Enter.to_text(), Some("\r")); + /// assert_eq!(NamedKey::F20.to_text(), None); + /// ``` + pub fn to_text(&self) -> Option<&str> { + match self { + NamedKey::Enter => Some("\r"), + NamedKey::Backspace => Some("\x08"), + NamedKey::Tab => Some("\t"), + NamedKey::Space => Some(" "), + NamedKey::Escape => Some("\x1b"), + _ => None, + } + } +} + +impl Key { + /// Convert a key to its approximate textual equivalent. + /// + /// # Examples + /// + /// ``` + /// use rio_window::keyboard::{Key, NamedKey}; + /// + /// assert_eq!(Key::Character("a".into()).to_text(), Some("a")); + /// assert_eq!(Key::Named(NamedKey::Enter).to_text(), Some("\r")); + /// assert_eq!(Key::Named(NamedKey::F20).to_text(), None); + /// ``` + pub fn to_text(&self) -> Option<&str> { + match self { + Key::Named(action) => action.to_text(), + Key::Character(ch) => Some(ch.as_str()), + _ => None, + } + } +} + +/// The location of the key on the keyboard. +/// +/// Certain physical keys on the keyboard can have the same value, but are in different locations. +/// For instance, the Shift key can be on the left or right side of the keyboard, or the number +/// keys can be above the letters or on the numpad. This enum allows the user to differentiate +/// them. +/// +/// See the documentation for the [`location`] field on the [`KeyEvent`] struct for more +/// information. +/// +/// [`location`]: ../event/struct.KeyEvent.html#structfield.location +/// [`KeyEvent`]: crate::event::KeyEvent +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum KeyLocation { + /// The key is in its "normal" location on the keyboard. + /// + /// For instance, the "1" key above the "Q" key on a QWERTY keyboard will use this location. + /// This invariant is also returned when the location of the key cannot be identified. + /// + /// ![Standard 1 key](https://raw.githubusercontent.com/rust-windowing/winit/master/docs/res/keyboard_standard_1_key.svg) + /// + /// + /// For image attribution, see the + /// + /// ATTRIBUTION.md + /// + /// file. + /// + Standard, + + /// The key is on the left side of the keyboard. + /// + /// For instance, the left Shift key below the Caps Lock key on a QWERTY keyboard will use this + /// location. + /// + /// ![Left Shift key](https://raw.githubusercontent.com/rust-windowing/winit/master/docs/res/keyboard_left_shift_key.svg) + /// + /// + /// For image attribution, see the + /// + /// ATTRIBUTION.md + /// + /// file. + /// + Left, + + /// The key is on the right side of the keyboard. + /// + /// For instance, the right Shift key below the Enter key on a QWERTY keyboard will use this + /// location. + /// + /// ![Right Shift key](https://raw.githubusercontent.com/rust-windowing/winit/master/docs/res/keyboard_right_shift_key.svg) + /// + /// + /// For image attribution, see the + /// + /// ATTRIBUTION.md + /// + /// file. + /// + Right, + + /// The key is on the numpad. + /// + /// For instance, the "1" key on the numpad will use this location. + /// + /// ![Numpad 1 key](https://raw.githubusercontent.com/rust-windowing/winit/master/docs/res/keyboard_numpad_1_key.svg) + /// + /// + /// For image attribution, see the + /// + /// ATTRIBUTION.md + /// + /// file. + /// + Numpad, +} + +bitflags! { + /// Represents the current state of the keyboard modifiers + /// + /// Each flag represents a modifier and is set if this modifier is active. + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ModifiersState: u32 { + /// The "shift" key. + const SHIFT = 0b100; + /// The "control" key. + const CONTROL = 0b100 << 3; + /// The "alt" key. + const ALT = 0b100 << 6; + /// This is the "windows" key on PC and "command" key on Mac. + const SUPER = 0b100 << 9; + } +} + +impl ModifiersState { + /// Returns `true` if the shift key is pressed. + pub fn shift_key(&self) -> bool { + self.intersects(Self::SHIFT) + } + + /// Returns `true` if the control key is pressed. + pub fn control_key(&self) -> bool { + self.intersects(Self::CONTROL) + } + + /// Returns `true` if the alt key is pressed. + pub fn alt_key(&self) -> bool { + self.intersects(Self::ALT) + } + + /// Returns `true` if the super key is pressed. + pub fn super_key(&self) -> bool { + self.intersects(Self::SUPER) + } +} + +/// The state of the particular modifiers key. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum ModifiersKeyState { + /// The particular key is pressed. + Pressed, + /// The state of the key is unknown. + #[default] + Unknown, +} + +// NOTE: the exact modifier key is not used to represent modifiers state in the +// first place due to a fact that modifiers state could be changed without any +// key being pressed and on some platforms like Wayland/X11 which key resulted +// in modifiers change is hidden, also, not that it really matters. +// +// The reason this API is even exposed is mostly to provide a way for users +// to treat modifiers differently based on their position, which is required +// on macOS due to their AltGr/Option situation. +bitflags! { + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub(crate) struct ModifiersKeys: u8 { + const LSHIFT = 0b0000_0001; + const RSHIFT = 0b0000_0010; + const LCONTROL = 0b0000_0100; + const RCONTROL = 0b0000_1000; + const LALT = 0b0001_0000; + const RALT = 0b0010_0000; + const LSUPER = 0b0100_0000; + const RSUPER = 0b1000_0000; + } +} diff --git a/rio-window/src/lib.rs b/rio-window/src/lib.rs new file mode 100644 index 00000000..c6cb9936 --- /dev/null +++ b/rio-window/src/lib.rs @@ -0,0 +1,193 @@ +//! Winit is a cross-platform window creation and event loop management library. +//! +//! # Building windows +//! +//! Before you can create a [`Window`], you first need to build an [`EventLoop`]. This is done with +//! the [`EventLoop::new()`] function. +//! +//! ```no_run +//! use rio_window::event_loop::EventLoop; +//! let event_loop = EventLoop::new().unwrap(); +//! ``` +//! +//! Then you create a [`Window`] with [`create_window`]. +//! +//! # Event handling +//! +//! Once a [`Window`] has been created, it will generate different *events*. A [`Window`] object can +//! generate [`WindowEvent`]s when certain input events occur, such as a cursor moving over the +//! window or a key getting pressed while the window is focused. Devices can generate +//! [`DeviceEvent`]s, which contain unfiltered event data that isn't specific to a certain window. +//! Some user activity, like mouse movement, can generate both a [`WindowEvent`] *and* a +//! [`DeviceEvent`]. You can also create and handle your own custom [`Event::UserEvent`]s, if +//! desired. +//! +//! You can retrieve events by calling [`EventLoop::run_app()`]. This function will +//! dispatch events for every [`Window`] that was created with that particular [`EventLoop`], and +//! will run until [`exit()`] is used, at which point [`Event::LoopExiting`]. +//! +//! Winit no longer uses a `EventLoop::poll_events() -> impl Iterator`-based event loop +//! model, since that can't be implemented properly on some platforms (e.g web, iOS) and works +//! poorly on most other platforms. However, this model can be re-implemented to an extent with +#![cfg_attr( + any(windows_platform, macos_platform, x11_platform, wayland_platform), + doc = "[`EventLoopExtPumpEvents::pump_app_events()`][platform::pump_events::EventLoopExtPumpEvents::pump_app_events()]" +)] +#![cfg_attr( + not(any(windows_platform, macos_platform, x11_platform, wayland_platform)), + doc = "`EventLoopExtPumpEvents::pump_app_events()`" +)] +//! [^1]. See that method's documentation for more reasons about why +//! it's discouraged beyond compatibility reasons. +//! +//! +//! ```no_run +//! use rio_window::application::ApplicationHandler; +//! use rio_window::event::WindowEvent; +//! use rio_window::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +//! use rio_window::window::{Window, WindowId}; +//! +//! #[derive(Default)] +//! struct App { +//! window: Option, +//! } +//! +//! impl ApplicationHandler for App { +//! fn resumed(&mut self, event_loop: &ActiveEventLoop) { +//! self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); +//! } +//! +//! fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { +//! match event { +//! WindowEvent::CloseRequested => { +//! println!("The close button was pressed; stopping"); +//! event_loop.exit(); +//! }, +//! WindowEvent::RedrawRequested => { +//! // Redraw the application. +//! // +//! // It's preferable for applications that do not render continuously to render in +//! // this event rather than in AboutToWait, since rendering in here allows +//! // the program to gracefully handle redraws requested by the OS. +//! +//! // Draw. +//! +//! // Queue a RedrawRequested event. +//! // +//! // You only need to call this if you've determined that you need to redraw in +//! // applications which do not always need to. Applications that redraw continuously +//! // can render here instead. +//! self.window.as_ref().unwrap().request_redraw(); +//! } +//! _ => (), +//! } +//! } +//! } +//! +//! let event_loop = EventLoop::new().unwrap(); +//! +//! // ControlFlow::Poll continuously runs the event loop, even if the OS hasn't +//! // dispatched any events. This is ideal for games and similar applications. +//! event_loop.set_control_flow(ControlFlow::Poll); +//! +//! // ControlFlow::Wait pauses the event loop if no events are available to process. +//! // This is ideal for non-game applications that only update in response to user +//! // input, and uses significantly less power/CPU time than ControlFlow::Poll. +//! event_loop.set_control_flow(ControlFlow::Wait); +//! +//! let mut app = App::default(); +//! event_loop.run_app(&mut app); +//! ``` +//! +//! [`WindowEvent`] has a [`WindowId`] member. In multi-window environments, it should be +//! compared to the value returned by [`Window::id()`] to determine which [`Window`] +//! dispatched the event. +//! +//! # Drawing on the window +//! +//! Winit doesn't directly provide any methods for drawing on a [`Window`]. However, it allows you +//! to retrieve the raw handle of the window and display (see the [`platform`] module and/or the +//! [`raw_window_handle`] and [`raw_display_handle`] methods), which in turn allows +//! you to create an OpenGL/Vulkan/DirectX/Metal/etc. context that can be used to render graphics. +//! +//! Note that many platforms will display garbage data in the window's client area if the +//! application doesn't render anything to the window by the time the desktop compositor is ready to +//! display the window to the user. If you notice this happening, you should create the window with +//! [`visible` set to `false`][crate::window::WindowAttributes::with_visible] and explicitly make +//! the window visible only once you're ready to render into it. +//! +//! # UI scaling +//! +//! UI scaling is important, go read the docs for the [`dpi`] crate for an +//! introduction. +//! +//! All of Winit's functions return physical types, but can take either logical or physical +//! coordinates as input, allowing you to use the most convenient coordinate system for your +//! particular application. +//! +//! Winit will dispatch a [`ScaleFactorChanged`] event whenever a window's scale factor has changed. +//! This can happen if the user drags their window from a standard-resolution monitor to a high-DPI +//! monitor or if the user changes their DPI settings. This allows you to rescale your application's +//! UI elements and adjust how the platform changes the window's size to reflect the new scale +//! factor. If a window hasn't received a [`ScaleFactorChanged`] event, its scale factor +//! can be found by calling [`window.scale_factor()`]. +//! +//! [`ScaleFactorChanged`]: event::WindowEvent::ScaleFactorChanged +//! [`window.scale_factor()`]: window::Window::scale_factor +//! +//! # Cargo Features +//! +//! Winit provides the following Cargo features: +//! +//! * `x11` (enabled by default): On Unix platforms, enables the X11 backend. +//! * `wayland` (enabled by default): On Unix platforms, enables the Wayland backend. +//! * `rwh_06`: Implement `raw-window-handle v0.6` traits. +//! * `serde`: Enables serialization/deserialization of certain types with [Serde](https://crates.io/crates/serde). +//! * `mint`: Enables mint (math interoperability standard types) conversions. +//! +//! See the [`platform`] module for documentation on platform-specific cargo +//! features. +//! +//! [`EventLoop`]: event_loop::EventLoop +//! [`EventLoop::new()`]: event_loop::EventLoop::new +//! [`EventLoop::run_app()`]: event_loop::EventLoop::run_app +//! [`exit()`]: event_loop::ActiveEventLoop::exit +//! [`Window`]: window::Window +//! [`WindowId`]: window::WindowId +//! [`WindowAttributes`]: window::WindowAttributes +//! [window_new]: window::Window::new +//! [`create_window`]: event_loop::ActiveEventLoop::create_window +//! [`Window::id()`]: window::Window::id +//! [`WindowEvent`]: event::WindowEvent +//! [`DeviceEvent`]: event::DeviceEvent +//! [`Event::UserEvent`]: event::Event::UserEvent +//! [`Event::LoopExiting`]: event::Event::LoopExiting +//! [`raw_window_handle`]: ./window/struct.Window.html#method.raw_window_handle +//! [`raw_display_handle`]: ./window/struct.Window.html#method.raw_display_handle +//! [^1]: `EventLoopExtPumpEvents::pump_app_events()` is only available on Windows, macOS, Android, X11 and Wayland. + +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(unsafe_op_in_unsafe_fn)] +#![allow(clippy::missing_safety_doc)] + +#[cfg(feature = "rwh_06")] +pub use rwh_06 as raw_window_handle; + +// Re-export DPI types so that users don't have to put it in Cargo.toml. +#[doc(inline)] +pub use dpi; + +pub mod application; +#[macro_use] +pub mod error; +mod cursor; +pub mod event; +pub mod event_loop; +mod icon; +pub mod keyboard; +pub mod monitor; +mod platform_impl; +mod utils; +pub mod window; + +pub mod platform; diff --git a/rio-window/src/monitor.rs b/rio-window/src/monitor.rs new file mode 100644 index 00000000..621f3b6f --- /dev/null +++ b/rio-window/src/monitor.rs @@ -0,0 +1,171 @@ +//! Types useful for interacting with a user's monitors. +//! +//! If you want to get basic information about a monitor, you can use the +//! [`MonitorHandle`] type. This is retrieved from one of the following +//! methods, which return an iterator of [`MonitorHandle`]: +//! - [`ActiveEventLoop::available_monitors`][crate::event_loop::ActiveEventLoop::available_monitors]. +//! - [`Window::available_monitors`][crate::window::Window::available_monitors]. +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::platform_impl; + +/// Deprecated! Use `VideoModeHandle` instead. +#[deprecated = "Renamed to `VideoModeHandle`"] +pub type VideoMode = VideoModeHandle; + +/// Describes a fullscreen video mode of a monitor. +/// +/// Can be acquired with [`MonitorHandle::video_modes`]. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct VideoModeHandle { + pub(crate) video_mode: platform_impl::VideoModeHandle, +} + +impl std::fmt::Debug for VideoModeHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.video_mode.fmt(f) + } +} + +impl PartialOrd for VideoModeHandle { + fn partial_cmp(&self, other: &VideoModeHandle) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for VideoModeHandle { + fn cmp(&self, other: &VideoModeHandle) -> std::cmp::Ordering { + self.monitor().cmp(&other.monitor()).then( + self.size() + .cmp(&other.size()) + .then( + self.refresh_rate_millihertz() + .cmp(&other.refresh_rate_millihertz()) + .then(self.bit_depth().cmp(&other.bit_depth())), + ) + .reverse(), + ) + } +} + +impl VideoModeHandle { + /// Returns the resolution of this video mode. + #[inline] + pub fn size(&self) -> PhysicalSize { + self.video_mode.size() + } + + /// Returns the bit depth of this video mode, as in how many bits you have + /// available per color. This is generally 24 bits or 32 bits on modern + /// systems, depending on whether the alpha channel is counted or not. + /// + /// ## Platform-specific + /// + /// - **Wayland / Orbital:** Always returns 32. + /// - **iOS:** Always returns 32. + #[inline] + pub fn bit_depth(&self) -> u16 { + self.video_mode.bit_depth() + } + + /// Returns the refresh rate of this video mode in mHz. + #[inline] + pub fn refresh_rate_millihertz(&self) -> u32 { + self.video_mode.refresh_rate_millihertz() + } + + /// Returns the monitor that this video mode is valid for. Each monitor has + /// a separate set of valid video modes. + #[inline] + pub fn monitor(&self) -> MonitorHandle { + MonitorHandle { + inner: self.video_mode.monitor(), + } + } +} + +impl std::fmt::Display for VideoModeHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}x{} @ {} mHz ({} bpp)", + self.size().width, + self.size().height, + self.refresh_rate_millihertz(), + self.bit_depth() + ) + } +} + +/// Handle to a monitor. +/// +/// Allows you to retrieve information about a given monitor and can be used in [`Window`] creation. +/// +/// [`Window`]: crate::window::Window +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct MonitorHandle { + pub(crate) inner: platform_impl::MonitorHandle, +} + +impl MonitorHandle { + /// Returns a human-readable name of the monitor. + /// + /// Returns `None` if the monitor doesn't exist anymore. + #[inline] + pub fn name(&self) -> Option { + self.inner.name() + } + + /// Returns the monitor's resolution. + #[inline] + pub fn size(&self) -> PhysicalSize { + self.inner.size() + } + + /// Returns the top-left corner position of the monitor relative to the larger full + /// screen area. + #[inline] + pub fn position(&self) -> PhysicalPosition { + self.inner.position() + } + + /// The monitor refresh rate used by the system. + /// + /// Return `Some` if succeed, or `None` if failed, which usually happens when the monitor + /// the window is on is removed. + /// + /// When using exclusive fullscreen, the refresh rate of the [`VideoModeHandle`] that was + /// used to enter fullscreen should be used instead. + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + self.inner.refresh_rate_millihertz() + } + + /// Returns the scale factor of the underlying monitor. To map logical pixels to physical + /// pixels and vice versa, use [`Window::scale_factor`]. + /// + /// See the [`dpi`] module for more information. + /// + /// ## Platform-specific + /// + /// - **X11:** Can be overridden using the `WINIT_X11_SCALE_FACTOR` environment variable. + /// - **Wayland:** May differ from [`Window::scale_factor`]. + /// - **Android:** Always returns 1.0. + /// + /// [`Window::scale_factor`]: crate::window::Window::scale_factor + #[inline] + pub fn scale_factor(&self) -> f64 { + self.inner.scale_factor() + } + + /// Returns all fullscreen video modes supported by this monitor. + /// + /// ## Platform-specific + /// + /// - **Web:** Always returns an empty iterator + #[inline] + pub fn video_modes(&self) -> impl Iterator { + self.inner + .video_modes() + .map(|video_mode| VideoModeHandle { video_mode }) + } +} diff --git a/rio-window/src/platform/ios.rs b/rio-window/src/platform/ios.rs new file mode 100644 index 00000000..1cf27556 --- /dev/null +++ b/rio-window/src/platform/ios.rs @@ -0,0 +1,444 @@ +//! # iOS / UIKit +//! +//! Winit has an OS requirement of iOS 8 or higher, and is regularly tested on +//! iOS 9.3. +//! +//! iOS's main `UIApplicationMain` does some init work that's required by all +//! UI-related code (see issue [#1705]). It is best to create your windows +//! inside `Event::Resumed`. +//! +//! [#1705]: https://github.com/rust-windowing/winit/issues/1705 +//! +//! ## Building app +//! +//! To build ios app you will need rustc built for this targets: +//! +//! - armv7-apple-ios +//! - armv7s-apple-ios +//! - i386-apple-ios +//! - aarch64-apple-ios +//! - x86_64-apple-ios +//! +//! Then +//! +//! ``` +//! cargo build --target=... +//! ``` +//! The simplest way to integrate your app into xcode environment is to build it +//! as a static library. Wrap your main function and export it. +//! +//! ```rust, ignore +//! #[no_mangle] +//! pub extern fn start_winit_app() { +//! start_inner() +//! } +//! +//! fn start_inner() { +//! ... +//! } +//! ``` +//! +//! Compile project and then drag resulting .a into Xcode project. Add winit.h to xcode. +//! +//! ```ignore +//! void start_winit_app(); +//! ``` +//! +//! Use start_winit_app inside your xcode's main function. +//! +//! +//! ## App lifecycle and events +//! +//! iOS environment is very different from other platforms and you must be very +//! careful with it's events. Familiarize yourself with +//! [app lifecycle](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplicationDelegate_Protocol/). +//! +//! This is how those event are represented in winit: +//! +//! - applicationDidBecomeActive is Resumed +//! - applicationWillResignActive is Suspended +//! - applicationWillTerminate is LoopExiting +//! +//! Keep in mind that after LoopExiting event is received every attempt to draw with +//! opengl will result in segfault. +//! +//! Also note that app may not receive the LoopExiting event if suspended; it might be SIGKILL'ed. + +use std::os::raw::c_void; + +use crate::event_loop::EventLoop; +use crate::monitor::{MonitorHandle, VideoModeHandle}; +use crate::window::{Window, WindowAttributes}; + +/// Additional methods on [`EventLoop`] that are specific to iOS. +pub trait EventLoopExtIOS { + /// Returns the [`Idiom`] (phone/tablet/tv/etc) for the current device. + fn idiom(&self) -> Idiom; +} + +impl EventLoopExtIOS for EventLoop { + fn idiom(&self) -> Idiom { + self.event_loop.idiom() + } +} + +/// Additional methods on [`Window`] that are specific to iOS. +pub trait WindowExtIOS { + /// Sets the [`contentScaleFactor`] of the underlying [`UIWindow`] to `scale_factor`. + /// + /// The default value is device dependent, and it's recommended GLES or Metal applications set + /// this to [`MonitorHandle::scale_factor()`]. + /// + /// [`UIWindow`]: https://developer.apple.com/documentation/uikit/uiwindow?language=objc + /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc + fn set_scale_factor(&self, scale_factor: f64); + + /// Sets the valid orientations for the [`Window`]. + /// + /// The default value is [`ValidOrientations::LandscapeAndPortrait`]. + /// + /// This changes the value returned by + /// [`-[UIViewController supportedInterfaceOrientations]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621435-supportedinterfaceorientations?language=objc), + /// and then calls + /// [`-[UIViewController attemptRotationToDeviceOrientation]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621400-attemptrotationtodeviceorientati?language=objc). + fn set_valid_orientations(&self, valid_orientations: ValidOrientations); + + /// Sets whether the [`Window`] prefers the home indicator hidden. + /// + /// The default is to prefer showing the home indicator. + /// + /// This changes the value returned by + /// [`-[UIViewController prefersHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887510-prefershomeindicatorautohidden?language=objc), + /// and then calls + /// [`-[UIViewController setNeedsUpdateOfHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887509-setneedsupdateofhomeindicatoraut?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn set_prefers_home_indicator_hidden(&self, hidden: bool); + + /// Sets the screen edges for which the system gestures will take a lower priority than the + /// application's touch handling. + /// + /// This changes the value returned by + /// [`-[UIViewController preferredScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887512-preferredscreenedgesdeferringsys?language=objc), + /// and then calls + /// [`-[UIViewController setNeedsUpdateOfScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887507-setneedsupdateofscreenedgesdefer?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge); + + /// Sets whether the [`Window`] prefers the status bar hidden. + /// + /// The default is to prefer showing the status bar. + /// + /// This sets the value of the + /// [`prefersStatusBarHidden`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc) + /// property. + /// + /// [`setNeedsStatusBarAppearanceUpdate()`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621354-setneedsstatusbarappearanceupdat?language=objc) + /// is also called for you. + fn set_prefers_status_bar_hidden(&self, hidden: bool); + + /// Sets the preferred status bar style for the [`Window`]. + /// + /// The default is system-defined. + /// + /// This sets the value of the + /// [`preferredStatusBarStyle`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621416-preferredstatusbarstyle?language=objc) + /// property. + /// + /// [`setNeedsStatusBarAppearanceUpdate()`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621354-setneedsstatusbarappearanceupdat?language=objc) + /// is also called for you. + fn set_preferred_status_bar_style(&self, status_bar_style: StatusBarStyle); + + /// Sets whether the [`Window`] should recognize pinch gestures. + /// + /// The default is to not recognize gestures. + fn recognize_pinch_gesture(&self, should_recognize: bool); + + /// Sets whether the [`Window`] should recognize pan gestures. + /// + /// The default is to not recognize gestures. + /// Installs [`UIPanGestureRecognizer`](https://developer.apple.com/documentation/uikit/uipangesturerecognizer) onto view + /// + /// Set the minimum number of touches required: [`minimumNumberOfTouches`](https://developer.apple.com/documentation/uikit/uipangesturerecognizer/1621208-minimumnumberoftouches) + /// + /// Set the maximum number of touches recognized: [`maximumNumberOfTouches`](https://developer.apple.com/documentation/uikit/uipangesturerecognizer/1621208-maximumnumberoftouches) + fn recognize_pan_gesture( + &self, + should_recognize: bool, + minimum_number_of_touches: u8, + maximum_number_of_touches: u8, + ); + + /// Sets whether the [`Window`] should recognize double tap gestures. + /// + /// The default is to not recognize gestures. + fn recognize_doubletap_gesture(&self, should_recognize: bool); + + /// Sets whether the [`Window`] should recognize rotation gestures. + /// + /// The default is to not recognize gestures. + fn recognize_rotation_gesture(&self, should_recognize: bool); +} + +impl WindowExtIOS for Window { + #[inline] + fn set_scale_factor(&self, scale_factor: f64) { + self.window + .maybe_queue_on_main(move |w| w.set_scale_factor(scale_factor)) + } + + #[inline] + fn set_valid_orientations(&self, valid_orientations: ValidOrientations) { + self.window + .maybe_queue_on_main(move |w| w.set_valid_orientations(valid_orientations)) + } + + #[inline] + fn set_prefers_home_indicator_hidden(&self, hidden: bool) { + self.window + .maybe_queue_on_main(move |w| w.set_prefers_home_indicator_hidden(hidden)) + } + + #[inline] + fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge) { + self.window.maybe_queue_on_main(move |w| { + w.set_preferred_screen_edges_deferring_system_gestures(edges) + }) + } + + #[inline] + fn set_prefers_status_bar_hidden(&self, hidden: bool) { + self.window + .maybe_queue_on_main(move |w| w.set_prefers_status_bar_hidden(hidden)) + } + + #[inline] + fn set_preferred_status_bar_style(&self, status_bar_style: StatusBarStyle) { + self.window.maybe_queue_on_main(move |w| { + w.set_preferred_status_bar_style(status_bar_style) + }) + } + + #[inline] + fn recognize_pinch_gesture(&self, should_recognize: bool) { + self.window + .maybe_queue_on_main(move |w| w.recognize_pinch_gesture(should_recognize)); + } + + #[inline] + fn recognize_pan_gesture( + &self, + should_recognize: bool, + minimum_number_of_touches: u8, + maximum_number_of_touches: u8, + ) { + self.window.maybe_queue_on_main(move |w| { + w.recognize_pan_gesture( + should_recognize, + minimum_number_of_touches, + maximum_number_of_touches, + ) + }); + } + + #[inline] + fn recognize_doubletap_gesture(&self, should_recognize: bool) { + self.window.maybe_queue_on_main(move |w| { + w.recognize_doubletap_gesture(should_recognize) + }); + } + + #[inline] + fn recognize_rotation_gesture(&self, should_recognize: bool) { + self.window + .maybe_queue_on_main(move |w| w.recognize_rotation_gesture(should_recognize)); + } +} + +/// Additional methods on [`WindowAttributes`] that are specific to iOS. +pub trait WindowAttributesExtIOS { + /// Sets the [`contentScaleFactor`] of the underlying [`UIWindow`] to `scale_factor`. + /// + /// The default value is device dependent, and it's recommended GLES or Metal applications set + /// this to [`MonitorHandle::scale_factor()`]. + /// + /// [`UIWindow`]: https://developer.apple.com/documentation/uikit/uiwindow?language=objc + /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc + fn with_scale_factor(self, scale_factor: f64) -> Self; + + /// Sets the valid orientations for the [`Window`]. + /// + /// The default value is [`ValidOrientations::LandscapeAndPortrait`]. + /// + /// This sets the initial value returned by + /// [`-[UIViewController supportedInterfaceOrientations]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621435-supportedinterfaceorientations?language=objc). + fn with_valid_orientations(self, valid_orientations: ValidOrientations) -> Self; + + /// Sets whether the [`Window`] prefers the home indicator hidden. + /// + /// The default is to prefer showing the home indicator. + /// + /// This sets the initial value returned by + /// [`-[UIViewController prefersHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887510-prefershomeindicatorautohidden?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn with_prefers_home_indicator_hidden(self, hidden: bool) -> Self; + + /// Sets the screen edges for which the system gestures will take a lower priority than the + /// application's touch handling. + /// + /// This sets the initial value returned by + /// [`-[UIViewController preferredScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887512-preferredscreenedgesdeferringsys?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn with_preferred_screen_edges_deferring_system_gestures( + self, + edges: ScreenEdge, + ) -> Self; + + /// Sets whether the [`Window`] prefers the status bar hidden. + /// + /// The default is to prefer showing the status bar. + /// + /// This sets the initial value returned by + /// [`-[UIViewController prefersStatusBarHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc). + fn with_prefers_status_bar_hidden(self, hidden: bool) -> Self; + + /// Sets the style of the [`Window`]'s status bar. + /// + /// The default is system-defined. + /// + /// This sets the initial value returned by + /// [`-[UIViewController preferredStatusBarStyle]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621416-preferredstatusbarstyle?language=objc), + fn with_preferred_status_bar_style(self, status_bar_style: StatusBarStyle) -> Self; +} + +impl WindowAttributesExtIOS for WindowAttributes { + #[inline] + fn with_scale_factor(mut self, scale_factor: f64) -> Self { + self.platform_specific.scale_factor = Some(scale_factor); + self + } + + #[inline] + fn with_valid_orientations(mut self, valid_orientations: ValidOrientations) -> Self { + self.platform_specific.valid_orientations = valid_orientations; + self + } + + #[inline] + fn with_prefers_home_indicator_hidden(mut self, hidden: bool) -> Self { + self.platform_specific.prefers_home_indicator_hidden = hidden; + self + } + + #[inline] + fn with_preferred_screen_edges_deferring_system_gestures( + mut self, + edges: ScreenEdge, + ) -> Self { + self.platform_specific + .preferred_screen_edges_deferring_system_gestures = edges; + self + } + + #[inline] + fn with_prefers_status_bar_hidden(mut self, hidden: bool) -> Self { + self.platform_specific.prefers_status_bar_hidden = hidden; + self + } + + #[inline] + fn with_preferred_status_bar_style( + mut self, + status_bar_style: StatusBarStyle, + ) -> Self { + self.platform_specific.preferred_status_bar_style = status_bar_style; + self + } +} + +/// Additional methods on [`MonitorHandle`] that are specific to iOS. +pub trait MonitorHandleExtIOS { + /// Returns a pointer to the [`UIScreen`] that is used by this monitor. + /// + /// [`UIScreen`]: https://developer.apple.com/documentation/uikit/uiscreen?language=objc + fn ui_screen(&self) -> *mut c_void; + + /// Returns the preferred [`VideoModeHandle`] for this monitor. + /// + /// This translates to a call to [`-[UIScreen preferredMode]`](https://developer.apple.com/documentation/uikit/uiscreen/1617823-preferredmode?language=objc). + fn preferred_video_mode(&self) -> VideoModeHandle; +} + +impl MonitorHandleExtIOS for MonitorHandle { + #[inline] + fn ui_screen(&self) -> *mut c_void { + // SAFETY: The marker is only used to get the pointer of the screen + let mtm = unsafe { objc2_foundation::MainThreadMarker::new_unchecked() }; + objc2::rc::Retained::as_ptr(self.inner.ui_screen(mtm)) as *mut c_void + } + + #[inline] + fn preferred_video_mode(&self) -> VideoModeHandle { + VideoModeHandle { + video_mode: self.inner.preferred_video_mode(), + } + } +} + +/// Valid orientations for a particular [`Window`]. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ValidOrientations { + /// Excludes `PortraitUpsideDown` on iphone + #[default] + LandscapeAndPortrait, + + Landscape, + + /// Excludes `PortraitUpsideDown` on iphone + Portrait, +} + +/// The device [idiom]. +/// +/// [idiom]: https://developer.apple.com/documentation/uikit/uidevice/1620037-userinterfaceidiom?language=objc +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Idiom { + Unspecified, + + /// iPhone and iPod touch. + Phone, + + /// iPad. + Pad, + + /// tvOS and Apple TV. + TV, + CarPlay, +} + +bitflags::bitflags! { + /// The [edges] of a screen. + /// + /// [edges]: https://developer.apple.com/documentation/uikit/uirectedge?language=objc + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct ScreenEdge: u8 { + const NONE = 0; + const TOP = 1 << 0; + const LEFT = 1 << 1; + const BOTTOM = 1 << 2; + const RIGHT = 1 << 3; + const ALL = ScreenEdge::TOP.bits() | ScreenEdge::LEFT.bits() + | ScreenEdge::BOTTOM.bits() | ScreenEdge::RIGHT.bits(); + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum StatusBarStyle { + #[default] + Default, + LightContent, + DarkContent, +} diff --git a/rio-window/src/platform/macos.rs b/rio-window/src/platform/macos.rs new file mode 100644 index 00000000..b4a699b4 --- /dev/null +++ b/rio-window/src/platform/macos.rs @@ -0,0 +1,453 @@ +//! # macOS / AppKit +//! +//! Winit has an OS requirement of macOS 10.11 or higher (same as Rust +//! itself), and is regularly tested on macOS 10.14. +//! +//! A lot of functionality expects the application to be ready before you +//! start doing anything; this includes creating windows, fetching monitors, +//! drawing, and so on, see issues [#2238], [#2051] and [#2087]. +//! +//! If you encounter problems, you should try doing your initialization inside +//! `Event::Resumed`. +//! +//! [#2238]: https://github.com/rust-windowing/winit/issues/2238 +//! [#2051]: https://github.com/rust-windowing/winit/issues/2051 +//! [#2087]: https://github.com/rust-windowing/winit/issues/2087 + +use std::os::raw::c_void; + +use crate::event_loop::{ActiveEventLoop, EventLoopBuilder}; +use crate::monitor::MonitorHandle; +use crate::window::{Window, WindowAttributes}; + +/// Additional methods on [`Window`] that are specific to MacOS. +pub trait WindowExtMacOS { + /// Returns whether or not the window is in simple fullscreen mode. + fn simple_fullscreen(&self) -> bool; + + /// Toggles a fullscreen mode that doesn't require a new macOS space. + /// Returns a boolean indicating whether the transition was successful (this + /// won't work if the window was already in the native fullscreen). + /// + /// This is how fullscreen used to work on macOS in versions before Lion. + /// And allows the user to have a fullscreen window without using another + /// space or taking control over the entire monitor. + fn set_simple_fullscreen(&self, fullscreen: bool) -> bool; + + /// Returns whether or not the window has shadow. + fn has_shadow(&self) -> bool; + + /// Sets whether or not the window has shadow. + fn set_has_shadow(&self, has_shadow: bool); + + /// Group windows together by using the same tabbing identifier. + /// + /// + fn set_tabbing_identifier(&self, identifier: &str); + + /// Returns the window's tabbing identifier. + fn tabbing_identifier(&self) -> String; + + /// Select next tab. + fn select_next_tab(&self); + + /// Select previous tab. + fn select_previous_tab(&self); + + /// Select the tab with the given index. + /// + /// Will no-op when the index is out of bounds. + fn select_tab_at_index(&self, index: usize); + + /// Get the number of tabs in the window tab group. + fn num_tabs(&self) -> usize; + + /// Get the window's edit state. + /// + /// # Examples + /// + /// ```ignore + /// WindowEvent::CloseRequested => { + /// if window.is_document_edited() { + /// // Show the user a save pop-up or similar + /// } else { + /// // Close the window + /// drop(window); + /// } + /// } + /// ``` + fn is_document_edited(&self) -> bool; + + /// Put the window in a state which indicates a file save is required. + fn set_document_edited(&self, edited: bool); + + /// Set option as alt behavior as described in [`OptionAsAlt`]. + /// + /// This will ignore diacritical marks and accent characters from + /// being processed as received characters. Instead, the input + /// device's raw character will be placed in event queues with the + /// Alt modifier set. + fn set_option_as_alt(&self, option_as_alt: OptionAsAlt); + + /// Getter for the [`WindowExtMacOS::set_option_as_alt`]. + fn option_as_alt(&self) -> OptionAsAlt; +} + +impl WindowExtMacOS for Window { + #[inline] + fn simple_fullscreen(&self) -> bool { + self.window.maybe_wait_on_main(|w| w.simple_fullscreen()) + } + + #[inline] + fn set_simple_fullscreen(&self, fullscreen: bool) -> bool { + self.window + .maybe_wait_on_main(move |w| w.set_simple_fullscreen(fullscreen)) + } + + #[inline] + fn has_shadow(&self) -> bool { + self.window.maybe_wait_on_main(|w| w.has_shadow()) + } + + #[inline] + fn set_has_shadow(&self, has_shadow: bool) { + self.window + .maybe_queue_on_main(move |w| w.set_has_shadow(has_shadow)) + } + + #[inline] + fn set_tabbing_identifier(&self, identifier: &str) { + self.window + .maybe_wait_on_main(|w| w.set_tabbing_identifier(identifier)) + } + + #[inline] + fn tabbing_identifier(&self) -> String { + self.window.maybe_wait_on_main(|w| w.tabbing_identifier()) + } + + #[inline] + fn select_next_tab(&self) { + self.window.maybe_queue_on_main(|w| w.select_next_tab()) + } + + #[inline] + fn select_previous_tab(&self) { + self.window.maybe_queue_on_main(|w| w.select_previous_tab()) + } + + #[inline] + fn select_tab_at_index(&self, index: usize) { + self.window + .maybe_queue_on_main(move |w| w.select_tab_at_index(index)) + } + + #[inline] + fn num_tabs(&self) -> usize { + self.window.maybe_wait_on_main(|w| w.num_tabs()) + } + + #[inline] + fn is_document_edited(&self) -> bool { + self.window.maybe_wait_on_main(|w| w.is_document_edited()) + } + + #[inline] + fn set_document_edited(&self, edited: bool) { + self.window + .maybe_queue_on_main(move |w| w.set_document_edited(edited)) + } + + #[inline] + fn set_option_as_alt(&self, option_as_alt: OptionAsAlt) { + self.window + .maybe_queue_on_main(move |w| w.set_option_as_alt(option_as_alt)) + } + + #[inline] + fn option_as_alt(&self) -> OptionAsAlt { + self.window.maybe_wait_on_main(|w| w.option_as_alt()) + } +} + +/// Corresponds to `NSApplicationActivationPolicy`. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ActivationPolicy { + /// Corresponds to `NSApplicationActivationPolicyRegular`. + #[default] + Regular, + + /// Corresponds to `NSApplicationActivationPolicyAccessory`. + Accessory, + + /// Corresponds to `NSApplicationActivationPolicyProhibited`. + Prohibited, +} + +/// Additional methods on [`WindowAttributes`] that are specific to MacOS. +/// +/// **Note:** Properties dealing with the titlebar will be overwritten by the +/// [`WindowAttributes::with_decorations`] method: +/// - `with_titlebar_transparent` +/// - `with_title_hidden` +/// - `with_titlebar_hidden` +/// - `with_titlebar_buttons_hidden` +/// - `with_fullsize_content_view` +pub trait WindowAttributesExtMacOS { + /// Enables click-and-drag behavior for the entire window, not just the titlebar. + fn with_movable_by_window_background( + self, + movable_by_window_background: bool, + ) -> Self; + /// Makes the titlebar transparent and allows the content to appear behind it. + fn with_titlebar_transparent(self, titlebar_transparent: bool) -> Self; + /// Hides the window title. + fn with_title_hidden(self, title_hidden: bool) -> Self; + /// Hides the window titlebar. + fn with_titlebar_hidden(self, titlebar_hidden: bool) -> Self; + /// Hides the window titlebar buttons. + fn with_titlebar_buttons_hidden(self, titlebar_buttons_hidden: bool) -> Self; + /// Makes the window content appear behind the titlebar. + fn with_fullsize_content_view(self, fullsize_content_view: bool) -> Self; + fn with_disallow_hidpi(self, disallow_hidpi: bool) -> Self; + fn with_has_shadow(self, has_shadow: bool) -> Self; + /// Window accepts click-through mouse events. + fn with_accepts_first_mouse(self, accepts_first_mouse: bool) -> Self; + /// Defines the window tabbing identifier. + /// + /// + fn with_tabbing_identifier(self, identifier: &str) -> Self; + /// Set how the Option keys are interpreted. + /// + /// See [`WindowExtMacOS::set_option_as_alt`] for details on what this means if set. + fn with_option_as_alt(self, option_as_alt: OptionAsAlt) -> Self; +} + +impl WindowAttributesExtMacOS for WindowAttributes { + #[inline] + fn with_movable_by_window_background( + mut self, + movable_by_window_background: bool, + ) -> Self { + self.platform_specific.movable_by_window_background = + movable_by_window_background; + self + } + + #[inline] + fn with_titlebar_transparent(mut self, titlebar_transparent: bool) -> Self { + self.platform_specific.titlebar_transparent = titlebar_transparent; + self + } + + #[inline] + fn with_titlebar_hidden(mut self, titlebar_hidden: bool) -> Self { + self.platform_specific.titlebar_hidden = titlebar_hidden; + self + } + + #[inline] + fn with_titlebar_buttons_hidden(mut self, titlebar_buttons_hidden: bool) -> Self { + self.platform_specific.titlebar_buttons_hidden = titlebar_buttons_hidden; + self + } + + #[inline] + fn with_title_hidden(mut self, title_hidden: bool) -> Self { + self.platform_specific.title_hidden = title_hidden; + self + } + + #[inline] + fn with_fullsize_content_view(mut self, fullsize_content_view: bool) -> Self { + self.platform_specific.fullsize_content_view = fullsize_content_view; + self + } + + #[inline] + fn with_disallow_hidpi(mut self, disallow_hidpi: bool) -> Self { + self.platform_specific.disallow_hidpi = disallow_hidpi; + self + } + + #[inline] + fn with_has_shadow(mut self, has_shadow: bool) -> Self { + self.platform_specific.has_shadow = has_shadow; + self + } + + #[inline] + fn with_accepts_first_mouse(mut self, accepts_first_mouse: bool) -> Self { + self.platform_specific.accepts_first_mouse = accepts_first_mouse; + self + } + + #[inline] + fn with_tabbing_identifier(mut self, tabbing_identifier: &str) -> Self { + self.platform_specific + .tabbing_identifier + .replace(tabbing_identifier.to_string()); + self + } + + #[inline] + fn with_option_as_alt(mut self, option_as_alt: OptionAsAlt) -> Self { + self.platform_specific.option_as_alt = option_as_alt; + self + } +} + +pub trait EventLoopBuilderExtMacOS { + /// Sets the activation policy for the application. + /// + /// It is set to [`ActivationPolicy::Regular`] by default. + /// + /// # Example + /// + /// Set the activation policy to "accessory". + /// + /// ``` + /// use rio_window::event_loop::EventLoopBuilder; + /// #[cfg(target_os = "macos")] + /// use rio_window::platform::macos::{ActivationPolicy, EventLoopBuilderExtMacOS}; + /// + /// let mut builder = EventLoopBuilder::new(); + /// #[cfg(target_os = "macos")] + /// builder.with_activation_policy(ActivationPolicy::Accessory); + /// # if false { // We can't test this part + /// let event_loop = builder.build(); + /// # } + /// ``` + fn with_activation_policy( + &mut self, + activation_policy: ActivationPolicy, + ) -> &mut Self; + + /// Used to control whether a default menubar menu is created. + /// + /// Menu creation is enabled by default. + /// + /// # Example + /// + /// Disable creating a default menubar. + /// + /// ``` + /// use rio_window::event_loop::EventLoopBuilder; + /// #[cfg(target_os = "macos")] + /// use rio_window::platform::macos::EventLoopBuilderExtMacOS; + /// + /// let mut builder = EventLoopBuilder::new(); + /// #[cfg(target_os = "macos")] + /// builder.with_default_menu(false); + /// # if false { // We can't test this part + /// let event_loop = builder.build(); + /// # } + /// ``` + fn with_default_menu(&mut self, enable: bool) -> &mut Self; + + /// Used to prevent the application from automatically activating when launched if + /// another application is already active. + /// + /// The default behavior is to ignore other applications and activate when launched. + fn with_activate_ignoring_other_apps(&mut self, ignore: bool) -> &mut Self; +} + +impl EventLoopBuilderExtMacOS for EventLoopBuilder { + #[inline] + fn with_activation_policy( + &mut self, + activation_policy: ActivationPolicy, + ) -> &mut Self { + self.platform_specific.activation_policy = activation_policy; + self + } + + #[inline] + fn with_default_menu(&mut self, enable: bool) -> &mut Self { + self.platform_specific.default_menu = enable; + self + } + + #[inline] + fn with_activate_ignoring_other_apps(&mut self, ignore: bool) -> &mut Self { + self.platform_specific.activate_ignoring_other_apps = ignore; + self + } +} + +/// Additional methods on [`MonitorHandle`] that are specific to MacOS. +pub trait MonitorHandleExtMacOS { + /// Returns the identifier of the monitor for Cocoa. + fn native_id(&self) -> u32; + /// Returns a pointer to the NSScreen representing this monitor. + fn ns_screen(&self) -> Option<*mut c_void>; +} + +impl MonitorHandleExtMacOS for MonitorHandle { + #[inline] + fn native_id(&self) -> u32 { + self.inner.native_identifier() + } + + fn ns_screen(&self) -> Option<*mut c_void> { + // SAFETY: We only use the marker to get a pointer + let mtm = unsafe { objc2_foundation::MainThreadMarker::new_unchecked() }; + self.inner + .ns_screen(mtm) + .map(|s| objc2::rc::Retained::as_ptr(&s) as _) + } +} + +/// Additional methods on [`ActiveEventLoop`] that are specific to macOS. +pub trait ActiveEventLoopExtMacOS { + /// Hide the entire application. In most applications this is typically triggered with + /// Command-H. + fn hide_application(&self); + /// Hide the other applications. In most applications this is typically triggered with + /// Command+Option-H. + fn hide_other_applications(&self); + /// Set whether the system can automatically organize windows into tabs. + /// + /// + fn set_allows_automatic_window_tabbing(&self, enabled: bool); + /// Returns whether the system can automatically organize windows into tabs. + fn allows_automatic_window_tabbing(&self) -> bool; +} + +impl ActiveEventLoopExtMacOS for ActiveEventLoop { + fn hide_application(&self) { + self.p.hide_application() + } + + fn hide_other_applications(&self) { + self.p.hide_other_applications() + } + + fn set_allows_automatic_window_tabbing(&self, enabled: bool) { + self.p.set_allows_automatic_window_tabbing(enabled); + } + + fn allows_automatic_window_tabbing(&self) -> bool { + self.p.allows_automatic_window_tabbing() + } +} + +/// Option as alt behavior. +/// +/// The default is `None`. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum OptionAsAlt { + /// The left `Option` key is treated as `Alt`. + OnlyLeft, + + /// The right `Option` key is treated as `Alt`. + OnlyRight, + + /// Both `Option` keys are treated as `Alt`. + Both, + + /// No special handling is applied for `Option` key. + #[default] + None, +} diff --git a/rio-window/src/platform/mod.rs b/rio-window/src/platform/mod.rs new file mode 100644 index 00000000..c31eafcc --- /dev/null +++ b/rio-window/src/platform/mod.rs @@ -0,0 +1,57 @@ +//! Contains traits with platform-specific methods in them. +//! +//! Only the modules corresponding to the platform you're compiling to will be available. + +#[cfg(any(ios_platform, docsrs))] +pub mod ios; +#[cfg(any(macos_platform, docsrs))] +pub mod macos; +#[cfg(any(orbital_platform, docsrs))] +pub mod orbital; +#[cfg(any(x11_platform, wayland_platform, docsrs))] +pub mod startup_notify; +#[cfg(any(wayland_platform, docsrs))] +pub mod wayland; +#[cfg(any(web_platform, docsrs))] +pub mod web; +#[cfg(any(windows_platform, docsrs))] +pub mod windows; +#[cfg(any(x11_platform, docsrs))] +pub mod x11; + +#[cfg(any( + windows_platform, + macos_platform, + x11_platform, + wayland_platform, + docsrs, +))] +pub mod run_on_demand; + +#[cfg(any( + windows_platform, + macos_platform, + x11_platform, + wayland_platform, + docsrs, +))] +pub mod pump_events; + +#[cfg(any( + windows_platform, + macos_platform, + x11_platform, + wayland_platform, + orbital_platform, + docsrs +))] +pub mod modifier_supplement; + +#[cfg(any( + windows_platform, + macos_platform, + x11_platform, + wayland_platform, + docsrs +))] +pub mod scancode; diff --git a/rio-window/src/platform/modifier_supplement.rs b/rio-window/src/platform/modifier_supplement.rs new file mode 100644 index 00000000..651ba7f3 --- /dev/null +++ b/rio-window/src/platform/modifier_supplement.rs @@ -0,0 +1,38 @@ +use crate::event::KeyEvent; +use crate::keyboard::Key; + +/// Additional methods for the `KeyEvent` which cannot be implemented on all +/// platforms. +pub trait KeyEventExtModifierSupplement { + /// Identical to `KeyEvent::text` but this is affected by Ctrl. + /// + /// For example, pressing Ctrl+a produces `Some("\x01")`. + fn text_with_all_modifiers(&self) -> Option<&str>; + + /// This value ignores all modifiers including, + /// but not limited to Shift, Caps Lock, + /// and Ctrl. In most cases this means that the + /// unicode character in the resulting string is lowercase. + /// + /// This is useful for key-bindings / shortcut key combinations. + /// + /// In case `logical_key` reports `Dead`, this will still report the + /// key as `Character` according to the current keyboard layout. This value + /// cannot be `Dead`. + fn key_without_modifiers(&self) -> Key; +} + +impl KeyEventExtModifierSupplement for KeyEvent { + #[inline] + fn text_with_all_modifiers(&self) -> Option<&str> { + self.platform_specific + .text_with_all_modifiers + .as_ref() + .map(|s| s.as_str()) + } + + #[inline] + fn key_without_modifiers(&self) -> Key { + self.platform_specific.key_without_modifiers.clone() + } +} diff --git a/rio-window/src/platform/orbital.rs b/rio-window/src/platform/orbital.rs new file mode 100644 index 00000000..d8093503 --- /dev/null +++ b/rio-window/src/platform/orbital.rs @@ -0,0 +1,6 @@ +//! # Orbital / Redox OS +//! +//! Redox OS has some functionality not yet present that will be implemented +//! when its orbital display server provides it. + +// There are no Orbital specific traits yet. diff --git a/rio-window/src/platform/pump_events.rs b/rio-window/src/platform/pump_events.rs new file mode 100644 index 00000000..cb759616 --- /dev/null +++ b/rio-window/src/platform/pump_events.rs @@ -0,0 +1,151 @@ +use std::time::Duration; + +use crate::application::ApplicationHandler; +use crate::event::Event; +use crate::event_loop::{self, ActiveEventLoop, EventLoop}; + +/// Additional methods on [`EventLoop`] for pumping events within an external event loop +pub trait EventLoopExtPumpEvents { + /// A type provided by the user that can be passed through [`Event::UserEvent`]. + type UserEvent: 'static; + + /// Pump the `EventLoop` to check for and dispatch pending events. + /// + /// This API is designed to enable applications to integrate Winit into an + /// external event loop, for platforms that can support this. + /// + /// The given `timeout` limits how long it may block waiting for new events. + /// + /// Passing a `timeout` of `Some(Duration::ZERO)` would ensure your external + /// event loop is never blocked but you would likely need to consider how + /// to throttle your own external loop. + /// + /// Passing a `timeout` of `None` means that it may wait indefinitely for new + /// events before returning control back to the external loop. + /// + /// **Note:** This is not a portable API, and its usage involves a number of + /// caveats and trade offs that should be considered before using this API! + /// + /// You almost certainly shouldn't use this API, unless you absolutely know it's + /// the only practical option you have. + /// + /// ## Synchronous events + /// + /// Some events _must_ only be handled synchronously via the closure that + /// is passed to Winit so that the handler will also be synchronized with + /// the window system and operating system. + /// + /// This is because some events are driven by a window system callback + /// where the window systems expects the application to have handled the + /// event before returning. + /// + /// **These events can not be buffered and handled outside of the closure + /// passed to Winit.** + /// + /// As a general rule it is not recommended to ever buffer events to handle + /// them outside of the closure passed to Winit since it's difficult to + /// provide guarantees about which events are safe to buffer across all + /// operating systems. + /// + /// Notable events that will certainly create portability problems if + /// buffered and handled outside of Winit include: + /// - `RedrawRequested` events, used to schedule rendering. + /// + /// macOS for example uses a `drawRect` callback to drive rendering + /// within applications and expects rendering to be finished before + /// the `drawRect` callback returns. + /// + /// For portability it's strongly recommended that applications should + /// keep their rendering inside the closure provided to Winit. + /// - Any lifecycle events, such as `Suspended` / `Resumed`. + /// + /// The handling of these events needs to be synchronized with the + /// operating system and it would never be appropriate to buffer a + /// notification that your application has been suspended or resumed and + /// then handled that later since there would always be a chance that + /// other lifecycle events occur while the event is buffered. + /// + /// ## Supported Platforms + /// + /// - Windows + /// - Linux + /// - MacOS + /// - Android + /// + /// ## Unsupported Platforms + /// + /// - **Web:** This API is fundamentally incompatible with the event-based way in which + /// Web browsers work because it's not possible to have a long-running external + /// loop that would block the browser and there is nothing that can be + /// polled to ask for new new events. Events are delivered via callbacks based + /// on an event loop that is internal to the browser itself. + /// - **iOS:** It's not possible to stop and start an `NSApplication` repeatedly on iOS so + /// there's no way to support the same approach to polling as on MacOS. + /// + /// ## Platform-specific + /// + /// - **Windows**: The implementation will use `PeekMessage` when checking for window messages + /// to avoid blocking your external event loop. + /// + /// - **MacOS**: The implementation works in terms of stopping the global application whenever + /// the application `RunLoop` indicates that it is preparing to block and wait for new events. + /// + /// This is very different to the polling APIs that are available on other + /// platforms (the lower level polling primitives on MacOS are private + /// implementation details for `NSApplication` which aren't accessible to + /// application developers) + /// + /// It's likely this will be less efficient than polling on other OSs and + /// it also means the `NSApplication` is stopped while outside of the Winit + /// event loop - and that's observable (for example to crates like `rfd`) + /// because the `NSApplication` is global state. + /// + /// If you render outside of Winit you are likely to see window resizing artifacts + /// since MacOS expects applications to render synchronously during any `drawRect` + /// callback. + fn pump_app_events>( + &mut self, + timeout: Option, + app: &mut A, + ) -> PumpStatus { + #[allow(deprecated)] + self.pump_events(timeout, |event, event_loop| { + event_loop::dispatch_event_for_app(app, event_loop, event) + }) + } + + /// See [`pump_app_events`]. + /// + /// [`pump_app_events`]: Self::pump_app_events + #[deprecated = "use EventLoopExtPumpEvents::pump_app_events"] + fn pump_events( + &mut self, + timeout: Option, + event_handler: F, + ) -> PumpStatus + where + F: FnMut(Event, &ActiveEventLoop); +} + +impl EventLoopExtPumpEvents for EventLoop { + type UserEvent = T; + + fn pump_events( + &mut self, + timeout: Option, + event_handler: F, + ) -> PumpStatus + where + F: FnMut(Event, &ActiveEventLoop), + { + self.event_loop.pump_events(timeout, event_handler) + } +} + +/// The return status for `pump_events` +pub enum PumpStatus { + /// Continue running external loop. + Continue, + /// Exit external loop. + Exit(i32), +} diff --git a/rio-window/src/platform/run_on_demand.rs b/rio-window/src/platform/run_on_demand.rs new file mode 100644 index 00000000..ecb22395 --- /dev/null +++ b/rio-window/src/platform/run_on_demand.rs @@ -0,0 +1,109 @@ +use crate::application::ApplicationHandler; +use crate::error::EventLoopError; +use crate::event::Event; +use crate::event_loop::{self, ActiveEventLoop, EventLoop}; + +#[cfg(doc)] +use crate::{platform::pump_events::EventLoopExtPumpEvents, window::Window}; + +/// Additional methods on [`EventLoop`] to return control flow to the caller. +pub trait EventLoopExtRunOnDemand { + /// A type provided by the user that can be passed through [`Event::UserEvent`]. + type UserEvent: 'static; + + /// See [`run_app_on_demand`]. + /// + /// [`run_app_on_demand`]: Self::run_app_on_demand + #[deprecated = "use EventLoopExtRunOnDemand::run_app_on_demand"] + fn run_on_demand(&mut self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &ActiveEventLoop); + + /// Run the application with the event loop on the calling thread. + /// + /// Unlike [`EventLoop::run_app`], this function accepts non-`'static` (i.e. non-`move`) + /// closures and it is possible to return control back to the caller without + /// consuming the `EventLoop` (by using [`exit()`]) and + /// so the event loop can be re-run after it has exit. + /// + /// It's expected that each run of the loop will be for orthogonal instantiations of your + /// Winit application, but internally each instantiation may re-use some common window + /// system resources, such as a display server connection. + /// + /// This API is not designed to run an event loop in bursts that you can exit from and return + /// to while maintaining the full state of your application. (If you need something like this + /// you can look at the [`EventLoopExtPumpEvents::pump_app_events()`] API) + /// + /// Each time `run_app_on_demand` is called the startup sequence of `init`, followed by + /// `resume` is being preserved. + /// + /// See the [`set_control_flow()`] docs on how to change the event loop's behavior. + /// + /// # Caveats + /// - This extension isn't available on all platforms, since it's not always possible to return + /// to the caller (specifically this is impossible on iOS and Web - though with the Web + /// backend it is possible to use `EventLoopExtWebSys::spawn()`[^1] more than once instead). + /// - No [`Window`] state can be carried between separate runs of the event loop. + /// + /// You are strongly encouraged to use [`EventLoop::run_app()`] for portability, unless you + /// specifically need the ability to re-run a single event loop more than once + /// + /// # Supported Platforms + /// - Windows + /// - Linux + /// - macOS + /// - Android + /// + /// # Unsupported Platforms + /// - **Web:** This API is fundamentally incompatible with the event-based way in which Web + /// browsers work because it's not possible to have a long-running external loop that would + /// block the browser and there is nothing that can be polled to ask for new events. Events + /// are delivered via callbacks based on an event loop that is internal to the browser itself. + /// - **iOS:** It's not possible to stop and start an `UIApplication` repeatedly on iOS. + #[cfg_attr(not(web_platform), doc = "[^1]: `spawn()` is only available on `wasm` platforms.")] + #[rustfmt::skip] + /// + /// [`exit()`]: ActiveEventLoop::exit() + /// [`set_control_flow()`]: ActiveEventLoop::set_control_flow() + fn run_app_on_demand>( + &mut self, + app: &mut A, + ) -> Result<(), EventLoopError> { + #[allow(deprecated)] + self.run_on_demand(|event, event_loop| { + event_loop::dispatch_event_for_app(app, event_loop, event) + }) + } +} + +impl EventLoopExtRunOnDemand for EventLoop { + type UserEvent = T; + + fn run_on_demand(&mut self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &ActiveEventLoop), + { + self.event_loop.window_target().clear_exit(); + self.event_loop.run_on_demand(event_handler) + } +} + +impl ActiveEventLoop { + /// Clear exit status. + pub(crate) fn clear_exit(&self) { + self.p.clear_exit() + } +} + +/// ```compile_fail +/// use winit::event_loop::EventLoop; +/// use winit::platform::run_on_demand::EventLoopExtRunOnDemand; +/// +/// let mut event_loop = EventLoop::new().unwrap(); +/// event_loop.run_on_demand(|_, _| { +/// // Attempt to run the event loop re-entrantly; this must fail. +/// event_loop.run_on_demand(|_, _| {}); +/// }); +/// ``` +#[allow(dead_code)] +fn test_run_on_demand_cannot_access_event_loop() {} diff --git a/rio-window/src/platform/scancode.rs b/rio-window/src/platform/scancode.rs new file mode 100644 index 00000000..0d783135 --- /dev/null +++ b/rio-window/src/platform/scancode.rs @@ -0,0 +1,50 @@ +use crate::keyboard::{KeyCode, PhysicalKey}; + +// TODO: Describe what this value contains for each platform + +/// Additional methods for the [`PhysicalKey`] type that allow the user to access the +/// platform-specific scancode. +/// +/// [`PhysicalKey`]: crate::keyboard::PhysicalKey +pub trait PhysicalKeyExtScancode { + /// The raw value of the platform-specific physical key identifier. + /// + /// Returns `Some(key_id)` if the conversion was successful; returns `None` otherwise. + /// + /// ## Platform-specific + /// - **Windows:** A 16bit extended scancode + /// - **Wayland/X11**: A 32-bit linux scancode, which is X11/Wayland keycode subtracted by 8. + fn to_scancode(self) -> Option; + + /// Constructs a `PhysicalKey` from a platform-specific physical key identifier. + /// + /// Note that this conversion may be lossy, i.e. converting the returned `PhysicalKey` back + /// using `to_scancode` might not yield the original value. + /// + /// ## Platform-specific + /// - **Wayland/X11**: A 32-bit linux scancode. When building from X11/Wayland keycode subtract + /// `8` to get the value you wanted. + fn from_scancode(scancode: u32) -> PhysicalKey; +} + +impl PhysicalKeyExtScancode for PhysicalKey { + fn to_scancode(self) -> Option { + crate::platform_impl::physicalkey_to_scancode(self) + } + + fn from_scancode(scancode: u32) -> PhysicalKey { + crate::platform_impl::scancode_to_physicalkey(scancode) + } +} + +impl PhysicalKeyExtScancode for KeyCode { + #[inline] + fn to_scancode(self) -> Option { + ::to_scancode(PhysicalKey::Code(self)) + } + + #[inline] + fn from_scancode(scancode: u32) -> PhysicalKey { + ::from_scancode(scancode) + } +} diff --git a/rio-window/src/platform/startup_notify.rs b/rio-window/src/platform/startup_notify.rs new file mode 100644 index 00000000..c9047a03 --- /dev/null +++ b/rio-window/src/platform/startup_notify.rs @@ -0,0 +1,99 @@ +//! Window startup notification to handle window raising. +//! +//! The [`ActivationToken`] is essential to ensure that your newly +//! created window will obtain the focus, otherwise the user could +//! be requered to click on the window. +//! +//! Such token is usually delivered via the environment variable and +//! could be read from it with the [`EventLoopExtStartupNotify::read_token_from_env`]. +//! +//! Such token must also be reset after reading it from your environment with +//! [`reset_activation_token_env`] otherwise child processes could inherit it. +//! +//! When starting a new child process with a newly obtained [`ActivationToken`] from +//! [`WindowExtStartupNotify::request_activation_token`] the [`set_activation_token_env`] +//! must be used to propagate it to the child +//! +//! To ensure the delivery of such token by other processes to you, the user should +//! set `StartupNotify=true` inside the `.desktop` file of their application. +//! +//! The specification could be found [`here`]. +//! +//! [`here`]: https://specifications.freedesktop.org/startup-notification-spec/startup-notification-latest.txt + +use std::env; + +use crate::error::NotSupportedError; +use crate::event_loop::{ActiveEventLoop, AsyncRequestSerial}; +use crate::window::{ActivationToken, Window, WindowAttributes}; + +/// The variable which is used mostly on X11. +const X11_VAR: &str = "DESKTOP_STARTUP_ID"; + +/// The variable which is used mostly on Wayland. +const WAYLAND_VAR: &str = "XDG_ACTIVATION_TOKEN"; + +pub trait EventLoopExtStartupNotify { + /// Read the token from the environment. + /// + /// It's recommended **to unset** this environment variable for child processes. + fn read_token_from_env(&self) -> Option; +} + +pub trait WindowExtStartupNotify { + /// Request a new activation token. + /// + /// The token will be delivered inside + fn request_activation_token(&self) -> Result; +} + +pub trait WindowAttributesExtStartupNotify { + /// Use this [`ActivationToken`] during window creation. + /// + /// Not using such a token upon a window could make your window not gaining + /// focus until the user clicks on the window. + fn with_activation_token(self, token: ActivationToken) -> Self; +} + +impl EventLoopExtStartupNotify for ActiveEventLoop { + fn read_token_from_env(&self) -> Option { + match self.p { + #[cfg(wayland_platform)] + crate::platform_impl::ActiveEventLoop::Wayland(_) => env::var(WAYLAND_VAR), + #[cfg(x11_platform)] + crate::platform_impl::ActiveEventLoop::X(_) => env::var(X11_VAR), + } + .ok() + .map(ActivationToken::_new) + } +} + +impl WindowExtStartupNotify for Window { + fn request_activation_token(&self) -> Result { + self.window.request_activation_token() + } +} + +impl WindowAttributesExtStartupNotify for WindowAttributes { + fn with_activation_token(mut self, token: ActivationToken) -> Self { + self.platform_specific.activation_token = Some(token); + self + } +} + +/// Remove the activation environment variables from the current process. +/// +/// This is wise to do before running child processes, +/// which may not to support the activation token. +pub fn reset_activation_token_env() { + env::remove_var(X11_VAR); + env::remove_var(WAYLAND_VAR); +} + +/// Set environment variables responsible for activation token. +/// +/// This could be used before running daemon processes. +pub fn set_activation_token_env(token: ActivationToken) { + env::set_var(X11_VAR, &token._token); + env::set_var(WAYLAND_VAR, token._token); +} diff --git a/rio-window/src/platform/wayland.rs b/rio-window/src/platform/wayland.rs new file mode 100644 index 00000000..aa75bb26 --- /dev/null +++ b/rio-window/src/platform/wayland.rs @@ -0,0 +1,105 @@ +//! # Wayland +//! +//! **Note:** Windows don't appear on Wayland until you draw/present to them. +//! +//! By default, Winit loads system libraries using `dlopen`. This can be +//! disabled by disabling the `"wayland-dlopen"` cargo feature. +//! +//! ## Client-side decorations +//! +//! Winit provides client-side decorations by default, but the behaviour can +//! be controlled with the following feature flags: +//! +//! * `wayland-csd-adwaita` (default). +//! * `wayland-csd-adwaita-crossfont`. +//! * `wayland-csd-adwaita-notitle`. +use crate::event_loop::{ActiveEventLoop, EventLoopBuilder}; +use crate::monitor::MonitorHandle; +use crate::window::{Window, WindowAttributes}; + +pub use crate::window::Theme; + +/// Additional methods on [`ActiveEventLoop`] that are specific to Wayland. +pub trait ActiveEventLoopExtWayland { + /// True if the [`ActiveEventLoop`] uses Wayland. + fn is_wayland(&self) -> bool; +} + +impl ActiveEventLoopExtWayland for ActiveEventLoop { + #[inline] + fn is_wayland(&self) -> bool { + self.p.is_wayland() + } +} + +/// Additional methods on [`EventLoopBuilder`] that are specific to Wayland. +pub trait EventLoopBuilderExtWayland { + /// Force using Wayland. + fn with_wayland(&mut self) -> &mut Self; + + /// Whether to allow the event loop to be created off of the main thread. + /// + /// By default, the window is only allowed to be created on the main + /// thread, to make platform compatibility easier. + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self; +} + +impl EventLoopBuilderExtWayland for EventLoopBuilder { + #[inline] + fn with_wayland(&mut self) -> &mut Self { + self.platform_specific.forced_backend = + Some(crate::platform_impl::Backend::Wayland); + self + } + + #[inline] + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self { + self.platform_specific.any_thread = any_thread; + self + } +} + +/// Additional methods on [`Window`] that are specific to Wayland. +pub trait WindowExtWayland {} + +impl WindowExtWayland for Window {} + +/// Additional methods on [`WindowAttributes`] that are specific to Wayland. +pub trait WindowAttributesExtWayland { + /// Build window with the given name. + /// + /// The `general` name sets an application ID, which should match the `.desktop` + /// file distributed with your program. The `instance` is a `no-op`. + /// + /// For details about application ID conventions, see the + /// [Desktop Entry Spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id) + fn with_name(self, general: impl Into, instance: impl Into) -> Self; +} + +impl WindowAttributesExtWayland for WindowAttributes { + #[inline] + fn with_name( + mut self, + general: impl Into, + instance: impl Into, + ) -> Self { + self.platform_specific.name = Some(crate::platform_impl::ApplicationName::new( + general.into(), + instance.into(), + )); + self + } +} + +/// Additional methods on `MonitorHandle` that are specific to Wayland. +pub trait MonitorHandleExtWayland { + /// Returns the inner identifier of the monitor. + fn native_id(&self) -> u32; +} + +impl MonitorHandleExtWayland for MonitorHandle { + #[inline] + fn native_id(&self) -> u32 { + self.inner.native_identifier() + } +} diff --git a/rio-window/src/platform/web.rs b/rio-window/src/platform/web.rs new file mode 100644 index 00000000..2b913b49 --- /dev/null +++ b/rio-window/src/platform/web.rs @@ -0,0 +1,390 @@ +//! # Web +//! +//! The officially supported browsers are Chrome, Firefox and Safari 13.1+, +//! though forks of these should work fine. +//! +//! Winit supports compiling to the `wasm32-unknown-unknown` target with +//! `web-sys`. +//! +//! On the web platform, a Winit window is backed by a `` element. You +//! can either [provide Winit with a `` element][with_canvas], or +//! [let Winit create a `` element which you can then retrieve][get] +//! and insert it into the DOM yourself. +//! +//! Currently, there is no example code using Winit on Web, see [#3473]. For +//! information on using Rust on WebAssembly, check out the [Rust and +//! WebAssembly book]. +//! +//! [with_canvas]: WindowAttributesExtWebSys::with_canvas +//! [get]: WindowExtWebSys::canvas +//! [#3473]: https://github.com/rust-windowing/winit/issues/3473 +//! [Rust and WebAssembly book]: https://rustwasm.github.io/book/ +//! +//! ## CSS properties +//! +//! It is recommended **not** to apply certain CSS properties to the canvas: +//! - [`transform`](https://developer.mozilla.org/en-US/docs/Web/CSS/transform) +//! - [`border`](https://developer.mozilla.org/en-US/docs/Web/CSS/border) +//! - [`padding`](https://developer.mozilla.org/en-US/docs/Web/CSS/padding) +//! +//! The following APIs can't take them into account and will therefore provide inaccurate results: +//! - [`WindowEvent::Resized`] and [`Window::(set_)inner_size()`] +//! - [`WindowEvent::Occluded`] +//! - [`WindowEvent::CursorMoved`], [`WindowEvent::CursorEntered`], [`WindowEvent::CursorLeft`], and +//! [`WindowEvent::Touch`]. +//! - [`Window::set_outer_position()`] +//! +//! [`WindowEvent::Resized`]: crate::event::WindowEvent::Resized +//! [`Window::(set_)inner_size()`]: crate::window::Window::inner_size +//! [`WindowEvent::Occluded`]: crate::event::WindowEvent::Occluded +//! [`WindowEvent::CursorMoved`]: crate::event::WindowEvent::CursorMoved +//! [`WindowEvent::CursorEntered`]: crate::event::WindowEvent::CursorEntered +//! [`WindowEvent::CursorLeft`]: crate::event::WindowEvent::CursorLeft +//! [`WindowEvent::Touch`]: crate::event::WindowEvent::Touch +//! [`Window::set_outer_position()`]: crate::window::Window::set_outer_position + +use std::error::Error; +use std::fmt::{self, Display, Formatter}; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; + +#[cfg(web_platform)] +use web_sys::HtmlCanvasElement; + +use crate::application::ApplicationHandler; +use crate::cursor::CustomCursorSource; +use crate::event::Event; +use crate::event_loop::{self, ActiveEventLoop, EventLoop}; +#[cfg(web_platform)] +use crate::platform_impl::CustomCursorFuture as PlatformCustomCursorFuture; +use crate::platform_impl::PlatformCustomCursorSource; +use crate::window::{CustomCursor, Window, WindowAttributes}; + +#[cfg(not(web_platform))] +#[doc(hidden)] +pub struct HtmlCanvasElement; + +pub trait WindowExtWebSys { + /// Only returns the canvas if called from inside the window context (the + /// main thread). + fn canvas(&self) -> Option; + + /// Returns [`true`] if calling `event.preventDefault()` is enabled. + /// + /// See [`Window::set_prevent_default()`] for more details. + fn prevent_default(&self) -> bool; + + /// Sets whether `event.preventDefault()` should be called on events on the + /// canvas that have side effects. + /// + /// For example, by default using the mouse wheel would cause the page to scroll, enabling this + /// would prevent that. + /// + /// Some events are impossible to prevent. E.g. Firefox allows to access the native browser + /// context menu with Shift+Rightclick. + fn set_prevent_default(&self, prevent_default: bool); +} + +impl WindowExtWebSys for Window { + #[inline] + fn canvas(&self) -> Option { + self.window.canvas() + } + + fn prevent_default(&self) -> bool { + self.window.prevent_default() + } + + fn set_prevent_default(&self, prevent_default: bool) { + self.window.set_prevent_default(prevent_default) + } +} + +pub trait WindowAttributesExtWebSys { + /// Pass an [`HtmlCanvasElement`] to be used for this [`Window`]. If [`None`], + /// [`WindowAttributes::default()`] will create one. + /// + /// In any case, the canvas won't be automatically inserted into the web page. + /// + /// [`None`] by default. + #[cfg_attr( + not(web_platform), + doc = "", + doc = "[`HtmlCanvasElement`]: #only-available-on-wasm" + )] + fn with_canvas(self, canvas: Option) -> Self; + + /// Sets whether `event.preventDefault()` should be called on events on the + /// canvas that have side effects. + /// + /// See [`Window::set_prevent_default()`] for more details. + /// + /// Enabled by default. + fn with_prevent_default(self, prevent_default: bool) -> Self; + + /// Whether the canvas should be focusable using the tab key. This is necessary to capture + /// canvas keyboard events. + /// + /// Enabled by default. + fn with_focusable(self, focusable: bool) -> Self; + + /// On window creation, append the canvas element to the web page if it isn't already. + /// + /// Disabled by default. + fn with_append(self, append: bool) -> Self; +} + +impl WindowAttributesExtWebSys for WindowAttributes { + fn with_canvas(mut self, canvas: Option) -> Self { + self.platform_specific.set_canvas(canvas); + self + } + + fn with_prevent_default(mut self, prevent_default: bool) -> Self { + self.platform_specific.prevent_default = prevent_default; + self + } + + fn with_focusable(mut self, focusable: bool) -> Self { + self.platform_specific.focusable = focusable; + self + } + + fn with_append(mut self, append: bool) -> Self { + self.platform_specific.append = append; + self + } +} + +/// Additional methods on `EventLoop` that are specific to the web. +pub trait EventLoopExtWebSys { + /// A type provided by the user that can be passed through `Event::UserEvent`. + type UserEvent: 'static; + + /// Initializes the winit event loop. + /// + /// Unlike + #[cfg_attr(all(web_platform, target_feature = "exception-handling"), doc = "`run_app()`")] + #[cfg_attr( + not(all(web_platform, target_feature = "exception-handling")), + doc = "[`run_app()`]" + )] + /// [^1], this returns immediately, and doesn't throw an exception in order to + /// satisfy its [`!`] return type. + /// + /// Once the event loop has been destroyed, it's possible to reinitialize another event loop + /// by calling this function again. This can be useful if you want to recreate the event loop + /// while the WebAssembly module is still loaded. For example, this can be used to recreate the + /// event loop when switching between tabs on a single page application. + #[rustfmt::skip] + /// + #[cfg_attr( + not(all(web_platform, target_feature = "exception-handling")), + doc = "[`run_app()`]: EventLoop::run_app()" + )] + /// [^1]: `run_app()` is _not_ available on WASM when the target supports `exception-handling`. + fn spawn_app + 'static>(self, app: A); + + /// See [`spawn_app`]. + /// + /// [`spawn_app`]: Self::spawn_app + #[deprecated = "use EventLoopExtWebSys::spawn_app"] + fn spawn(self, event_handler: F) + where + F: 'static + FnMut(Event, &ActiveEventLoop); +} + +impl EventLoopExtWebSys for EventLoop { + type UserEvent = T; + + fn spawn_app + 'static>(self, mut app: A) { + self.event_loop.spawn(move |event, event_loop| { + event_loop::dispatch_event_for_app(&mut app, event_loop, event) + }); + } + + fn spawn(self, event_handler: F) + where + F: 'static + FnMut(Event, &ActiveEventLoop), + { + self.event_loop.spawn(event_handler) + } +} + +pub trait ActiveEventLoopExtWebSys { + /// Sets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollStrategy`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn set_poll_strategy(&self, strategy: PollStrategy); + + /// Gets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollStrategy`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn poll_strategy(&self) -> PollStrategy; + + /// Async version of [`ActiveEventLoop::create_custom_cursor()`] which waits until the + /// cursor has completely finished loading. + fn create_custom_cursor_async( + &self, + source: CustomCursorSource, + ) -> CustomCursorFuture; +} + +impl ActiveEventLoopExtWebSys for ActiveEventLoop { + #[inline] + fn create_custom_cursor_async( + &self, + source: CustomCursorSource, + ) -> CustomCursorFuture { + self.p.create_custom_cursor_async(source) + } + + #[inline] + fn set_poll_strategy(&self, strategy: PollStrategy) { + self.p.set_poll_strategy(strategy); + } + + #[inline] + fn poll_strategy(&self) -> PollStrategy { + self.p.poll_strategy() + } +} + +/// Strategy used for [`ControlFlow::Poll`][crate::event_loop::ControlFlow::Poll]. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum PollStrategy { + /// Uses [`Window.requestIdleCallback()`] to queue the next event loop. If not available + /// this will fallback to [`setTimeout()`]. + /// + /// This strategy will wait for the browser to enter an idle period before running and might + /// be affected by browser throttling. + /// + /// [`Window.requestIdleCallback()`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback + /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout + IdleCallback, + /// Uses the [Prioritized Task Scheduling API] to queue the next event loop. If not available + /// this will fallback to [`setTimeout()`]. + /// + /// This strategy will run as fast as possible without disturbing users from interacting with + /// the page and is not affected by browser throttling. + /// + /// This is the default strategy. + /// + /// [Prioritized Task Scheduling API]: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API + /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout + #[default] + Scheduler, +} + +pub trait CustomCursorExtWebSys { + /// Returns if this cursor is an animation. + fn is_animation(&self) -> bool; + + /// Creates a new cursor from a URL pointing to an image. + /// It uses the [url css function](https://developer.mozilla.org/en-US/docs/Web/CSS/url), + /// but browser support for image formats is inconsistent. Using [PNG] is recommended. + /// + /// [PNG]: https://en.wikipedia.org/wiki/PNG + fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorSource; + + /// Crates a new animated cursor from multiple [`CustomCursor`]s. + /// Supplied `cursors` can't be empty or other animations. + fn from_animation( + duration: Duration, + cursors: Vec, + ) -> Result; +} + +impl CustomCursorExtWebSys for CustomCursor { + fn is_animation(&self) -> bool { + self.inner.animation + } + + fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorSource { + CustomCursorSource { + inner: PlatformCustomCursorSource::Url { + url, + hotspot_x, + hotspot_y, + }, + } + } + + fn from_animation( + duration: Duration, + cursors: Vec, + ) -> Result { + if cursors.is_empty() { + return Err(BadAnimation::Empty); + } + + if cursors.iter().any(CustomCursor::is_animation) { + return Err(BadAnimation::Animation); + } + + Ok(CustomCursorSource { + inner: PlatformCustomCursorSource::Animation { duration, cursors }, + }) + } +} + +/// An error produced when using [`CustomCursor::from_animation`] with invalid arguments. +#[derive(Debug, Clone)] +pub enum BadAnimation { + /// Produced when no cursors were supplied. + Empty, + /// Produced when a supplied cursor is an animation. + Animation, +} + +impl fmt::Display for BadAnimation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Empty => write!(f, "No cursors supplied"), + Self::Animation => write!(f, "A supplied cursor is an animtion"), + } + } +} + +impl Error for BadAnimation {} + +#[cfg(not(web_platform))] +struct PlatformCustomCursorFuture; + +#[derive(Debug)] +pub struct CustomCursorFuture(pub(crate) PlatformCustomCursorFuture); + +impl Future for CustomCursorFuture { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.0) + .poll(cx) + .map_ok(|cursor| CustomCursor { inner: cursor }) + } +} + +#[derive(Clone, Debug)] +pub enum CustomCursorError { + Blob, + Decode(String), + Animation, +} + +impl Display for CustomCursorError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Blob => write!(f, "failed to create `Blob`"), + Self::Decode(error) => write!(f, "failed to decode image: {error}"), + Self::Animation => { + write!(f, "found `CustomCursor` that is an animation when building an animation") + } + } + } +} diff --git a/rio-window/src/platform/windows.rs b/rio-window/src/platform/windows.rs new file mode 100644 index 00000000..55230c5a --- /dev/null +++ b/rio-window/src/platform/windows.rs @@ -0,0 +1,718 @@ +//! # Windows +//! +//! The supported OS version is Windows 7 or higher, though Windows 10 is +//! tested regularly. +use std::borrow::Borrow; +use std::ffi::c_void; +use std::path::Path; + +use crate::dpi::PhysicalSize; +use crate::event::DeviceId; +use crate::event_loop::EventLoopBuilder; +use crate::monitor::MonitorHandle; +use crate::window::{BadIcon, Icon, Window, WindowAttributes}; + +/// Window Handle type used by Win32 API +pub type HWND = isize; +/// Menu Handle type used by Win32 API +pub type HMENU = isize; +/// Monitor Handle type used by Win32 API +pub type HMONITOR = isize; + +/// Describes a system-drawn backdrop material of a window. +/// +/// For a detailed explanation, see [`DWM_SYSTEMBACKDROP_TYPE docs`]. +/// +/// [`DWM_SYSTEMBACKDROP_TYPE docs`]: https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwm_systembackdrop_type +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BackdropType { + /// Corresponds to `DWMSBT_AUTO`. + /// + /// Usually draws a default backdrop effect on the title bar. + #[default] + Auto = 0, + + /// Corresponds to `DWMSBT_NONE`. + None = 1, + + /// Corresponds to `DWMSBT_MAINWINDOW`. + /// + /// Draws the Mica backdrop material. + MainWindow = 2, + + /// Corresponds to `DWMSBT_TRANSIENTWINDOW`. + /// + /// Draws the Background Acrylic backdrop material. + TransientWindow = 3, + + /// Corresponds to `DWMSBT_TABBEDWINDOW`. + /// + /// Draws the Alt Mica backdrop material. + TabbedWindow = 4, +} + +/// Describes a color used by Windows +#[repr(transparent)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Color(u32); + +impl Color { + // Special constant only valid for the window border and therefore modeled using Option + // for user facing code + const NONE: Color = Color(0xfffffffe); + /// Use the system's default color + pub const SYSTEM_DEFAULT: Color = Color(0xffffffff); + + /// Create a new color from the given RGB values + pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self { + Self((r as u32) | ((g as u32) << 8) | ((b as u32) << 16)) + } +} + +impl Default for Color { + fn default() -> Self { + Self::SYSTEM_DEFAULT + } +} + +/// Describes how the corners of a window should look like. +/// +/// For a detailed explanation, see [`DWM_WINDOW_CORNER_PREFERENCE docs`]. +/// +/// [`DWM_WINDOW_CORNER_PREFERENCE docs`]: https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwm_window_corner_preference +#[repr(i32)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CornerPreference { + /// Corresponds to `DWMWCP_DEFAULT`. + /// + /// Let the system decide when to round window corners. + #[default] + Default = 0, + + /// Corresponds to `DWMWCP_DONOTROUND`. + /// + /// Never round window corners. + DoNotRound = 1, + + /// Corresponds to `DWMWCP_ROUND`. + /// + /// Round the corners, if appropriate. + Round = 2, + + /// Corresponds to `DWMWCP_ROUNDSMALL`. + /// + /// Round the corners if appropriate, with a small radius. + RoundSmall = 3, +} + +/// A wrapper around a [`Window`] that ignores thread-specific window handle limitations. +/// +/// See [`WindowBorrowExtWindows::any_thread`] for more information. +#[derive(Debug)] +pub struct AnyThread(W); + +impl> AnyThread { + /// Get a reference to the inner window. + #[inline] + pub fn get_ref(&self) -> &Window { + self.0.borrow() + } + + /// Get a reference to the inner object. + #[inline] + pub fn inner(&self) -> &W { + &self.0 + } + + /// Unwrap and get the inner window. + #[inline] + pub fn into_inner(self) -> W { + self.0 + } +} + +impl> AsRef for AnyThread { + fn as_ref(&self) -> &Window { + self.get_ref() + } +} + +impl> Borrow for AnyThread { + fn borrow(&self) -> &Window { + self.get_ref() + } +} + +impl> std::ops::Deref for AnyThread { + type Target = Window; + + fn deref(&self) -> &Self::Target { + self.get_ref() + } +} + +#[cfg(feature = "rwh_06")] +impl> rwh_06::HasWindowHandle for AnyThread { + fn window_handle(&self) -> Result, rwh_06::HandleError> { + // SAFETY: The top level user has asserted this is only used safely. + unsafe { self.get_ref().window_handle_any_thread() } + } +} + +/// Additional methods on `EventLoop` that are specific to Windows. +pub trait EventLoopBuilderExtWindows { + /// Whether to allow the event loop to be created off of the main thread. + /// + /// By default, the window is only allowed to be created on the main + /// thread, to make platform compatibility easier. + /// + /// # `Window` caveats + /// + /// Note that any `Window` created on the new thread will be destroyed when the thread + /// terminates. Attempting to use a `Window` after its parent thread terminates has + /// unspecified, although explicitly not undefined, behavior. + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self; + + /// Whether to enable process-wide DPI awareness. + /// + /// By default, `winit` will attempt to enable process-wide DPI awareness. If + /// that's undesirable, you can disable it with this function. + /// + /// # Example + /// + /// Disable process-wide DPI awareness. + /// + /// ``` + /// use winit::event_loop::EventLoopBuilder; + /// #[cfg(target_os = "windows")] + /// use winit::platform::windows::EventLoopBuilderExtWindows; + /// + /// let mut builder = EventLoopBuilder::new(); + /// #[cfg(target_os = "windows")] + /// builder.with_dpi_aware(false); + /// # if false { // We can't test this part + /// let event_loop = builder.build(); + /// # } + /// ``` + fn with_dpi_aware(&mut self, dpi_aware: bool) -> &mut Self; + + /// A callback to be executed before dispatching a win32 message to the window procedure. + /// Return true to disable winit's internal message dispatching. + /// + /// # Example + /// + /// ``` + /// # use windows_sys::Win32::UI::WindowsAndMessaging::{ACCEL, CreateAcceleratorTableW, TranslateAcceleratorW, DispatchMessageW, TranslateMessage, MSG}; + /// use winit::event_loop::EventLoopBuilder; + /// #[cfg(target_os = "windows")] + /// use winit::platform::windows::EventLoopBuilderExtWindows; + /// + /// let mut builder = EventLoopBuilder::new(); + /// #[cfg(target_os = "windows")] + /// builder.with_msg_hook(|msg|{ + /// let msg = msg as *const MSG; + /// # let accels: Vec = Vec::new(); + /// let translated = unsafe { + /// TranslateAcceleratorW( + /// (*msg).hwnd, + /// CreateAcceleratorTableW(accels.as_ptr() as _, 1), + /// msg, + /// ) == 1 + /// }; + /// translated + /// }); + /// ``` + fn with_msg_hook(&mut self, callback: F) -> &mut Self + where + F: FnMut(*const c_void) -> bool + 'static; +} + +impl EventLoopBuilderExtWindows for EventLoopBuilder { + #[inline] + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self { + self.platform_specific.any_thread = any_thread; + self + } + + #[inline] + fn with_dpi_aware(&mut self, dpi_aware: bool) -> &mut Self { + self.platform_specific.dpi_aware = dpi_aware; + self + } + + #[inline] + fn with_msg_hook(&mut self, callback: F) -> &mut Self + where + F: FnMut(*const c_void) -> bool + 'static, + { + self.platform_specific.msg_hook = Some(Box::new(callback)); + self + } +} + +/// Additional methods on `Window` that are specific to Windows. +pub trait WindowExtWindows { + /// Enables or disables mouse and keyboard input to the specified window. + /// + /// A window must be enabled before it can be activated. + /// If an application has create a modal dialog box by disabling its owner window + /// (as described in [`WindowAttributesExtWindows::with_owner_window`]), the application must + /// enable the owner window before destroying the dialog box. + /// Otherwise, another window will receive the keyboard focus and be activated. + /// + /// If a child window is disabled, it is ignored when the system tries to determine which + /// window should receive mouse messages. + /// + /// For more information, see + /// and + fn set_enable(&self, enabled: bool); + + /// This sets `ICON_BIG`. A good ceiling here is 256x256. + fn set_taskbar_icon(&self, taskbar_icon: Option); + + /// Whether to show or hide the window icon in the taskbar. + fn set_skip_taskbar(&self, skip: bool); + + /// Shows or hides the background drop shadow for undecorated windows. + /// + /// Enabling the shadow causes a thin 1px line to appear on the top of the window. + fn set_undecorated_shadow(&self, shadow: bool); + + /// Sets system-drawn backdrop type. + /// + /// Requires Windows 11 build 22523+. + fn set_system_backdrop(&self, backdrop_type: BackdropType); + + /// Sets the color of the window border. + /// + /// Supported starting with Windows 11 Build 22000. + fn set_border_color(&self, color: Option); + + /// Sets the background color of the title bar. + /// + /// Supported starting with Windows 11 Build 22000. + fn set_title_background_color(&self, color: Option); + + /// Sets the color of the window title. + /// + /// Supported starting with Windows 11 Build 22000. + fn set_title_text_color(&self, color: Color); + + /// Sets the preferred style of the window corners. + /// + /// Supported starting with Windows 11 Build 22000. + fn set_corner_preference(&self, preference: CornerPreference); + + /// Get the raw window handle for this [`Window`] without checking for thread affinity. + /// + /// Window handles in Win32 have a property called "thread affinity" that ties them to their + /// origin thread. Some operations can only happen on the window's origin thread, while others + /// can be called from any thread. For example, [`SetWindowSubclass`] is not thread safe while + /// [`GetDC`] is thread safe. + /// + /// In Rust terms, the window handle is `Send` sometimes but `!Send` other times. + /// + /// Therefore, in order to avoid confusing threading errors, [`Window`] only returns the + /// window handle when the [`window_handle`] function is called from the thread that created + /// the window. In other cases, it returns an [`Unavailable`] error. + /// + /// However in some cases you may already know that you are using the window handle for + /// operations that are guaranteed to be thread-safe. In which case this function aims + /// to provide an escape hatch so these functions are still accessible from other threads. + /// + /// # Safety + /// + /// It is the responsibility of the user to only pass the window handle into thread-safe + /// Win32 APIs. + /// + /// [`SetWindowSubclass`]: https://learn.microsoft.com/en-us/windows/win32/api/commctrl/nf-commctrl-setwindowsubclass + /// [`GetDC`]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdc + /// [`Window`]: crate::window::Window + /// [`window_handle`]: https://docs.rs/raw-window-handle/latest/raw_window_handle/trait.HasWindowHandle.html#tymethod.window_handle + /// [`Unavailable`]: https://docs.rs/raw-window-handle/latest/raw_window_handle/enum.HandleError.html#variant.Unavailable + /// + /// ## Example + /// + /// ```no_run + /// # use winit::window::Window; + /// # fn scope(window: Window) { + /// use std::thread; + /// use winit::platform::windows::WindowExtWindows; + /// use winit::raw_window_handle::HasWindowHandle; + /// + /// // We can get the window handle on the current thread. + /// let handle = window.window_handle().unwrap(); + /// + /// // However, on another thread, we can't! + /// thread::spawn(move || { + /// assert!(window.window_handle().is_err()); + /// + /// // We can use this function as an escape hatch. + /// let handle = unsafe { window.window_handle_any_thread().unwrap() }; + /// }); + /// # } + /// ``` + #[cfg(feature = "rwh_06")] + unsafe fn window_handle_any_thread( + &self, + ) -> Result, rwh_06::HandleError>; +} + +impl WindowExtWindows for Window { + #[inline] + fn set_enable(&self, enabled: bool) { + self.window.set_enable(enabled) + } + + #[inline] + fn set_taskbar_icon(&self, taskbar_icon: Option) { + self.window.set_taskbar_icon(taskbar_icon) + } + + #[inline] + fn set_skip_taskbar(&self, skip: bool) { + self.window.set_skip_taskbar(skip) + } + + #[inline] + fn set_undecorated_shadow(&self, shadow: bool) { + self.window.set_undecorated_shadow(shadow) + } + + #[inline] + fn set_system_backdrop(&self, backdrop_type: BackdropType) { + self.window.set_system_backdrop(backdrop_type) + } + + #[inline] + fn set_border_color(&self, color: Option) { + self.window.set_border_color(color.unwrap_or(Color::NONE)) + } + + #[inline] + fn set_title_background_color(&self, color: Option) { + // The windows docs don't mention NONE as a valid options but it works in practice and is + // useful to circumvent the Windows option "Show accent color on title bars and + // window borders" + self.window + .set_title_background_color(color.unwrap_or(Color::NONE)) + } + + #[inline] + fn set_title_text_color(&self, color: Color) { + self.window.set_title_text_color(color) + } + + #[inline] + fn set_corner_preference(&self, preference: CornerPreference) { + self.window.set_corner_preference(preference) + } + + #[cfg(feature = "rwh_06")] + unsafe fn window_handle_any_thread( + &self, + ) -> Result, rwh_06::HandleError> { + unsafe { + let handle = self.window.rwh_06_no_thread_check()?; + + // SAFETY: The handle is valid in this context. + Ok(rwh_06::WindowHandle::borrow_raw(handle)) + } + } +} + +/// Additional methods for anything that dereference to [`Window`]. +/// +/// [`Window`]: crate::window::Window +pub trait WindowBorrowExtWindows: Borrow + Sized { + /// Create an object that allows accessing the inner window handle in a thread-unsafe way. + /// + /// It is possible to call [`window_handle_any_thread`] to get around Windows's thread + /// affinity limitations. However, it may be desired to pass the [`Window`] into something + /// that requires the [`HasWindowHandle`] trait, while ignoring thread affinity limitations. + /// + /// This function wraps anything that implements `Borrow` into a structure that + /// uses the inner window handle as a mean of implementing [`HasWindowHandle`]. It wraps + /// `Window`, `&Window`, `Arc`, and other reference types. + /// + /// # Safety + /// + /// It is the responsibility of the user to only pass the window handle into thread-safe + /// Win32 APIs. + /// + /// [`Window`]: crate::window::Window + #[cfg_attr( + feature = "rwh_06", + doc = "[`HasWindowHandle`]: rwh_06::HasWindowHandle", + doc = "[`window_handle_any_thread`]: WindowExtWindows::window_handle_any_thread" + )] + #[cfg_attr( + not(feature = "rwh_06"), + doc = "[`HasWindowHandle`]: #only-available-with-rwh_06", + doc = "[`window_handle_any_thread`]: #only-available-with-rwh_06" + )] + unsafe fn any_thread(self) -> AnyThread { + AnyThread(self) + } +} + +impl + Sized> WindowBorrowExtWindows for W {} + +/// Additional methods on `WindowAttributes` that are specific to Windows. +#[allow(rustdoc::broken_intra_doc_links)] +pub trait WindowAttributesExtWindows { + /// Set an owner to the window to be created. Can be used to create a dialog box, for example. + /// This only works when [`WindowAttributes::with_parent_window`] isn't called or set to `None`. + /// Can be used in combination with + /// [`WindowExtWindows::set_enable(false)`][WindowExtWindows::set_enable] on the owner + /// window to create a modal dialog box. + /// + /// From MSDN: + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// + /// For more information, see + fn with_owner_window(self, parent: HWND) -> Self; + + /// Sets a menu on the window to be created. + /// + /// Parent and menu are mutually exclusive; a child window cannot have a menu! + /// + /// The menu must have been manually created beforehand with [`CreateMenu`] or similar. + /// + /// Note: Dark mode cannot be supported for win32 menus, it's simply not possible to change how + /// the menus look. If you use this, it is recommended that you combine it with + /// `with_theme(Some(Theme::Light))` to avoid a jarring effect. + #[cfg_attr( + windows_platform, + doc = "[`CreateMenu`]: windows_sys::Win32::UI::WindowsAndMessaging::CreateMenu" + )] + #[cfg_attr( + not(windows_platform), + doc = "[`CreateMenu`]: #only-available-on-windows" + )] + fn with_menu(self, menu: HMENU) -> Self; + + /// This sets `ICON_BIG`. A good ceiling here is 256x256. + fn with_taskbar_icon(self, taskbar_icon: Option) -> Self; + + /// This sets `WS_EX_NOREDIRECTIONBITMAP`. + fn with_no_redirection_bitmap(self, flag: bool) -> Self; + + /// Enables or disables drag and drop support (enabled by default). Will interfere with other + /// crates that use multi-threaded COM API (`CoInitializeEx` with `COINIT_MULTITHREADED` + /// instead of `COINIT_APARTMENTTHREADED`) on the same thread. Note that winit may still + /// attempt to initialize COM API regardless of this option. Currently only fullscreen mode + /// does that, but there may be more in the future. If you need COM API with + /// `COINIT_MULTITHREADED` you must initialize it before calling any winit functions. See for more information. + fn with_drag_and_drop(self, flag: bool) -> Self; + + /// Whether show or hide the window icon in the taskbar. + fn with_skip_taskbar(self, skip: bool) -> Self; + + /// Customize the window class name. + fn with_class_name>(self, class_name: S) -> Self; + + /// Shows or hides the background drop shadow for undecorated windows. + /// + /// The shadow is hidden by default. + /// Enabling the shadow causes a thin 1px line to appear on the top of the window. + fn with_undecorated_shadow(self, shadow: bool) -> Self; + + /// Sets system-drawn backdrop type. + /// + /// Requires Windows 11 build 22523+. + fn with_system_backdrop(self, backdrop_type: BackdropType) -> Self; + + /// This sets or removes `WS_CLIPCHILDREN` style. + fn with_clip_children(self, flag: bool) -> Self; + + /// Sets the color of the window border. + /// + /// Supported starting with Windows 11 Build 22000. + fn with_border_color(self, color: Option) -> Self; + + /// Sets the background color of the title bar. + /// + /// Supported starting with Windows 11 Build 22000. + fn with_title_background_color(self, color: Option) -> Self; + + /// Sets the color of the window title. + /// + /// Supported starting with Windows 11 Build 22000. + fn with_title_text_color(self, color: Color) -> Self; + + /// Sets the preferred style of the window corners. + /// + /// Supported starting with Windows 11 Build 22000. + fn with_corner_preference(self, corners: CornerPreference) -> Self; +} + +impl WindowAttributesExtWindows for WindowAttributes { + #[inline] + fn with_owner_window(mut self, parent: HWND) -> Self { + self.platform_specific.owner = Some(parent); + self + } + + #[inline] + fn with_menu(mut self, menu: HMENU) -> Self { + self.platform_specific.menu = Some(menu); + self + } + + #[inline] + fn with_taskbar_icon(mut self, taskbar_icon: Option) -> Self { + self.platform_specific.taskbar_icon = taskbar_icon; + self + } + + #[inline] + fn with_no_redirection_bitmap(mut self, flag: bool) -> Self { + self.platform_specific.no_redirection_bitmap = flag; + self + } + + #[inline] + fn with_drag_and_drop(mut self, flag: bool) -> Self { + self.platform_specific.drag_and_drop = flag; + self + } + + #[inline] + fn with_skip_taskbar(mut self, skip: bool) -> Self { + self.platform_specific.skip_taskbar = skip; + self + } + + #[inline] + fn with_class_name>(mut self, class_name: S) -> Self { + self.platform_specific.class_name = class_name.into(); + self + } + + #[inline] + fn with_undecorated_shadow(mut self, shadow: bool) -> Self { + self.platform_specific.decoration_shadow = shadow; + self + } + + #[inline] + fn with_system_backdrop(mut self, backdrop_type: BackdropType) -> Self { + self.platform_specific.backdrop_type = backdrop_type; + self + } + + #[inline] + fn with_clip_children(mut self, flag: bool) -> Self { + self.platform_specific.clip_children = flag; + self + } + + #[inline] + fn with_border_color(mut self, color: Option) -> Self { + self.platform_specific.border_color = Some(color.unwrap_or(Color::NONE)); + self + } + + #[inline] + fn with_title_background_color(mut self, color: Option) -> Self { + self.platform_specific.title_background_color = + Some(color.unwrap_or(Color::NONE)); + self + } + + #[inline] + fn with_title_text_color(mut self, color: Color) -> Self { + self.platform_specific.title_text_color = Some(color); + self + } + + #[inline] + fn with_corner_preference(mut self, corners: CornerPreference) -> Self { + self.platform_specific.corner_preference = Some(corners); + self + } +} + +/// Additional methods on `MonitorHandle` that are specific to Windows. +pub trait MonitorHandleExtWindows { + /// Returns the name of the monitor adapter specific to the Win32 API. + fn native_id(&self) -> String; + + /// Returns the handle of the monitor - `HMONITOR`. + fn hmonitor(&self) -> HMONITOR; +} + +impl MonitorHandleExtWindows for MonitorHandle { + #[inline] + fn native_id(&self) -> String { + self.inner.native_identifier() + } + + #[inline] + fn hmonitor(&self) -> HMONITOR { + self.inner.hmonitor() + } +} + +/// Additional methods on `DeviceId` that are specific to Windows. +pub trait DeviceIdExtWindows { + /// Returns an identifier that persistently refers to this specific device. + /// + /// Will return `None` if the device is no longer available. + fn persistent_identifier(&self) -> Option; +} + +impl DeviceIdExtWindows for DeviceId { + #[inline] + fn persistent_identifier(&self) -> Option { + self.0.persistent_identifier() + } +} + +/// Additional methods on `Icon` that are specific to Windows. +pub trait IconExtWindows: Sized { + /// Create an icon from a file path. + /// + /// Specify `size` to load a specific icon size from the file, or `None` to load the default + /// icon size from the file. + /// + /// In cases where the specified size does not exist in the file, Windows may perform scaling + /// to get an icon of the desired size. + fn from_path>( + path: P, + size: Option>, + ) -> Result; + + /// Create an icon from a resource embedded in this executable or library. + /// + /// Specify `size` to load a specific icon size from the file, or `None` to load the default + /// icon size from the file. + /// + /// In cases where the specified size does not exist in the file, Windows may perform scaling + /// to get an icon of the desired size. + fn from_resource( + ordinal: u16, + size: Option>, + ) -> Result; +} + +impl IconExtWindows for Icon { + fn from_path>( + path: P, + size: Option>, + ) -> Result { + let win_icon = crate::platform_impl::WinIcon::from_path(path, size)?; + Ok(Icon { inner: win_icon }) + } + + fn from_resource( + ordinal: u16, + size: Option>, + ) -> Result { + let win_icon = crate::platform_impl::WinIcon::from_resource(ordinal, size)?; + Ok(Icon { inner: win_icon }) + } +} diff --git a/rio-window/src/platform/x11.rs b/rio-window/src/platform/x11.rs new file mode 100644 index 00000000..97c26959 --- /dev/null +++ b/rio-window/src/platform/x11.rs @@ -0,0 +1,252 @@ +//! # X11 +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::event_loop::{ActiveEventLoop, EventLoopBuilder}; +use crate::monitor::MonitorHandle; +use crate::window::{Window, WindowAttributes}; + +use crate::dpi::Size; + +/// X window type. Maps directly to +/// [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html). +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum WindowType { + /// A desktop feature. This can include a single window containing desktop icons with the same + /// dimensions as the screen, allowing the desktop environment to have full control of the + /// desktop, without the need for proxying root window clicks. + Desktop, + /// A dock or panel feature. Typically a Window Manager would keep such windows on top of all + /// other windows. + Dock, + /// Toolbar windows. "Torn off" from the main application. + Toolbar, + /// Pinnable menu windows. "Torn off" from the main application. + Menu, + /// A small persistent utility window, such as a palette or toolbox. + Utility, + /// The window is a splash screen displayed as an application is starting up. + Splash, + /// This is a dialog window. + Dialog, + /// A dropdown menu that usually appears when the user clicks on an item in a menu bar. + /// This property is typically used on override-redirect windows. + DropdownMenu, + /// A popup menu that usually appears when the user right clicks on an object. + /// This property is typically used on override-redirect windows. + PopupMenu, + /// A tooltip window. Usually used to show additional information when hovering over an object + /// with the cursor. This property is typically used on override-redirect windows. + Tooltip, + /// The window is a notification. + /// This property is typically used on override-redirect windows. + Notification, + /// This should be used on the windows that are popped up by combo boxes. + /// This property is typically used on override-redirect windows. + Combo, + /// This indicates the window is being dragged. + /// This property is typically used on override-redirect windows. + Dnd, + /// This is a normal, top-level window. + #[default] + Normal, +} + +/// The first argument in the provided hook will be the pointer to `XDisplay` +/// and the second one the pointer to [`XErrorEvent`]. The returned `bool` is an +/// indicator whether the error was handled by the callback. +/// +/// [`XErrorEvent`]: https://linux.die.net/man/3/xerrorevent +pub type XlibErrorHook = + Box bool + Send + Sync>; + +/// A unique identifier for an X11 visual. +pub type XVisualID = u32; + +/// A unique identifier for an X11 window. +pub type XWindow = u32; + +/// Hook to winit's xlib error handling callback. +/// +/// This method is provided as a safe way to handle the errors coming from X11 +/// when using xlib in external crates, like glutin for GLX access. Trying to +/// handle errors by speculating with `XSetErrorHandler` is [`unsafe`]. +/// +/// **Be aware that your hook is always invoked and returning `true` from it will +/// prevent `winit` from getting the error itself. It's wise to always return +/// `false` if you're not initiated the `Sync`.** +/// +/// [`unsafe`]: https://www.remlab.net/op/xlib.shtml +#[inline] +pub fn register_xlib_error_hook(hook: XlibErrorHook) { + // Append new hook. + unsafe { + crate::platform_impl::XLIB_ERROR_HOOKS + .lock() + .unwrap() + .push(hook); + } +} + +/// Additional methods on [`ActiveEventLoop`] that are specific to X11. +pub trait ActiveEventLoopExtX11 { + /// True if the [`ActiveEventLoop`] uses X11. + fn is_x11(&self) -> bool; +} + +impl ActiveEventLoopExtX11 for ActiveEventLoop { + #[inline] + fn is_x11(&self) -> bool { + !self.p.is_wayland() + } +} + +/// Additional methods on [`EventLoopBuilder`] that are specific to X11. +pub trait EventLoopBuilderExtX11 { + /// Force using X11. + fn with_x11(&mut self) -> &mut Self; + + /// Whether to allow the event loop to be created off of the main thread. + /// + /// By default, the window is only allowed to be created on the main + /// thread, to make platform compatibility easier. + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self; +} + +impl EventLoopBuilderExtX11 for EventLoopBuilder { + #[inline] + fn with_x11(&mut self) -> &mut Self { + self.platform_specific.forced_backend = Some(crate::platform_impl::Backend::X); + self + } + + #[inline] + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self { + self.platform_specific.any_thread = any_thread; + self + } +} + +/// Additional methods on [`Window`] that are specific to X11. +pub trait WindowExtX11 {} + +impl WindowExtX11 for Window {} + +/// Additional methods on [`WindowAttributes`] that are specific to X11. +pub trait WindowAttributesExtX11 { + /// Create this window with a specific X11 visual. + fn with_x11_visual(self, visual_id: XVisualID) -> Self; + + fn with_x11_screen(self, screen_id: i32) -> Self; + + /// Build window with the given `general` and `instance` names. + /// + /// The `general` sets general class of `WM_CLASS(STRING)`, while `instance` set the + /// instance part of it. The resulted property looks like `WM_CLASS(STRING) = "instance", + /// "general"`. + /// + /// For details about application ID conventions, see the + /// [Desktop Entry Spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id) + fn with_name(self, general: impl Into, instance: impl Into) -> Self; + + /// Build window with override-redirect flag; defaults to false. + fn with_override_redirect(self, override_redirect: bool) -> Self; + + /// Build window with `_NET_WM_WINDOW_TYPE` hints; defaults to `Normal`. + fn with_x11_window_type(self, x11_window_type: Vec) -> Self; + + /// Build window with base size hint. + /// + /// ``` + /// # use winit::dpi::{LogicalSize, PhysicalSize}; + /// # use winit::window::Window; + /// # use winit::platform::x11::WindowAttributesExtX11; + /// // Specify the size in logical dimensions like this: + /// Window::default_attributes().with_base_size(LogicalSize::new(400.0, 200.0)); + /// + /// // Or specify the size in physical dimensions like this: + /// Window::default_attributes().with_base_size(PhysicalSize::new(400, 200)); + /// ``` + fn with_base_size>(self, base_size: S) -> Self; + + /// Embed this window into another parent window. + /// + /// # Example + /// + /// ```no_run + /// use winit::window::Window; + /// use winit::event_loop::ActiveEventLoop; + /// use winit::platform::x11::{XWindow, WindowAttributesExtX11}; + /// # fn create_window(event_loop: &ActiveEventLoop) -> Result<(), Box> { + /// let parent_window_id = std::env::args().nth(1).unwrap().parse::()?; + /// let window_attributes = Window::default_attributes().with_embed_parent_window(parent_window_id); + /// let window = event_loop.create_window(window_attributes)?; + /// # Ok(()) } + /// ``` + fn with_embed_parent_window(self, parent_window_id: XWindow) -> Self; +} + +impl WindowAttributesExtX11 for WindowAttributes { + #[inline] + fn with_x11_visual(mut self, visual_id: XVisualID) -> Self { + self.platform_specific.x11.visual_id = Some(visual_id); + self + } + + #[inline] + fn with_x11_screen(mut self, screen_id: i32) -> Self { + self.platform_specific.x11.screen_id = Some(screen_id); + self + } + + #[inline] + fn with_name( + mut self, + general: impl Into, + instance: impl Into, + ) -> Self { + self.platform_specific.name = Some(crate::platform_impl::ApplicationName::new( + general.into(), + instance.into(), + )); + self + } + + #[inline] + fn with_override_redirect(mut self, override_redirect: bool) -> Self { + self.platform_specific.x11.override_redirect = override_redirect; + self + } + + #[inline] + fn with_x11_window_type(mut self, x11_window_types: Vec) -> Self { + self.platform_specific.x11.x11_window_types = x11_window_types; + self + } + + #[inline] + fn with_base_size>(mut self, base_size: S) -> Self { + self.platform_specific.x11.base_size = Some(base_size.into()); + self + } + + #[inline] + fn with_embed_parent_window(mut self, parent_window_id: XWindow) -> Self { + self.platform_specific.x11.embed_window = Some(parent_window_id); + self + } +} + +/// Additional methods on `MonitorHandle` that are specific to X11. +pub trait MonitorHandleExtX11 { + /// Returns the inner identifier of the monitor. + fn native_id(&self) -> u32; +} + +impl MonitorHandleExtX11 for MonitorHandle { + #[inline] + fn native_id(&self) -> u32 { + self.inner.native_identifier() + } +} diff --git a/rio-window/src/platform_impl/ios/app_delegate.rs b/rio-window/src/platform_impl/ios/app_delegate.rs new file mode 100644 index 00000000..6a363027 --- /dev/null +++ b/rio-window/src/platform_impl/ios/app_delegate.rs @@ -0,0 +1,107 @@ +use objc2::{declare_class, mutability, ClassType, DeclaredClass}; +use objc2_foundation::{MainThreadMarker, NSObject, NSObjectProtocol}; +use objc2_ui_kit::{UIApplication, UIWindow}; + +use super::app_state::{self, EventWrapper}; +use super::window::WinitUIWindow; +use crate::event::{Event, WindowEvent}; +use crate::window::WindowId as RootWindowId; + +declare_class!( + pub struct AppDelegate; + + unsafe impl ClassType for AppDelegate { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + const NAME: &'static str = "WinitApplicationDelegate"; + } + + impl DeclaredClass for AppDelegate {} + + // UIApplicationDelegate protocol + unsafe impl AppDelegate { + #[method(application:didFinishLaunchingWithOptions:)] + fn did_finish_launching(&self, _application: &UIApplication, _: *mut NSObject) -> bool { + app_state::did_finish_launching(MainThreadMarker::new().unwrap()); + true + } + + #[method(applicationDidBecomeActive:)] + fn did_become_active(&self, _application: &UIApplication) { + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Resumed)) + } + + #[method(applicationWillResignActive:)] + fn will_resign_active(&self, _application: &UIApplication) { + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Suspended)) + } + + #[method(applicationWillEnterForeground:)] + fn will_enter_foreground(&self, application: &UIApplication) { + self.send_occluded_event_for_all_windows(application, false); + } + + #[method(applicationDidEnterBackground:)] + fn did_enter_background(&self, application: &UIApplication) { + self.send_occluded_event_for_all_windows(application, true); + } + + #[method(applicationWillTerminate:)] + fn will_terminate(&self, application: &UIApplication) { + let mut events = Vec::new(); + #[allow(deprecated)] + for window in application.windows().iter() { + if window.is_kind_of::() { + // SAFETY: We just checked that the window is a `winit` window + let window = unsafe { + let ptr: *const UIWindow = window; + let ptr: *const WinitUIWindow = ptr.cast(); + &*ptr + }; + events.push(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Destroyed, + })); + } + } + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_events(mtm, events); + app_state::terminated(mtm); + } + + #[method(applicationDidReceiveMemoryWarning:)] + fn did_receive_memory_warning(&self, _application: &UIApplication) { + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::MemoryWarning)) + } + } +); + +impl AppDelegate { + fn send_occluded_event_for_all_windows( + &self, + application: &UIApplication, + occluded: bool, + ) { + let mut events = Vec::new(); + #[allow(deprecated)] + for window in application.windows().iter() { + if window.is_kind_of::() { + // SAFETY: We just checked that the window is a `winit` window + let window = unsafe { + let ptr: *const UIWindow = window; + let ptr: *const WinitUIWindow = ptr.cast(); + &*ptr + }; + events.push(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Occluded(occluded), + })); + } + } + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_events(mtm, events); + } +} diff --git a/rio-window/src/platform_impl/ios/app_state.rs b/rio-window/src/platform_impl/ios/app_state.rs new file mode 100644 index 00000000..fb56344f --- /dev/null +++ b/rio-window/src/platform_impl/ios/app_state.rs @@ -0,0 +1,994 @@ +#![deny(unused_results)] + +use std::cell::{RefCell, RefMut}; +use std::collections::HashSet; +use std::os::raw::c_void; +use std::sync::{Arc, Mutex, OnceLock}; +use std::time::Instant; +use std::{fmt, mem, ptr}; + +use core_foundation::base::CFRelease; +use core_foundation::date::CFAbsoluteTimeGetCurrent; +use core_foundation::runloop::{ + kCFRunLoopCommonModes, CFRunLoopAddTimer, CFRunLoopGetMain, CFRunLoopRef, + CFRunLoopTimerCreate, CFRunLoopTimerInvalidate, CFRunLoopTimerRef, + CFRunLoopTimerSetNextFireDate, +}; +use objc2::rc::Retained; +use objc2::runtime::AnyObject; +use objc2::{msg_send, sel}; +use objc2_foundation::{ + CGRect, CGSize, MainThreadMarker, NSInteger, NSObjectProtocol, + NSOperatingSystemVersion, NSProcessInfo, +}; +use objc2_ui_kit::{UICoordinateSpace, UIView}; + +use super::window::WinitUIWindow; +use crate::dpi::PhysicalSize; +use crate::event::{Event, InnerSizeWriter, StartCause, WindowEvent}; +use crate::event_loop::{ActiveEventLoop as RootActiveEventLoop, ControlFlow}; +use crate::window::WindowId as RootWindowId; + +macro_rules! bug { + ($($msg:tt)*) => { + panic!("winit iOS bug, file an issue: {}", format!($($msg)*)) + }; +} + +macro_rules! bug_assert { + ($test:expr, $($msg:tt)*) => { + assert!($test, "winit iOS bug, file an issue: {}", format!($($msg)*)) + }; +} + +#[derive(Debug)] +pub(crate) struct HandlePendingUserEvents; + +pub(crate) struct EventLoopHandler { + #[allow(clippy::type_complexity)] + pub(crate) handler: + Box, &RootActiveEventLoop)>, + pub(crate) event_loop: RootActiveEventLoop, +} + +impl fmt::Debug for EventLoopHandler { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("EventLoopHandler") + .field("handler", &"...") + .field("event_loop", &self.event_loop) + .finish() + } +} + +impl EventLoopHandler { + fn handle_event(&mut self, event: Event) { + (self.handler)(event, &self.event_loop) + } +} + +#[derive(Debug)] +pub(crate) enum EventWrapper { + StaticEvent(Event), + ScaleFactorChanged(ScaleFactorChanged), +} + +#[derive(Debug)] +pub struct ScaleFactorChanged { + pub(super) window: Retained, + pub(super) suggested_size: PhysicalSize, + pub(super) scale_factor: f64, +} + +enum UserCallbackTransitionResult<'a> { + Success { + handler: EventLoopHandler, + active_control_flow: ControlFlow, + processing_redraws: bool, + }, + ReentrancyPrevented { + queued_events: &'a mut Vec, + }, +} + +impl Event { + fn is_redraw(&self) -> bool { + matches!( + self, + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } + ) + } +} + +// this is the state machine for the app lifecycle +#[derive(Debug)] +#[must_use = "dropping `AppStateImpl` without inspecting it is probably a bug"] +enum AppStateImpl { + NotLaunched { + queued_windows: Vec>, + queued_events: Vec, + queued_gpu_redraws: HashSet>, + }, + Launching { + queued_windows: Vec>, + queued_events: Vec, + queued_handler: EventLoopHandler, + queued_gpu_redraws: HashSet>, + }, + ProcessingEvents { + handler: EventLoopHandler, + queued_gpu_redraws: HashSet>, + active_control_flow: ControlFlow, + }, + // special state to deal with reentrancy and prevent mutable aliasing. + InUserCallback { + queued_events: Vec, + queued_gpu_redraws: HashSet>, + }, + ProcessingRedraws { + handler: EventLoopHandler, + active_control_flow: ControlFlow, + }, + Waiting { + waiting_handler: EventLoopHandler, + start: Instant, + }, + PollFinished { + waiting_handler: EventLoopHandler, + }, + Terminated, +} + +pub(crate) struct AppState { + // This should never be `None`, except for briefly during a state transition. + app_state: Option, + control_flow: ControlFlow, + waker: EventLoopWaker, +} + +impl AppState { + pub(crate) fn get_mut(_mtm: MainThreadMarker) -> RefMut<'static, AppState> { + // basically everything in UIKit requires the main thread, so it's pointless to use the + // std::sync APIs. + // must be mut because plain `static` requires `Sync` + static mut APP_STATE: RefCell> = RefCell::new(None); + + let mut guard = unsafe { APP_STATE.borrow_mut() }; + if guard.is_none() { + #[inline(never)] + #[cold] + fn init_guard(guard: &mut RefMut<'static, Option>) { + let waker = EventLoopWaker::new(unsafe { CFRunLoopGetMain() }); + **guard = Some(AppState { + app_state: Some(AppStateImpl::NotLaunched { + queued_windows: Vec::new(), + queued_events: Vec::new(), + queued_gpu_redraws: HashSet::new(), + }), + control_flow: ControlFlow::default(), + waker, + }); + } + init_guard(&mut guard); + } + RefMut::map(guard, |state| state.as_mut().unwrap()) + } + + fn state(&self) -> &AppStateImpl { + match &self.app_state { + Some(ref state) => state, + None => bug!("`AppState` previously failed a state transition"), + } + } + + fn state_mut(&mut self) -> &mut AppStateImpl { + match &mut self.app_state { + Some(ref mut state) => state, + None => bug!("`AppState` previously failed a state transition"), + } + } + + fn take_state(&mut self) -> AppStateImpl { + match self.app_state.take() { + Some(state) => state, + None => bug!("`AppState` previously failed a state transition"), + } + } + + fn set_state(&mut self, new_state: AppStateImpl) { + bug_assert!( + self.app_state.is_none(), + "attempted to set an `AppState` without calling `take_state` first {:?}", + self.app_state + ); + self.app_state = Some(new_state) + } + + fn replace_state(&mut self, new_state: AppStateImpl) -> AppStateImpl { + match &mut self.app_state { + Some(ref mut state) => mem::replace(state, new_state), + None => bug!("`AppState` previously failed a state transition"), + } + } + + fn has_launched(&self) -> bool { + !matches!( + self.state(), + AppStateImpl::NotLaunched { .. } | AppStateImpl::Launching { .. } + ) + } + + fn has_terminated(&self) -> bool { + matches!(self.state(), AppStateImpl::Terminated) + } + + fn will_launch_transition(&mut self, queued_handler: EventLoopHandler) { + let (queued_windows, queued_events, queued_gpu_redraws) = match self.take_state() + { + AppStateImpl::NotLaunched { + queued_windows, + queued_events, + queued_gpu_redraws, + } => (queued_windows, queued_events, queued_gpu_redraws), + s => bug!("unexpected state {:?}", s), + }; + self.set_state(AppStateImpl::Launching { + queued_windows, + queued_events, + queued_handler, + queued_gpu_redraws, + }); + } + + fn did_finish_launching_transition( + &mut self, + ) -> (Vec>, Vec) { + let (windows, events, handler, queued_gpu_redraws) = match self.take_state() { + AppStateImpl::Launching { + queued_windows, + queued_events, + queued_handler, + queued_gpu_redraws, + } => ( + queued_windows, + queued_events, + queued_handler, + queued_gpu_redraws, + ), + s => bug!("unexpected state {:?}", s), + }; + self.set_state(AppStateImpl::ProcessingEvents { + handler, + active_control_flow: self.control_flow, + queued_gpu_redraws, + }); + (windows, events) + } + + fn wakeup_transition(&mut self) -> Option { + // before `AppState::did_finish_launching` is called, pretend there is no running + // event loop. + if !self.has_launched() || self.has_terminated() { + return None; + } + + let (handler, event) = match (self.control_flow, self.take_state()) { + (ControlFlow::Poll, AppStateImpl::PollFinished { waiting_handler }) => ( + waiting_handler, + EventWrapper::StaticEvent(Event::NewEvents(StartCause::Poll)), + ), + ( + ControlFlow::Wait, + AppStateImpl::Waiting { + waiting_handler, + start, + }, + ) => ( + waiting_handler, + EventWrapper::StaticEvent(Event::NewEvents(StartCause::WaitCancelled { + start, + requested_resume: None, + })), + ), + ( + ControlFlow::WaitUntil(requested_resume), + AppStateImpl::Waiting { + waiting_handler, + start, + }, + ) => { + let event = if Instant::now() >= requested_resume { + EventWrapper::StaticEvent(Event::NewEvents( + StartCause::ResumeTimeReached { + start, + requested_resume, + }, + )) + } else { + EventWrapper::StaticEvent(Event::NewEvents( + StartCause::WaitCancelled { + start, + requested_resume: Some(requested_resume), + }, + )) + }; + (waiting_handler, event) + } + s => bug!("`EventHandler` unexpectedly woke up {:?}", s), + }; + + self.set_state(AppStateImpl::ProcessingEvents { + handler, + queued_gpu_redraws: Default::default(), + active_control_flow: self.control_flow, + }); + Some(event) + } + + fn try_user_callback_transition(&mut self) -> UserCallbackTransitionResult<'_> { + // If we're not able to process an event due to recursion or `Init` not having been sent out + // yet, then queue the events up. + match self.state_mut() { + &mut AppStateImpl::Launching { + ref mut queued_events, + .. + } + | &mut AppStateImpl::NotLaunched { + ref mut queued_events, + .. + } + | &mut AppStateImpl::InUserCallback { + ref mut queued_events, + .. + } => { + // A lifetime cast: early returns are not currently handled well with NLL, but + // polonius handles them well. This transmute is a safe workaround. + return unsafe { + mem::transmute::< + UserCallbackTransitionResult<'_>, + UserCallbackTransitionResult<'_>, + >( + UserCallbackTransitionResult::ReentrancyPrevented { + queued_events, + }, + ) + }; + } + + &mut AppStateImpl::ProcessingEvents { .. } + | &mut AppStateImpl::ProcessingRedraws { .. } => {} + + s @ &mut AppStateImpl::PollFinished { .. } + | s @ &mut AppStateImpl::Waiting { .. } + | s @ &mut AppStateImpl::Terminated => { + bug!("unexpected attempted to process an event {:?}", s) + } + } + + let (handler, queued_gpu_redraws, active_control_flow, processing_redraws) = + match self.take_state() { + AppStateImpl::Launching { .. } + | AppStateImpl::NotLaunched { .. } + | AppStateImpl::InUserCallback { .. } => unreachable!(), + AppStateImpl::ProcessingEvents { + handler, + queued_gpu_redraws, + active_control_flow, + } => (handler, queued_gpu_redraws, active_control_flow, false), + AppStateImpl::ProcessingRedraws { + handler, + active_control_flow, + } => (handler, Default::default(), active_control_flow, true), + AppStateImpl::PollFinished { .. } + | AppStateImpl::Waiting { .. } + | AppStateImpl::Terminated => unreachable!(), + }; + self.set_state(AppStateImpl::InUserCallback { + queued_events: Vec::new(), + queued_gpu_redraws, + }); + UserCallbackTransitionResult::Success { + handler, + active_control_flow, + processing_redraws, + } + } + + fn main_events_cleared_transition(&mut self) -> HashSet> { + let (handler, queued_gpu_redraws, active_control_flow) = match self.take_state() { + AppStateImpl::ProcessingEvents { + handler, + queued_gpu_redraws, + active_control_flow, + } => (handler, queued_gpu_redraws, active_control_flow), + s => bug!("unexpected state {:?}", s), + }; + self.set_state(AppStateImpl::ProcessingRedraws { + handler, + active_control_flow, + }); + queued_gpu_redraws + } + + fn events_cleared_transition(&mut self) { + if !self.has_launched() || self.has_terminated() { + return; + } + let (waiting_handler, old) = match self.take_state() { + AppStateImpl::ProcessingRedraws { + handler, + active_control_flow, + } => (handler, active_control_flow), + s => bug!("unexpected state {:?}", s), + }; + + let new = self.control_flow; + match (old, new) { + (ControlFlow::Wait, ControlFlow::Wait) => { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { + waiting_handler, + start, + }); + } + ( + ControlFlow::WaitUntil(old_instant), + ControlFlow::WaitUntil(new_instant), + ) if old_instant == new_instant => { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { + waiting_handler, + start, + }); + } + (_, ControlFlow::Wait) => { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { + waiting_handler, + start, + }); + self.waker.stop() + } + (_, ControlFlow::WaitUntil(new_instant)) => { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { + waiting_handler, + start, + }); + self.waker.start_at(new_instant) + } + // Unlike on macOS, handle Poll to Poll transition here to call the waker + (_, ControlFlow::Poll) => { + self.set_state(AppStateImpl::PollFinished { waiting_handler }); + self.waker.start() + } + } + } + + fn terminated_transition(&mut self) -> EventLoopHandler { + match self.replace_state(AppStateImpl::Terminated) { + AppStateImpl::ProcessingEvents { handler, .. } => handler, + s => bug!("`LoopExiting` happened while not processing events {:?}", s), + } + } + + pub(crate) fn set_control_flow(&mut self, control_flow: ControlFlow) { + self.control_flow = control_flow; + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow + } +} + +pub(crate) fn set_key_window(mtm: MainThreadMarker, window: &Retained) { + let mut this = AppState::get_mut(mtm); + match this.state_mut() { + &mut AppStateImpl::NotLaunched { + ref mut queued_windows, + .. + } => return queued_windows.push(window.clone()), + &mut AppStateImpl::ProcessingEvents { .. } + | &mut AppStateImpl::InUserCallback { .. } + | &mut AppStateImpl::ProcessingRedraws { .. } => {} + s @ &mut AppStateImpl::Launching { .. } + | s @ &mut AppStateImpl::Waiting { .. } + | s @ &mut AppStateImpl::PollFinished { .. } => bug!("unexpected state {:?}", s), + &mut AppStateImpl::Terminated => { + panic!("Attempt to create a `Window` after the app has terminated") + } + } + drop(this); + window.makeKeyAndVisible(); +} + +pub(crate) fn queue_gl_or_metal_redraw( + mtm: MainThreadMarker, + window: Retained, +) { + let mut this = AppState::get_mut(mtm); + match this.state_mut() { + &mut AppStateImpl::NotLaunched { + ref mut queued_gpu_redraws, + .. + } + | &mut AppStateImpl::Launching { + ref mut queued_gpu_redraws, + .. + } + | &mut AppStateImpl::ProcessingEvents { + ref mut queued_gpu_redraws, + .. + } + | &mut AppStateImpl::InUserCallback { + ref mut queued_gpu_redraws, + .. + } => { + let _ = queued_gpu_redraws.insert(window); + } + s @ &mut AppStateImpl::ProcessingRedraws { .. } + | s @ &mut AppStateImpl::Waiting { .. } + | s @ &mut AppStateImpl::PollFinished { .. } => bug!("unexpected state {:?}", s), + &mut AppStateImpl::Terminated => { + panic!("Attempt to create a `Window` after the app has terminated") + } + } +} + +pub(crate) fn will_launch(mtm: MainThreadMarker, queued_handler: EventLoopHandler) { + AppState::get_mut(mtm).will_launch_transition(queued_handler) +} + +pub fn did_finish_launching(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); + let windows = match this.state_mut() { + AppStateImpl::Launching { queued_windows, .. } => mem::take(queued_windows), + s => bug!("unexpected state {:?}", s), + }; + + this.waker.start(); + + // have to drop RefMut because the window setup code below can trigger new events + drop(this); + + for window in windows { + // Do a little screen dance here to account for windows being created before + // `UIApplicationMain` is called. This fixes visual issues such as being + // offcenter and sized incorrectly. Additionally, to fix orientation issues, we + // gotta reset the `rootViewController`. + // + // relevant iOS log: + // ``` + // [ApplicationLifecycle] Windows were created before application initialization + // completed. This may result in incorrect visual appearance. + // ``` + let screen = window.screen(); + let _: () = unsafe { msg_send![&window, setScreen: ptr::null::()] }; + window.setScreen(&screen); + + let controller = window.rootViewController(); + window.setRootViewController(None); + window.setRootViewController(controller.as_deref()); + + window.makeKeyAndVisible(); + } + + let (windows, events) = AppState::get_mut(mtm).did_finish_launching_transition(); + + let events = std::iter::once(EventWrapper::StaticEvent(Event::NewEvents( + StartCause::Init, + ))) + .chain(events); + handle_nonuser_events(mtm, events); + + // the above window dance hack, could possibly trigger new windows to be created. + // we can just set those windows up normally, as they were created after didFinishLaunching + for window in windows { + window.makeKeyAndVisible(); + } +} + +// AppState::did_finish_launching handles the special transition `Init` +pub fn handle_wakeup_transition(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); + let wakeup_event = match this.wakeup_transition() { + None => return, + Some(wakeup_event) => wakeup_event, + }; + drop(this); + + handle_nonuser_event(mtm, wakeup_event) +} + +pub(crate) fn handle_nonuser_event(mtm: MainThreadMarker, event: EventWrapper) { + handle_nonuser_events(mtm, std::iter::once(event)) +} + +pub(crate) fn handle_nonuser_events>( + mtm: MainThreadMarker, + events: I, +) { + let mut this = AppState::get_mut(mtm); + if this.has_terminated() { + return; + } + + let (mut handler, active_control_flow, processing_redraws) = + match this.try_user_callback_transition() { + UserCallbackTransitionResult::ReentrancyPrevented { queued_events } => { + queued_events.extend(events); + return; + } + UserCallbackTransitionResult::Success { + handler, + active_control_flow, + processing_redraws, + } => (handler, active_control_flow, processing_redraws), + }; + drop(this); + + for wrapper in events { + match wrapper { + EventWrapper::StaticEvent(event) => { + if !processing_redraws && event.is_redraw() { + tracing::info!( + "processing `RedrawRequested` during the main event loop" + ); + } else if processing_redraws && !event.is_redraw() { + tracing::warn!( + "processing non `RedrawRequested` event after the main event loop: {:#?}", + event + ); + } + handler.handle_event(event) + } + EventWrapper::ScaleFactorChanged(event) => { + handle_hidpi_proxy(&mut handler, event) + } + } + } + + loop { + let mut this = AppState::get_mut(mtm); + let queued_events = match this.state_mut() { + &mut AppStateImpl::InUserCallback { + ref mut queued_events, + queued_gpu_redraws: _, + } => mem::take(queued_events), + s => bug!("unexpected state {:?}", s), + }; + if queued_events.is_empty() { + let queued_gpu_redraws = match this.take_state() { + AppStateImpl::InUserCallback { + queued_events: _, + queued_gpu_redraws, + } => queued_gpu_redraws, + _ => unreachable!(), + }; + this.app_state = Some(if processing_redraws { + bug_assert!( + queued_gpu_redraws.is_empty(), + "redraw queued while processing redraws" + ); + AppStateImpl::ProcessingRedraws { + handler, + active_control_flow, + } + } else { + AppStateImpl::ProcessingEvents { + handler, + queued_gpu_redraws, + active_control_flow, + } + }); + break; + } + drop(this); + + for wrapper in queued_events { + match wrapper { + EventWrapper::StaticEvent(event) => { + if !processing_redraws && event.is_redraw() { + tracing::info!( + "processing `RedrawRequested` during the main event loop" + ); + } else if processing_redraws && !event.is_redraw() { + tracing::warn!( + "processing non-`RedrawRequested` event after the main event loop: \ + {:#?}", + event + ); + } + handler.handle_event(event) + } + EventWrapper::ScaleFactorChanged(event) => { + handle_hidpi_proxy(&mut handler, event) + } + } + } + } +} + +fn handle_user_events(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); + let (mut handler, active_control_flow, processing_redraws) = + match this.try_user_callback_transition() { + UserCallbackTransitionResult::ReentrancyPrevented { .. } => { + bug!("unexpected attempted to process an event") + } + UserCallbackTransitionResult::Success { + handler, + active_control_flow, + processing_redraws, + } => (handler, active_control_flow, processing_redraws), + }; + if processing_redraws { + bug!("user events attempted to be sent out while `ProcessingRedraws`"); + } + drop(this); + + handler.handle_event(Event::UserEvent(HandlePendingUserEvents)); + + loop { + let mut this = AppState::get_mut(mtm); + let queued_events = match this.state_mut() { + &mut AppStateImpl::InUserCallback { + ref mut queued_events, + queued_gpu_redraws: _, + } => mem::take(queued_events), + s => bug!("unexpected state {:?}", s), + }; + if queued_events.is_empty() { + let queued_gpu_redraws = match this.take_state() { + AppStateImpl::InUserCallback { + queued_events: _, + queued_gpu_redraws, + } => queued_gpu_redraws, + _ => unreachable!(), + }; + this.app_state = Some(AppStateImpl::ProcessingEvents { + handler, + queued_gpu_redraws, + active_control_flow, + }); + break; + } + drop(this); + + for wrapper in queued_events { + match wrapper { + EventWrapper::StaticEvent(event) => handler.handle_event(event), + EventWrapper::ScaleFactorChanged(event) => { + handle_hidpi_proxy(&mut handler, event) + } + } + } + + handler.handle_event(Event::UserEvent(HandlePendingUserEvents)); + } +} + +pub fn handle_main_events_cleared(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); + if !this.has_launched() || this.has_terminated() { + return; + } + match this.state_mut() { + AppStateImpl::ProcessingEvents { .. } => {} + _ => bug!("`ProcessingRedraws` happened unexpectedly"), + }; + drop(this); + + handle_user_events(mtm); + + let mut this = AppState::get_mut(mtm); + let redraw_events: Vec = this + .main_events_cleared_transition() + .into_iter() + .map(|window| { + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::RedrawRequested, + }) + }) + .collect(); + drop(this); + + handle_nonuser_events(mtm, redraw_events); + handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::AboutToWait)); +} + +pub fn handle_events_cleared(mtm: MainThreadMarker) { + AppState::get_mut(mtm).events_cleared_transition(); +} + +pub fn terminated(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); + let mut handler = this.terminated_transition(); + drop(this); + + handler.handle_event(Event::LoopExiting) +} + +fn handle_hidpi_proxy(handler: &mut EventLoopHandler, event: ScaleFactorChanged) { + let ScaleFactorChanged { + suggested_size, + scale_factor, + window, + } = event; + let new_inner_size = Arc::new(Mutex::new(suggested_size)); + let event = Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_inner_size)), + }, + }; + handler.handle_event(event); + let (view, screen_frame) = get_view_and_screen_frame(&window); + let physical_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); + let logical_size = physical_size.to_logical(scale_factor); + let size = CGSize::new(logical_size.width, logical_size.height); + let new_frame: CGRect = CGRect::new(screen_frame.origin, size); + view.setFrame(new_frame); +} + +fn get_view_and_screen_frame(window: &WinitUIWindow) -> (Retained, CGRect) { + let view_controller = window.rootViewController().unwrap(); + let view = view_controller.view().unwrap(); + let bounds = window.bounds(); + let screen = window.screen(); + let screen_space = screen.coordinateSpace(); + let screen_frame = window.convertRect_toCoordinateSpace(bounds, &screen_space); + (view, screen_frame) +} + +struct EventLoopWaker { + timer: CFRunLoopTimerRef, +} + +impl Drop for EventLoopWaker { + fn drop(&mut self) { + unsafe { + CFRunLoopTimerInvalidate(self.timer); + CFRelease(self.timer as _); + } + } +} + +impl EventLoopWaker { + fn new(rl: CFRunLoopRef) -> EventLoopWaker { + extern "C" fn wakeup_main_loop(_timer: CFRunLoopTimerRef, _info: *mut c_void) {} + unsafe { + // Create a timer with a 0.1µs interval (1ns does not work) to mimic polling. + // It is initially setup with a first fire time really far into the + // future, but that gets changed to fire immediately in did_finish_launching + let timer = CFRunLoopTimerCreate( + ptr::null_mut(), + std::f64::MAX, + 0.000_000_1, + 0, + 0, + wakeup_main_loop, + ptr::null_mut(), + ); + CFRunLoopAddTimer(rl, timer, kCFRunLoopCommonModes); + + EventLoopWaker { timer } + } + } + + fn stop(&mut self) { + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MAX) } + } + + fn start(&mut self) { + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MIN) } + } + + fn start_at(&mut self, instant: Instant) { + let now = Instant::now(); + if now >= instant { + self.start(); + } else { + unsafe { + let current = CFAbsoluteTimeGetCurrent(); + let duration = instant - now; + let fsecs = duration.subsec_nanos() as f64 / 1_000_000_000.0 + + duration.as_secs() as f64; + CFRunLoopTimerSetNextFireDate(self.timer, current + fsecs) + } + } + } +} + +macro_rules! os_capabilities { + ( + $( + $(#[$attr:meta])* + $error_name:ident: $objc_call:literal, + $name:ident: $major:literal-$minor:literal + ),* + $(,)* + ) => { + #[derive(Clone, Debug)] + pub struct OSCapabilities { + $( + pub $name: bool, + )* + + os_version: NSOperatingSystemVersion, + } + + impl OSCapabilities { + fn from_os_version(os_version: NSOperatingSystemVersion) -> Self { + $(let $name = meets_requirements(os_version, $major, $minor);)* + Self { $($name,)* os_version, } + } + } + + impl OSCapabilities {$( + $(#[$attr])* + pub fn $error_name(&self, extra_msg: &str) { + tracing::warn!( + concat!("`", $objc_call, "` requires iOS {}.{}+. This device is running iOS {}.{}.{}. {}"), + $major, $minor, self.os_version.majorVersion, self.os_version.minorVersion, self.os_version.patchVersion, + extra_msg + ) + } + )*} + }; +} + +os_capabilities! { + /// + #[allow(unused)] // error message unused + safe_area_err_msg: "-[UIView safeAreaInsets]", + safe_area: 11-0, + /// + home_indicator_hidden_err_msg: "-[UIViewController setNeedsUpdateOfHomeIndicatorAutoHidden]", + home_indicator_hidden: 11-0, + /// + defer_system_gestures_err_msg: "-[UIViewController setNeedsUpdateOfScreenEdgesDeferringSystem]", + defer_system_gestures: 11-0, + /// + maximum_frames_per_second_err_msg: "-[UIScreen maximumFramesPerSecond]", + maximum_frames_per_second: 10-3, + /// + #[allow(unused)] // error message unused + force_touch_err_msg: "-[UITouch force]", + force_touch: 9-0, +} + +fn meets_requirements( + version: NSOperatingSystemVersion, + required_major: NSInteger, + required_minor: NSInteger, +) -> bool { + (version.majorVersion, version.minorVersion) >= (required_major, required_minor) +} + +fn get_version() -> NSOperatingSystemVersion { + let process_info = NSProcessInfo::processInfo(); + let atleast_ios_8 = process_info.respondsToSelector(sel!(operatingSystemVersion)); + // Winit requires atleast iOS 8 because no one has put the time into supporting earlier os + // versions. Older iOS versions are increasingly difficult to test. For example, Xcode 11 does + // not support debugging on devices with an iOS version of less than 8. Another example, in + // order to use an iOS simulator older than iOS 8, you must download an older version of Xcode + // (<9), and at least Xcode 7 has been tested to not even run on macOS 10.15 - Xcode 8 might? + // + // The minimum required iOS version is likely to grow in the future. + assert!(atleast_ios_8, "`winit` requires iOS version 8 or greater"); + process_info.operatingSystemVersion() +} + +pub fn os_capabilities() -> OSCapabilities { + // Cache the version lookup for efficiency + static OS_CAPABILITIES: OnceLock = OnceLock::new(); + OS_CAPABILITIES + .get_or_init(|| OSCapabilities::from_os_version(get_version())) + .clone() +} diff --git a/rio-window/src/platform_impl/ios/event_loop.rs b/rio-window/src/platform_impl/ios/event_loop.rs new file mode 100644 index 00000000..91284ff8 --- /dev/null +++ b/rio-window/src/platform_impl/ios/event_loop.rs @@ -0,0 +1,395 @@ +use std::collections::VecDeque; +use std::ffi::{c_char, c_int, c_void}; +use std::marker::PhantomData; +use std::ptr::{self, NonNull}; +use std::sync::mpsc::{self, Receiver, Sender}; + +use core_foundation::base::{CFIndex, CFRelease}; +use core_foundation::runloop::{ + kCFRunLoopAfterWaiting, kCFRunLoopBeforeWaiting, kCFRunLoopCommonModes, + kCFRunLoopDefaultMode, kCFRunLoopExit, CFRunLoopActivity, CFRunLoopAddObserver, + CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopObserverCreate, CFRunLoopObserverRef, + CFRunLoopSourceContext, CFRunLoopSourceCreate, CFRunLoopSourceInvalidate, + CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp, +}; +use objc2::rc::Retained; +use objc2::{msg_send_id, ClassType}; +use objc2_foundation::{MainThreadMarker, NSString}; +use objc2_ui_kit::{ + UIApplication, UIApplicationMain, UIDevice, UIScreen, UIUserInterfaceIdiom, +}; + +use crate::error::EventLoopError; +use crate::event::Event; +use crate::event_loop::{ + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, EventLoopClosed, +}; +use crate::platform::ios::Idiom; +use crate::platform_impl::platform::app_state::{ + EventLoopHandler, HandlePendingUserEvents, +}; +use crate::window::{CustomCursor, CustomCursorSource}; + +use super::app_delegate::AppDelegate; +use super::app_state::AppState; +use super::{app_state, monitor, MonitorHandle}; + +#[derive(Debug)] +pub struct ActiveEventLoop { + pub(super) mtm: MainThreadMarker, +} + +impl ActiveEventLoop { + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> CustomCursor { + let _ = source.inner; + CustomCursor { + inner: super::PlatformCustomCursor, + } + } + + pub fn available_monitors(&self) -> VecDeque { + monitor::uiscreens(self.mtm) + } + + pub fn primary_monitor(&self) -> Option { + #[allow(deprecated)] + Some(MonitorHandle::new(UIScreen::mainScreen(self.mtm))) + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::UiKit( + rwh_06::UiKitDisplayHandle::new(), + )) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + AppState::get_mut(self.mtm).set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + AppState::get_mut(self.mtm).control_flow() + } + + pub(crate) fn exit(&self) { + // https://developer.apple.com/library/archive/qa/qa1561/_index.html + // it is not possible to quit an iOS app gracefully and programmatically + tracing::warn!("`ControlFlow::Exit` ignored on iOS"); + } + + pub(crate) fn exiting(&self) -> bool { + false + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::UiKitDisplayHandle::new().into()) + } +} + +fn map_user_event( + mut handler: impl FnMut(Event, &RootActiveEventLoop), + receiver: mpsc::Receiver, +) -> impl FnMut(Event, &RootActiveEventLoop) { + move |event, window_target| match event.map_nonuser_event() { + Ok(event) => (handler)(event, window_target), + Err(_) => { + for event in receiver.try_iter() { + (handler)(Event::UserEvent(event), window_target); + } + } + } +} + +pub struct EventLoop { + mtm: MainThreadMarker, + sender: Sender, + receiver: Receiver, + window_target: RootActiveEventLoop, +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes {} + +impl EventLoop { + pub(crate) fn new( + _: &PlatformSpecificEventLoopAttributes, + ) -> Result, EventLoopError> { + let mtm = MainThreadMarker::new() + .expect("On iOS, `EventLoop` must be created on the main thread"); + + static mut SINGLETON_INIT: bool = false; + unsafe { + assert!( + !SINGLETON_INIT, + "Only one `EventLoop` is supported on iOS. `EventLoopProxy` might be helpful" + ); + SINGLETON_INIT = true; + } + + let (sender, receiver) = mpsc::channel(); + + // this line sets up the main run loop before `UIApplicationMain` + setup_control_flow_observers(); + + Ok(EventLoop { + mtm, + sender, + receiver, + window_target: RootActiveEventLoop { + p: ActiveEventLoop { mtm }, + _marker: PhantomData, + }, + }) + } + + pub fn run(self, handler: F) -> ! + where + F: FnMut(Event, &RootActiveEventLoop), + { + let application: Option> = + unsafe { msg_send_id![UIApplication::class(), sharedApplication] }; + assert!( + application.is_none(), + "\ + `EventLoop` cannot be `run` after a call to `UIApplicationMain` on iOS\nNote: \ + `EventLoop::run_app` calls `UIApplicationMain` on iOS", + ); + + let handler = map_user_event(handler, self.receiver); + + let handler = unsafe { + std::mem::transmute::< + Box, &RootActiveEventLoop)>, + Box, &RootActiveEventLoop)>, + >(Box::new(handler)) + }; + + let handler = EventLoopHandler { + handler, + event_loop: self.window_target, + }; + + app_state::will_launch(self.mtm, handler); + + // Ensure application delegate is initialized + let _ = AppDelegate::class(); + + extern "C" { + // These functions are in crt_externs.h. + fn _NSGetArgc() -> *mut c_int; + fn _NSGetArgv() -> *mut *mut *mut c_char; + } + + unsafe { + UIApplicationMain( + *_NSGetArgc(), + NonNull::new(*_NSGetArgv()).unwrap(), + None, + Some(&NSString::from_str(AppDelegate::NAME)), + ) + }; + unreachable!() + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.sender.clone()) + } + + pub fn window_target(&self) -> &RootActiveEventLoop { + &self.window_target + } +} + +// EventLoopExtIOS +impl EventLoop { + pub fn idiom(&self) -> Idiom { + match UIDevice::currentDevice(self.mtm).userInterfaceIdiom() { + UIUserInterfaceIdiom::Unspecified => Idiom::Unspecified, + UIUserInterfaceIdiom::Phone => Idiom::Phone, + UIUserInterfaceIdiom::Pad => Idiom::Pad, + UIUserInterfaceIdiom::TV => Idiom::TV, + UIUserInterfaceIdiom::CarPlay => Idiom::CarPlay, + _ => Idiom::Unspecified, + } + } +} + +pub struct EventLoopProxy { + sender: Sender, + source: CFRunLoopSourceRef, +} + +unsafe impl Send for EventLoopProxy {} +unsafe impl Sync for EventLoopProxy {} + +impl Clone for EventLoopProxy { + fn clone(&self) -> EventLoopProxy { + EventLoopProxy::new(self.sender.clone()) + } +} + +impl Drop for EventLoopProxy { + fn drop(&mut self) { + unsafe { + CFRunLoopSourceInvalidate(self.source); + CFRelease(self.source as _); + } + } +} + +impl EventLoopProxy { + fn new(sender: Sender) -> EventLoopProxy { + unsafe { + // just wake up the eventloop + extern "C" fn event_loop_proxy_handler(_: *const c_void) {} + + // adding a Source to the main CFRunLoop lets us wake it up and + // process user events through the normal OS EventLoop mechanisms. + let rl = CFRunLoopGetMain(); + let mut context = CFRunLoopSourceContext { + version: 0, + info: ptr::null_mut(), + retain: None, + release: None, + copyDescription: None, + equal: None, + hash: None, + schedule: None, + cancel: None, + perform: event_loop_proxy_handler, + }; + let source = CFRunLoopSourceCreate( + ptr::null_mut(), + CFIndex::max_value() - 1, + &mut context, + ); + CFRunLoopAddSource(rl, source, kCFRunLoopCommonModes); + CFRunLoopWakeUp(rl); + + EventLoopProxy { sender, source } + } + } + + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.sender + .send(event) + .map_err(|::std::sync::mpsc::SendError(x)| EventLoopClosed(x))?; + unsafe { + // let the main thread know there's a new event + CFRunLoopSourceSignal(self.source); + let rl = CFRunLoopGetMain(); + CFRunLoopWakeUp(rl); + } + Ok(()) + } +} + +fn setup_control_flow_observers() { + unsafe { + // begin is queued with the highest priority to ensure it is processed before other + // observers + extern "C" fn control_flow_begin_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + _: *mut c_void, + ) { + let mtm = MainThreadMarker::new().unwrap(); + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopAfterWaiting => app_state::handle_wakeup_transition(mtm), + _ => unreachable!(), + } + } + + // Core Animation registers its `CFRunLoopObserver` that performs drawing operations in + // `CA::Transaction::ensure_implicit` with a priority of `0x1e8480`. We set the main_end + // priority to be 0, in order to send AboutToWait before RedrawRequested. This value was + // chosen conservatively to guard against apple using different priorities for their redraw + // observers in different OS's or on different devices. If it so happens that it's too + // conservative, the main symptom would be non-redraw events coming in after `AboutToWait`. + // + // The value of `0x1e8480` was determined by inspecting stack traces and the associated + // registers for every `CFRunLoopAddObserver` call on an iPad Air 2 running iOS 11.4. + // + // Also tested to be `0x1e8480` on iPhone 8, iOS 13 beta 4. + extern "C" fn control_flow_main_end_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + _: *mut c_void, + ) { + let mtm = MainThreadMarker::new().unwrap(); + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => app_state::handle_main_events_cleared(mtm), + kCFRunLoopExit => {} // may happen when running on macOS + _ => unreachable!(), + } + } + + // end is queued with the lowest priority to ensure it is processed after other observers + extern "C" fn control_flow_end_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + _: *mut c_void, + ) { + let mtm = MainThreadMarker::new().unwrap(); + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => app_state::handle_events_cleared(mtm), + kCFRunLoopExit => {} // may happen when running on macOS + _ => unreachable!(), + } + } + + let main_loop = CFRunLoopGetMain(); + + let begin_observer = CFRunLoopObserverCreate( + ptr::null_mut(), + kCFRunLoopAfterWaiting, + 1, // repeat = true + CFIndex::min_value(), + control_flow_begin_handler, + ptr::null_mut(), + ); + CFRunLoopAddObserver(main_loop, begin_observer, kCFRunLoopDefaultMode); + + let main_end_observer = CFRunLoopObserverCreate( + ptr::null_mut(), + kCFRunLoopExit | kCFRunLoopBeforeWaiting, + 1, // repeat = true + 0, // see comment on `control_flow_main_end_handler` + control_flow_main_end_handler, + ptr::null_mut(), + ); + CFRunLoopAddObserver(main_loop, main_end_observer, kCFRunLoopDefaultMode); + + let end_observer = CFRunLoopObserverCreate( + ptr::null_mut(), + kCFRunLoopExit | kCFRunLoopBeforeWaiting, + 1, // repeat = true + CFIndex::max_value(), + control_flow_end_handler, + ptr::null_mut(), + ); + CFRunLoopAddObserver(main_loop, end_observer, kCFRunLoopDefaultMode); + } +} diff --git a/rio-window/src/platform_impl/ios/mod.rs b/rio-window/src/platform_impl/ios/mod.rs new file mode 100644 index 00000000..bf2c011f --- /dev/null +++ b/rio-window/src/platform_impl/ios/mod.rs @@ -0,0 +1,53 @@ +#![cfg(ios_platform)] +#![allow(clippy::let_unit_value)] + +mod app_delegate; +mod app_state; +mod event_loop; +mod monitor; +mod view; +mod view_controller; +mod window; + +use std::fmt; + +use crate::event::DeviceId as RootDeviceId; + +pub(crate) use self::event_loop::{ + ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle, + PlatformSpecificEventLoopAttributes, +}; +pub(crate) use self::monitor::{MonitorHandle, VideoModeHandle}; +pub(crate) use self::window::{PlatformSpecificWindowAttributes, Window, WindowId}; +pub(crate) use crate::cursor::{ + NoCustomCursor as PlatformCustomCursor, NoCustomCursor as PlatformCustomCursorSource, +}; +pub(crate) use crate::icon::NoIcon as PlatformIcon; +pub(crate) use crate::platform_impl::Fullscreen; + +/// There is no way to detect which device that performed a certain event in +/// UIKit (i.e. you can't differentiate between different external keyboards, +/// or whether it was the main touchscreen, assistive technologies, or some +/// other pointer device that caused a touch event). +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId; + +impl DeviceId { + pub const unsafe fn dummy() -> Self { + DeviceId + } +} + +pub(crate) const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct KeyEventExtra {} + +#[derive(Debug)] +pub enum OsError {} + +impl fmt::Display for OsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "os error") + } +} diff --git a/rio-window/src/platform_impl/ios/monitor.rs b/rio-window/src/platform_impl/ios/monitor.rs new file mode 100644 index 00000000..6765b068 --- /dev/null +++ b/rio-window/src/platform_impl/ios/monitor.rs @@ -0,0 +1,262 @@ +#![allow(clippy::unnecessary_cast)] + +use std::collections::{BTreeSet, VecDeque}; +use std::{fmt, hash, ptr}; + +use objc2::mutability::IsRetainable; +use objc2::rc::Retained; +use objc2::Message; +use objc2_foundation::{run_on_main, MainThreadBound, MainThreadMarker, NSInteger}; +use objc2_ui_kit::{UIScreen, UIScreenMode}; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::monitor::VideoModeHandle as RootVideoModeHandle; +use crate::platform_impl::platform::app_state; + +// Workaround for `MainThreadBound` implementing almost no traits +#[derive(Debug)] +struct MainThreadBoundDelegateImpls(MainThreadBound>); + +impl Clone for MainThreadBoundDelegateImpls { + fn clone(&self) -> Self { + Self(run_on_main(|mtm| { + MainThreadBound::new(Retained::clone(self.0.get(mtm)), mtm) + })) + } +} + +impl hash::Hash for MainThreadBoundDelegateImpls { + fn hash(&self, state: &mut H) { + // SAFETY: Marker only used to get the pointer + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + Retained::as_ptr(self.0.get(mtm)).hash(state); + } +} + +impl PartialEq for MainThreadBoundDelegateImpls { + fn eq(&self, other: &Self) -> bool { + // SAFETY: Marker only used to get the pointer + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + Retained::as_ptr(self.0.get(mtm)) == Retained::as_ptr(other.0.get(mtm)) + } +} + +impl Eq for MainThreadBoundDelegateImpls {} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct VideoModeHandle { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate_millihertz: u32, + screen_mode: MainThreadBoundDelegateImpls, + pub(crate) monitor: MonitorHandle, +} + +impl VideoModeHandle { + fn new( + uiscreen: Retained, + screen_mode: Retained, + mtm: MainThreadMarker, + ) -> VideoModeHandle { + let refresh_rate_millihertz = refresh_rate_millihertz(&uiscreen); + let size = screen_mode.size(); + VideoModeHandle { + size: (size.width as u32, size.height as u32), + bit_depth: 32, + refresh_rate_millihertz, + screen_mode: MainThreadBoundDelegateImpls(MainThreadBound::new( + screen_mode, + mtm, + )), + monitor: MonitorHandle::new(uiscreen), + } + } + + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } + + pub(super) fn screen_mode(&self, mtm: MainThreadMarker) -> &Retained { + self.screen_mode.0.get(mtm) + } +} + +pub struct MonitorHandle { + ui_screen: MainThreadBound>, +} + +impl Clone for MonitorHandle { + fn clone(&self) -> Self { + run_on_main(|mtm| Self { + ui_screen: MainThreadBound::new(self.ui_screen.get(mtm).clone(), mtm), + }) + } +} + +impl hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + (self as *const Self).hash(state); + } +} + +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + ptr::eq(self, other) + } +} + +impl Eq for MonitorHandle {} + +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MonitorHandle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // TODO: Make a better ordering + (self as *const Self).cmp(&(other as *const Self)) + } +} + +impl fmt::Debug for MonitorHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MonitorHandle") + .field("name", &self.name()) + .field("size", &self.size()) + .field("position", &self.position()) + .field("scale_factor", &self.scale_factor()) + .field("refresh_rate_millihertz", &self.refresh_rate_millihertz()) + .finish_non_exhaustive() + } +} + +impl MonitorHandle { + pub(crate) fn new(ui_screen: Retained) -> Self { + // Holding `Retained` implies we're on the main thread. + let mtm = MainThreadMarker::new().unwrap(); + Self { + ui_screen: MainThreadBound::new(ui_screen, mtm), + } + } + + pub fn name(&self) -> Option { + run_on_main(|mtm| { + #[allow(deprecated)] + let main = UIScreen::mainScreen(mtm); + if *self.ui_screen(mtm) == main { + Some("Primary".to_string()) + } else if Some(self.ui_screen(mtm)) == main.mirroredScreen().as_ref() { + Some("Mirrored".to_string()) + } else { + #[allow(deprecated)] + UIScreen::screens(mtm) + .iter() + .position(|rhs| rhs == &**self.ui_screen(mtm)) + .map(|idx| idx.to_string()) + } + }) + } + + pub fn size(&self) -> PhysicalSize { + let bounds = self + .ui_screen + .get_on_main(|ui_screen| ui_screen.nativeBounds()); + PhysicalSize::new(bounds.size.width as u32, bounds.size.height as u32) + } + + pub fn position(&self) -> PhysicalPosition { + let bounds = self + .ui_screen + .get_on_main(|ui_screen| ui_screen.nativeBounds()); + (bounds.origin.x as f64, bounds.origin.y as f64).into() + } + + pub fn scale_factor(&self) -> f64 { + self.ui_screen + .get_on_main(|ui_screen| ui_screen.nativeScale()) as f64 + } + + pub fn refresh_rate_millihertz(&self) -> Option { + Some( + self.ui_screen + .get_on_main(|ui_screen| refresh_rate_millihertz(ui_screen)), + ) + } + + pub fn video_modes(&self) -> impl Iterator { + run_on_main(|mtm| { + let ui_screen = self.ui_screen(mtm); + // Use Ord impl of RootVideoModeHandle + + let modes: BTreeSet<_> = ui_screen + .availableModes() + .into_iter() + .map(|mode| RootVideoModeHandle { + video_mode: VideoModeHandle::new(ui_screen.clone(), mode, mtm), + }) + .collect(); + + modes.into_iter().map(|mode| mode.video_mode) + }) + } + + pub(crate) fn ui_screen(&self, mtm: MainThreadMarker) -> &Retained { + self.ui_screen.get(mtm) + } + + pub fn preferred_video_mode(&self) -> VideoModeHandle { + run_on_main(|mtm| { + VideoModeHandle::new( + self.ui_screen(mtm).clone(), + self.ui_screen(mtm).preferredMode().unwrap(), + mtm, + ) + }) + } +} + +fn refresh_rate_millihertz(uiscreen: &UIScreen) -> u32 { + let refresh_rate_millihertz: NSInteger = { + let os_capabilities = app_state::os_capabilities(); + if os_capabilities.maximum_frames_per_second { + uiscreen.maximumFramesPerSecond() + } else { + // https://developer.apple.com/library/archive/technotes/tn2460/_index.html + // https://en.wikipedia.org/wiki/IPad_Pro#Model_comparison + // + // All iOS devices support 60 fps, and on devices where `maximumFramesPerSecond` is not + // supported, they are all guaranteed to have 60hz refresh rates. This does not + // correctly handle external displays. ProMotion displays support 120fps, but they were + // introduced at the same time as the `maximumFramesPerSecond` API. + // + // FIXME: earlier OSs could calculate the refresh rate using + // `-[CADisplayLink duration]`. + os_capabilities.maximum_frames_per_second_err_msg("defaulting to 60 fps"); + 60 + } + }; + + refresh_rate_millihertz as u32 * 1000 +} + +pub fn uiscreens(mtm: MainThreadMarker) -> VecDeque { + #[allow(deprecated)] + UIScreen::screens(mtm) + .into_iter() + .map(MonitorHandle::new) + .collect() +} diff --git a/rio-window/src/platform_impl/ios/view.rs b/rio-window/src/platform_impl/ios/view.rs new file mode 100644 index 00000000..1ef54ef9 --- /dev/null +++ b/rio-window/src/platform_impl/ios/view.rs @@ -0,0 +1,525 @@ +#![allow(clippy::unnecessary_cast)] +use std::cell::{Cell, RefCell}; + +use objc2::rc::Retained; +use objc2::runtime::{NSObjectProtocol, ProtocolObject}; +use objc2::{ + declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass, +}; +use objc2_foundation::{CGFloat, CGPoint, CGRect, MainThreadMarker, NSObject, NSSet}; +use objc2_ui_kit::{ + UICoordinateSpace, UIEvent, UIForceTouchCapability, UIGestureRecognizer, + UIGestureRecognizerDelegate, UIGestureRecognizerState, UIPanGestureRecognizer, + UIPinchGestureRecognizer, UIResponder, UIRotationGestureRecognizer, + UITapGestureRecognizer, UITouch, UITouchPhase, UITouchType, UITraitEnvironment, + UIView, +}; + +use super::app_state::{self, EventWrapper}; +use super::window::WinitUIWindow; +use crate::dpi::PhysicalPosition; +use crate::event::{Event, Force, Touch, TouchPhase, WindowEvent}; +use crate::platform_impl::platform::DEVICE_ID; +use crate::window::{WindowAttributes, WindowId as RootWindowId}; + +pub struct WinitViewState { + pinch_gesture_recognizer: RefCell>>, + doubletap_gesture_recognizer: RefCell>>, + rotation_gesture_recognizer: RefCell>>, + pan_gesture_recognizer: RefCell>>, + + // for iOS delta references the start of the Gesture + rotation_last_delta: Cell, + pinch_last_delta: Cell, + pan_last_delta: Cell, +} + +declare_class!( + pub(crate) struct WinitView; + + unsafe impl ClassType for WinitView { + #[inherits(UIResponder, NSObject)] + type Super = UIView; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitUIView"; + } + + impl DeclaredClass for WinitView { + type Ivars = WinitViewState; + } + + unsafe impl WinitView { + #[method(drawRect:)] + fn draw_rect(&self, rect: CGRect) { + let mtm = MainThreadMarker::new().unwrap(); + let window = self.window().unwrap(); + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::RedrawRequested, + }), + ); + let _: () = unsafe { msg_send![super(self), drawRect: rect] }; + } + + #[method(layoutSubviews)] + fn layout_subviews(&self) { + let mtm = MainThreadMarker::new().unwrap(); + let _: () = unsafe { msg_send![super(self), layoutSubviews] }; + + let window = self.window().unwrap(); + let window_bounds = window.bounds(); + let screen = window.screen(); + let screen_space = screen.coordinateSpace(); + let screen_frame = self.convertRect_toCoordinateSpace(window_bounds, &screen_space); + let scale_factor = screen.scale(); + let size = crate::dpi::LogicalSize { + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, + } + .to_physical(scale_factor as f64); + + // If the app is started in landscape, the view frame and window bounds can be mismatched. + // The view frame will be in portrait and the window bounds in landscape. So apply the + // window bounds to the view frame to make it consistent. + let view_frame = self.frame(); + if view_frame != window_bounds { + self.setFrame(window_bounds); + } + + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Resized(size), + }), + ); + } + + #[method(setContentScaleFactor:)] + fn set_content_scale_factor(&self, untrusted_scale_factor: CGFloat) { + let mtm = MainThreadMarker::new().unwrap(); + let _: () = + unsafe { msg_send![super(self), setContentScaleFactor: untrusted_scale_factor] }; + + // `window` is null when `setContentScaleFactor` is invoked prior to `[UIWindow + // makeKeyAndVisible]` at window creation time (either manually or internally by + // UIKit when the `UIView` is first created), in which case we send no events here + let window = match self.window() { + Some(window) => window, + None => return, + }; + // `setContentScaleFactor` may be called with a value of 0, which means "reset the + // content scale factor to a device-specific default value", so we can't use the + // parameter here. We can query the actual factor using the getter + let scale_factor = self.contentScaleFactor(); + assert!( + !scale_factor.is_nan() + && scale_factor.is_finite() + && scale_factor.is_sign_positive() + && scale_factor > 0.0, + "invalid scale_factor set on UIView", + ); + let scale_factor = scale_factor as f64; + let bounds = self.bounds(); + let screen = window.screen(); + let screen_space = screen.coordinateSpace(); + let screen_frame = self.convertRect_toCoordinateSpace(bounds, &screen_space); + let size = crate::dpi::LogicalSize { + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, + }; + let window_id = RootWindowId(window.id()); + app_state::handle_nonuser_events( + mtm, + std::iter::once(EventWrapper::ScaleFactorChanged( + app_state::ScaleFactorChanged { + window, + scale_factor, + suggested_size: size.to_physical(scale_factor), + }, + )) + .chain(std::iter::once(EventWrapper::StaticEvent( + Event::WindowEvent { + window_id, + event: WindowEvent::Resized(size.to_physical(scale_factor)), + }, + ))), + ); + } + + #[method(touchesBegan:withEvent:)] + fn touches_began(&self, touches: &NSSet, _event: Option<&UIEvent>) { + self.handle_touches(touches) + } + + #[method(touchesMoved:withEvent:)] + fn touches_moved(&self, touches: &NSSet, _event: Option<&UIEvent>) { + self.handle_touches(touches) + } + + #[method(touchesEnded:withEvent:)] + fn touches_ended(&self, touches: &NSSet, _event: Option<&UIEvent>) { + self.handle_touches(touches) + } + + #[method(touchesCancelled:withEvent:)] + fn touches_cancelled(&self, touches: &NSSet, _event: Option<&UIEvent>) { + self.handle_touches(touches) + } + + #[method(pinchGesture:)] + fn pinch_gesture(&self, recognizer: &UIPinchGestureRecognizer) { + let window = self.window().unwrap(); + + let (phase, delta) = match recognizer.state() { + UIGestureRecognizerState::Began => { + self.ivars().pinch_last_delta.set(recognizer.scale()); + (TouchPhase::Started, 0.0) + } + UIGestureRecognizerState::Changed => { + let last_scale: f64 = self.ivars().pinch_last_delta.replace(recognizer.scale()); + (TouchPhase::Moved, recognizer.scale() - last_scale) + } + UIGestureRecognizerState::Ended => { + let last_scale: f64 = self.ivars().pinch_last_delta.replace(0.0); + (TouchPhase::Moved, recognizer.scale() - last_scale) + } + UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => { + self.ivars().rotation_last_delta.set(0.0); + // Pass -delta so that action is reversed + (TouchPhase::Cancelled, -recognizer.scale()) + } + state => panic!("unexpected recognizer state: {:?}", state), + }; + + let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::PinchGesture { + device_id: DEVICE_ID, + delta: delta as f64, + phase, + }, + }); + + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, gesture_event); + } + + #[method(doubleTapGesture:)] + fn double_tap_gesture(&self, recognizer: &UITapGestureRecognizer) { + let window = self.window().unwrap(); + + if recognizer.state() == UIGestureRecognizerState::Ended { + let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::DoubleTapGesture { + device_id: DEVICE_ID, + }, + }); + + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, gesture_event); + } + } + + #[method(rotationGesture:)] + fn rotation_gesture(&self, recognizer: &UIRotationGestureRecognizer) { + let window = self.window().unwrap(); + + let (phase, delta) = match recognizer.state() { + UIGestureRecognizerState::Began => { + self.ivars().rotation_last_delta.set(0.0); + + (TouchPhase::Started, 0.0) + } + UIGestureRecognizerState::Changed => { + let last_rotation = self.ivars().rotation_last_delta.replace(recognizer.rotation()); + + (TouchPhase::Moved, recognizer.rotation() - last_rotation) + } + UIGestureRecognizerState::Ended => { + let last_rotation = self.ivars().rotation_last_delta.replace(0.0); + + (TouchPhase::Ended, recognizer.rotation() - last_rotation) + } + UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => { + self.ivars().rotation_last_delta.set(0.0); + + // Pass -delta so that action is reversed + (TouchPhase::Cancelled, -recognizer.rotation()) + } + state => panic!("unexpected recognizer state: {:?}", state), + }; + + // Make delta negative to match macos, convert to degrees + let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::RotationGesture { + device_id: DEVICE_ID, + delta: -delta.to_degrees() as _, + phase, + }, + }); + + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, gesture_event); + } + + #[method(panGesture:)] + fn pan_gesture(&self, recognizer: &UIPanGestureRecognizer) { + let window = self.window().unwrap(); + + let translation = recognizer.translationInView(Some(self)); + + let (phase, dx, dy) = match recognizer.state() { + UIGestureRecognizerState::Began => { + self.ivars().pan_last_delta.set(translation); + + (TouchPhase::Started, 0.0, 0.0) + } + UIGestureRecognizerState::Changed => { + let last_pan: CGPoint = self.ivars().pan_last_delta.replace(translation); + + let dx = translation.x - last_pan.x; + let dy = translation.y - last_pan.y; + + (TouchPhase::Moved, dx, dy) + } + UIGestureRecognizerState::Ended => { + let last_pan: CGPoint = self.ivars().pan_last_delta.replace(CGPoint{x:0.0, y:0.0}); + + let dx = translation.x - last_pan.x; + let dy = translation.y - last_pan.y; + + (TouchPhase::Ended, dx, dy) + } + UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => { + let last_pan: CGPoint = self.ivars().pan_last_delta.replace(CGPoint{x:0.0, y:0.0}); + + // Pass -delta so that action is reversed + (TouchPhase::Cancelled, -last_pan.x, -last_pan.y) + } + state => panic!("unexpected recognizer state: {:?}", state), + }; + + + let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::PanGesture { + device_id: DEVICE_ID, + delta: PhysicalPosition::new(dx as _, dy as _), + phase, + }, + }); + + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, gesture_event); + } + } + + unsafe impl NSObjectProtocol for WinitView {} + + unsafe impl UIGestureRecognizerDelegate for WinitView { + #[method(gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)] + fn should_recognize_simultaneously(&self, _gesture_recognizer: &UIGestureRecognizer, _other_gesture_recognizer: &UIGestureRecognizer) -> bool { + true + } + } +); + +impl WinitView { + pub(crate) fn new( + mtm: MainThreadMarker, + window_attributes: &WindowAttributes, + frame: CGRect, + ) -> Retained { + let this = mtm.alloc().set_ivars(WinitViewState { + pinch_gesture_recognizer: RefCell::new(None), + doubletap_gesture_recognizer: RefCell::new(None), + rotation_gesture_recognizer: RefCell::new(None), + pan_gesture_recognizer: RefCell::new(None), + + rotation_last_delta: Cell::new(0.0), + pinch_last_delta: Cell::new(0.0), + pan_last_delta: Cell::new(CGPoint { x: 0.0, y: 0.0 }), + }); + let this: Retained = + unsafe { msg_send_id![super(this), initWithFrame: frame] }; + + this.setMultipleTouchEnabled(true); + + if let Some(scale_factor) = window_attributes.platform_specific.scale_factor { + this.setContentScaleFactor(scale_factor as _); + } + + this + } + + fn window(&self) -> Option> { + // SAFETY: `WinitView`s are always installed in a `WinitUIWindow` + (**self) + .window() + .map(|window| unsafe { Retained::cast(window) }) + } + + pub(crate) fn recognize_pinch_gesture(&self, should_recognize: bool) { + let mtm = MainThreadMarker::from(self); + if should_recognize { + if self.ivars().pinch_gesture_recognizer.borrow().is_none() { + let pinch = unsafe { + UIPinchGestureRecognizer::initWithTarget_action( + mtm.alloc(), + Some(self), + Some(sel!(pinchGesture:)), + ) + }; + pinch.setDelegate(Some(ProtocolObject::from_ref(self))); + self.addGestureRecognizer(&pinch); + self.ivars().pinch_gesture_recognizer.replace(Some(pinch)); + } + } else if let Some(recognizer) = self.ivars().pinch_gesture_recognizer.take() { + self.removeGestureRecognizer(&recognizer); + } + } + + pub(crate) fn recognize_pan_gesture( + &self, + should_recognize: bool, + minimum_number_of_touches: u8, + maximum_number_of_touches: u8, + ) { + let mtm = MainThreadMarker::from(self); + if should_recognize { + if self.ivars().pan_gesture_recognizer.borrow().is_none() { + let pan = unsafe { + UIPanGestureRecognizer::initWithTarget_action( + mtm.alloc(), + Some(self), + Some(sel!(panGesture:)), + ) + }; + pan.setDelegate(Some(ProtocolObject::from_ref(self))); + pan.setMinimumNumberOfTouches(minimum_number_of_touches as _); + pan.setMaximumNumberOfTouches(maximum_number_of_touches as _); + self.addGestureRecognizer(&pan); + self.ivars().pan_gesture_recognizer.replace(Some(pan)); + } + } else if let Some(recognizer) = self.ivars().pan_gesture_recognizer.take() { + self.removeGestureRecognizer(&recognizer); + } + } + + pub(crate) fn recognize_doubletap_gesture(&self, should_recognize: bool) { + let mtm = MainThreadMarker::from(self); + if should_recognize { + if self.ivars().doubletap_gesture_recognizer.borrow().is_none() { + let tap = unsafe { + UITapGestureRecognizer::initWithTarget_action( + mtm.alloc(), + Some(self), + Some(sel!(doubleTapGesture:)), + ) + }; + tap.setDelegate(Some(ProtocolObject::from_ref(self))); + tap.setNumberOfTapsRequired(2); + tap.setNumberOfTouchesRequired(1); + self.addGestureRecognizer(&tap); + self.ivars().doubletap_gesture_recognizer.replace(Some(tap)); + } + } else if let Some(recognizer) = self.ivars().doubletap_gesture_recognizer.take() + { + self.removeGestureRecognizer(&recognizer); + } + } + + pub(crate) fn recognize_rotation_gesture(&self, should_recognize: bool) { + let mtm = MainThreadMarker::from(self); + if should_recognize { + if self.ivars().rotation_gesture_recognizer.borrow().is_none() { + let rotation = unsafe { + UIRotationGestureRecognizer::initWithTarget_action( + mtm.alloc(), + Some(self), + Some(sel!(rotationGesture:)), + ) + }; + rotation.setDelegate(Some(ProtocolObject::from_ref(self))); + self.addGestureRecognizer(&rotation); + self.ivars() + .rotation_gesture_recognizer + .replace(Some(rotation)); + } + } else if let Some(recognizer) = self.ivars().rotation_gesture_recognizer.take() { + self.removeGestureRecognizer(&recognizer); + } + } + + fn handle_touches(&self, touches: &NSSet) { + let window = self.window().unwrap(); + let mut touch_events = Vec::new(); + let os_supports_force = app_state::os_capabilities().force_touch; + for touch in touches { + let logical_location = touch.locationInView(None); + let touch_type = touch.r#type(); + let force = if os_supports_force { + let trait_collection = self.traitCollection(); + let touch_capability = trait_collection.forceTouchCapability(); + // Both the OS _and_ the device need to be checked for force touch support. + if touch_capability == UIForceTouchCapability::Available + || touch_type == UITouchType::Pencil + { + let force = touch.force(); + let max_possible_force = touch.maximumPossibleForce(); + let altitude_angle: Option = if touch_type == UITouchType::Pencil + { + let angle = touch.altitudeAngle(); + Some(angle as _) + } else { + None + }; + Some(Force::Calibrated { + force: force as _, + max_possible_force: max_possible_force as _, + altitude_angle, + }) + } else { + None + } + } else { + None + }; + let touch_id = touch as *const UITouch as u64; + let phase = touch.phase(); + let phase = match phase { + UITouchPhase::Began => TouchPhase::Started, + UITouchPhase::Moved => TouchPhase::Moved, + // 2 is UITouchPhase::Stationary and is not expected here + UITouchPhase::Ended => TouchPhase::Ended, + UITouchPhase::Cancelled => TouchPhase::Cancelled, + _ => panic!("unexpected touch phase: {phase:?}"), + }; + + let physical_location = { + let scale_factor = self.contentScaleFactor(); + PhysicalPosition::from_logical::<(f64, f64), f64>( + (logical_location.x as _, logical_location.y as _), + scale_factor as f64, + ) + }; + touch_events.push(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Touch(Touch { + device_id: DEVICE_ID, + id: touch_id, + location: physical_location, + force, + phase, + }), + })); + } + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_events(mtm, touch_events); + } +} diff --git a/rio-window/src/platform_impl/ios/view_controller.rs b/rio-window/src/platform_impl/ios/view_controller.rs new file mode 100644 index 00000000..d22a651c --- /dev/null +++ b/rio-window/src/platform_impl/ios/view_controller.rs @@ -0,0 +1,200 @@ +use std::cell::Cell; + +use objc2::rc::Retained; +use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; +use objc2_foundation::{MainThreadMarker, NSObject}; +use objc2_ui_kit::{ + UIDevice, UIInterfaceOrientationMask, UIRectEdge, UIResponder, UIStatusBarStyle, + UIUserInterfaceIdiom, UIView, UIViewController, +}; + +use super::app_state::{self}; +use crate::platform::ios::{ScreenEdge, StatusBarStyle, ValidOrientations}; +use crate::window::WindowAttributes; + +pub struct ViewControllerState { + prefers_status_bar_hidden: Cell, + preferred_status_bar_style: Cell, + prefers_home_indicator_auto_hidden: Cell, + supported_orientations: Cell, + preferred_screen_edges_deferring_system_gestures: Cell, +} + +declare_class!( + pub(crate) struct WinitViewController; + + unsafe impl ClassType for WinitViewController { + #[inherits(UIResponder, NSObject)] + type Super = UIViewController; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitUIViewController"; + } + + impl DeclaredClass for WinitViewController { + type Ivars = ViewControllerState; + } + + unsafe impl WinitViewController { + #[method(shouldAutorotate)] + fn should_autorotate(&self) -> bool { + true + } + + #[method(prefersStatusBarHidden)] + fn prefers_status_bar_hidden(&self) -> bool { + self.ivars().prefers_status_bar_hidden.get() + } + + #[method(preferredStatusBarStyle)] + fn preferred_status_bar_style(&self) -> UIStatusBarStyle { + self.ivars().preferred_status_bar_style.get() + } + + #[method(prefersHomeIndicatorAutoHidden)] + fn prefers_home_indicator_auto_hidden(&self) -> bool { + self.ivars().prefers_home_indicator_auto_hidden.get() + } + + #[method(supportedInterfaceOrientations)] + fn supported_orientations(&self) -> UIInterfaceOrientationMask { + self.ivars().supported_orientations.get() + } + + #[method(preferredScreenEdgesDeferringSystemGestures)] + fn preferred_screen_edges_deferring_system_gestures(&self) -> UIRectEdge { + self.ivars() + .preferred_screen_edges_deferring_system_gestures + .get() + } + } +); + +impl WinitViewController { + pub(crate) fn set_prefers_status_bar_hidden(&self, val: bool) { + self.ivars().prefers_status_bar_hidden.set(val); + self.setNeedsStatusBarAppearanceUpdate(); + } + + pub(crate) fn set_preferred_status_bar_style(&self, val: StatusBarStyle) { + let val = match val { + StatusBarStyle::Default => UIStatusBarStyle::Default, + StatusBarStyle::LightContent => UIStatusBarStyle::LightContent, + StatusBarStyle::DarkContent => UIStatusBarStyle::DarkContent, + }; + self.ivars().preferred_status_bar_style.set(val); + self.setNeedsStatusBarAppearanceUpdate(); + } + + pub(crate) fn set_prefers_home_indicator_auto_hidden(&self, val: bool) { + self.ivars().prefers_home_indicator_auto_hidden.set(val); + let os_capabilities = app_state::os_capabilities(); + if os_capabilities.home_indicator_hidden { + self.setNeedsUpdateOfHomeIndicatorAutoHidden(); + } else { + os_capabilities.home_indicator_hidden_err_msg("ignoring") + } + } + + pub(crate) fn set_preferred_screen_edges_deferring_system_gestures( + &self, + val: ScreenEdge, + ) { + let val = { + assert_eq!( + val.bits() & !ScreenEdge::ALL.bits(), + 0, + "invalid `ScreenEdge`" + ); + UIRectEdge(val.bits().into()) + }; + self.ivars() + .preferred_screen_edges_deferring_system_gestures + .set(val); + let os_capabilities = app_state::os_capabilities(); + if os_capabilities.defer_system_gestures { + self.setNeedsUpdateOfScreenEdgesDeferringSystemGestures(); + } else { + os_capabilities.defer_system_gestures_err_msg("ignoring") + } + } + + pub(crate) fn set_supported_interface_orientations( + &self, + mtm: MainThreadMarker, + valid_orientations: ValidOrientations, + ) { + let mask = match ( + valid_orientations, + UIDevice::currentDevice(mtm).userInterfaceIdiom(), + ) { + (ValidOrientations::LandscapeAndPortrait, UIUserInterfaceIdiom::Phone) => { + UIInterfaceOrientationMask::AllButUpsideDown + } + (ValidOrientations::LandscapeAndPortrait, _) => { + UIInterfaceOrientationMask::All + } + (ValidOrientations::Landscape, _) => UIInterfaceOrientationMask::Landscape, + (ValidOrientations::Portrait, UIUserInterfaceIdiom::Phone) => { + UIInterfaceOrientationMask::Portrait + } + (ValidOrientations::Portrait, _) => { + UIInterfaceOrientationMask::Portrait + | UIInterfaceOrientationMask::PortraitUpsideDown + } + }; + self.ivars().supported_orientations.set(mask); + #[allow(deprecated)] + UIViewController::attemptRotationToDeviceOrientation(mtm); + } + + pub(crate) fn new( + mtm: MainThreadMarker, + window_attributes: &WindowAttributes, + view: &UIView, + ) -> Retained { + // These are set properly below, we just to set them to something in the meantime. + let this = mtm.alloc().set_ivars(ViewControllerState { + prefers_status_bar_hidden: Cell::new(false), + preferred_status_bar_style: Cell::new(UIStatusBarStyle::Default), + prefers_home_indicator_auto_hidden: Cell::new(false), + supported_orientations: Cell::new(UIInterfaceOrientationMask::All), + preferred_screen_edges_deferring_system_gestures: Cell::new( + UIRectEdge::empty(), + ), + }); + let this: Retained = unsafe { msg_send_id![super(this), init] }; + + this.set_prefers_status_bar_hidden( + window_attributes + .platform_specific + .prefers_status_bar_hidden, + ); + + this.set_preferred_status_bar_style( + window_attributes + .platform_specific + .preferred_status_bar_style, + ); + + this.set_supported_interface_orientations( + mtm, + window_attributes.platform_specific.valid_orientations, + ); + + this.set_prefers_home_indicator_auto_hidden( + window_attributes + .platform_specific + .prefers_home_indicator_hidden, + ); + + this.set_preferred_screen_edges_deferring_system_gestures( + window_attributes + .platform_specific + .preferred_screen_edges_deferring_system_gestures, + ); + + this.setView(Some(view)); + + this + } +} diff --git a/rio-window/src/platform_impl/ios/window.rs b/rio-window/src/platform_impl/ios/window.rs new file mode 100644 index 00000000..8f0e7347 --- /dev/null +++ b/rio-window/src/platform_impl/ios/window.rs @@ -0,0 +1,772 @@ +#![allow(clippy::unnecessary_cast)] + +use std::collections::VecDeque; + +use objc2::rc::Retained; +use objc2::runtime::{AnyObject, NSObject}; +use objc2::{ + class, declare_class, msg_send, msg_send_id, mutability, ClassType, DeclaredClass, +}; +use objc2_foundation::{ + CGFloat, CGPoint, CGRect, CGSize, MainThreadBound, MainThreadMarker, NSObjectProtocol, +}; +use objc2_ui_kit::{ + UIApplication, UICoordinateSpace, UIResponder, UIScreen, + UIScreenOverscanCompensation, UIViewController, UIWindow, +}; +use tracing::{debug, warn}; + +use super::app_state::EventWrapper; +use super::view::WinitView; +use super::view_controller::WinitViewController; +use crate::cursor::Cursor; +use crate::dpi::{ + LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size, +}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event::{Event, WindowEvent}; +use crate::icon::Icon; +use crate::platform::ios::{ScreenEdge, StatusBarStyle, ValidOrientations}; +use crate::platform_impl::platform::{ + app_state, monitor, ActiveEventLoop, Fullscreen, MonitorHandle, +}; +use crate::window::{ + CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowId as RootWindowId, WindowLevel, +}; + +declare_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct WinitUIWindow; + + unsafe impl ClassType for WinitUIWindow { + #[inherits(UIResponder, NSObject)] + type Super = UIWindow; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitUIWindow"; + } + + impl DeclaredClass for WinitUIWindow {} + + unsafe impl WinitUIWindow { + #[method(becomeKeyWindow)] + fn become_key_window(&self) { + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(self.id()), + event: WindowEvent::Focused(true), + }), + ); + let _: () = unsafe { msg_send![super(self), becomeKeyWindow] }; + } + + #[method(resignKeyWindow)] + fn resign_key_window(&self) { + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(self.id()), + event: WindowEvent::Focused(false), + }), + ); + let _: () = unsafe { msg_send![super(self), resignKeyWindow] }; + } + } +); + +impl WinitUIWindow { + pub(crate) fn new( + mtm: MainThreadMarker, + window_attributes: &WindowAttributes, + frame: CGRect, + view_controller: &UIViewController, + ) -> Retained { + let this: Retained = + unsafe { msg_send_id![mtm.alloc(), initWithFrame: frame] }; + + this.setRootViewController(Some(view_controller)); + + match window_attributes.fullscreen.clone().map(Into::into) { + Some(Fullscreen::Exclusive(ref video_mode)) => { + let monitor = video_mode.monitor(); + let screen = monitor.ui_screen(mtm); + screen.setCurrentMode(Some(video_mode.screen_mode(mtm))); + this.setScreen(screen); + } + Some(Fullscreen::Borderless(Some(ref monitor))) => { + let screen = monitor.ui_screen(mtm); + this.setScreen(screen); + } + _ => (), + } + + this + } + + pub(crate) fn id(&self) -> WindowId { + (self as *const Self as usize as u64).into() + } +} + +pub struct Inner { + window: Retained, + view_controller: Retained, + view: Retained, + gl_or_metal_backed: bool, +} + +impl Inner { + pub fn set_title(&self, _title: &str) { + debug!("`Window::set_title` is ignored on iOS") + } + + pub fn set_transparent(&self, _transparent: bool) { + debug!("`Window::set_transparent` is ignored on iOS") + } + + pub fn set_blur(&self, _blur: bool) { + debug!("`Window::set_blur` is ignored on iOS") + } + + pub fn set_visible(&self, visible: bool) { + self.window.setHidden(!visible) + } + + pub fn is_visible(&self) -> Option { + warn!("`Window::is_visible` is ignored on iOS"); + None + } + + pub fn request_redraw(&self) { + if self.gl_or_metal_backed { + let mtm = MainThreadMarker::new().unwrap(); + // `setNeedsDisplay` does nothing on UIViews which are directly backed by CAEAGLLayer or + // CAMetalLayer. Ordinarily the OS sets up a bunch of UIKit state before + // calling drawRect: on a UIView, but when using raw or gl/metal for drawing + // this work is completely avoided. + // + // The docs for `setNeedsDisplay` don't mention `CAMetalLayer`; however, this has been + // confirmed via testing. + // + // https://developer.apple.com/documentation/uikit/uiview/1622437-setneedsdisplay?language=objc + app_state::queue_gl_or_metal_redraw(mtm, self.window.clone()); + } else { + self.view.setNeedsDisplay(); + } + } + + pub fn pre_present_notify(&self) {} + + pub fn inner_position(&self) -> Result, NotSupportedError> { + let safe_area = self.safe_area_screen_space(); + let position = LogicalPosition { + x: safe_area.origin.x as f64, + y: safe_area.origin.y as f64, + }; + let scale_factor = self.scale_factor(); + Ok(position.to_physical(scale_factor)) + } + + pub fn outer_position(&self) -> Result, NotSupportedError> { + let screen_frame = self.screen_frame(); + let position = LogicalPosition { + x: screen_frame.origin.x as f64, + y: screen_frame.origin.y as f64, + }; + let scale_factor = self.scale_factor(); + Ok(position.to_physical(scale_factor)) + } + + pub fn set_outer_position(&self, physical_position: Position) { + let scale_factor = self.scale_factor(); + let position = physical_position.to_logical::(scale_factor); + let screen_frame = self.screen_frame(); + let new_screen_frame = CGRect { + origin: CGPoint { + x: position.x as _, + y: position.y as _, + }, + size: screen_frame.size, + }; + let bounds = self.rect_from_screen_space(new_screen_frame); + self.window.setBounds(bounds); + } + + pub fn inner_size(&self) -> PhysicalSize { + let scale_factor = self.scale_factor(); + let safe_area = self.safe_area_screen_space(); + let size = LogicalSize { + width: safe_area.size.width as f64, + height: safe_area.size.height as f64, + }; + size.to_physical(scale_factor) + } + + pub fn outer_size(&self) -> PhysicalSize { + let scale_factor = self.scale_factor(); + let screen_frame = self.screen_frame(); + let size = LogicalSize { + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, + }; + size.to_physical(scale_factor) + } + + pub fn request_inner_size(&self, _size: Size) -> Option> { + Some(self.inner_size()) + } + + pub fn set_min_inner_size(&self, _dimensions: Option) { + warn!("`Window::set_min_inner_size` is ignored on iOS") + } + + pub fn set_max_inner_size(&self, _dimensions: Option) { + warn!("`Window::set_max_inner_size` is ignored on iOS") + } + + pub fn resize_increments(&self) -> Option> { + None + } + + #[inline] + pub fn set_resize_increments(&self, _increments: Option) { + warn!("`Window::set_resize_increments` is ignored on iOS") + } + + pub fn set_resizable(&self, _resizable: bool) { + warn!("`Window::set_resizable` is ignored on iOS") + } + + pub fn is_resizable(&self) -> bool { + warn!("`Window::is_resizable` is ignored on iOS"); + false + } + + #[inline] + pub fn set_enabled_buttons(&self, _buttons: WindowButtons) { + warn!("`Window::set_enabled_buttons` is ignored on iOS"); + } + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + warn!("`Window::enabled_buttons` is ignored on iOS"); + WindowButtons::all() + } + + pub fn scale_factor(&self) -> f64 { + self.view.contentScaleFactor() as _ + } + + pub fn set_cursor(&self, _cursor: Cursor) { + debug!("`Window::set_cursor` ignored on iOS") + } + + pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + pub fn set_cursor_grab(&self, _: CursorGrabMode) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + pub fn set_cursor_visible(&self, _visible: bool) { + debug!("`Window::set_cursor_visible` is ignored on iOS") + } + + pub fn drag_window(&self) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + pub fn drag_resize_window( + &self, + _direction: ResizeDirection, + ) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + pub fn set_minimized(&self, _minimized: bool) { + warn!("`Window::set_minimized` is ignored on iOS") + } + + pub fn is_minimized(&self) -> Option { + warn!("`Window::is_minimized` is ignored on iOS"); + None + } + + pub fn set_maximized(&self, _maximized: bool) { + warn!("`Window::set_maximized` is ignored on iOS") + } + + pub fn is_maximized(&self) -> bool { + warn!("`Window::is_maximized` is ignored on iOS"); + false + } + + pub(crate) fn set_fullscreen(&self, monitor: Option) { + let mtm = MainThreadMarker::new().unwrap(); + let uiscreen = match &monitor { + Some(Fullscreen::Exclusive(video_mode)) => { + let uiscreen = video_mode.monitor.ui_screen(mtm); + uiscreen.setCurrentMode(Some(video_mode.screen_mode(mtm))); + uiscreen.clone() + } + Some(Fullscreen::Borderless(Some(monitor))) => monitor.ui_screen(mtm).clone(), + Some(Fullscreen::Borderless(None)) => { + self.current_monitor_inner().ui_screen(mtm).clone() + } + None => { + warn!("`Window::set_fullscreen(None)` ignored on iOS"); + return; + } + }; + + // this is pretty slow on iOS, so avoid doing it if we can + let current = self.window.screen(); + if uiscreen != current { + self.window.setScreen(&uiscreen); + } + + let bounds = uiscreen.bounds(); + self.window.setFrame(bounds); + + // For external displays, we must disable overscan compensation or + // the displayed image will have giant black bars surrounding it on + // each side + uiscreen.setOverscanCompensation(UIScreenOverscanCompensation::None); + } + + pub(crate) fn fullscreen(&self) -> Option { + let mtm = MainThreadMarker::new().unwrap(); + let monitor = self.current_monitor_inner(); + let uiscreen = monitor.ui_screen(mtm); + let screen_space_bounds = self.screen_frame(); + let screen_bounds = uiscreen.bounds(); + + // TODO: track fullscreen instead of relying on brittle float comparisons + if screen_space_bounds.origin.x == screen_bounds.origin.x + && screen_space_bounds.origin.y == screen_bounds.origin.y + && screen_space_bounds.size.width == screen_bounds.size.width + && screen_space_bounds.size.height == screen_bounds.size.height + { + Some(Fullscreen::Borderless(Some(monitor))) + } else { + None + } + } + + pub fn set_decorations(&self, _decorations: bool) {} + + pub fn is_decorated(&self) -> bool { + true + } + + pub fn set_window_level(&self, _level: WindowLevel) { + warn!("`Window::set_window_level` is ignored on iOS") + } + + pub fn set_window_icon(&self, _icon: Option) { + warn!("`Window::set_window_icon` is ignored on iOS") + } + + pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) { + warn!("`Window::set_ime_cursor_area` is ignored on iOS") + } + + pub fn set_ime_allowed(&self, _allowed: bool) { + warn!("`Window::set_ime_allowed` is ignored on iOS") + } + + pub fn set_ime_purpose(&self, _purpose: ImePurpose) { + warn!("`Window::set_ime_allowed` is ignored on iOS") + } + + pub fn focus_window(&self) { + warn!("`Window::set_focus` is ignored on iOS") + } + + pub fn request_user_attention(&self, _request_type: Option) { + warn!("`Window::request_user_attention` is ignored on iOS") + } + + // Allow directly accessing the current monitor internally without unwrapping. + fn current_monitor_inner(&self) -> MonitorHandle { + MonitorHandle::new(self.window.screen()) + } + + pub fn current_monitor(&self) -> Option { + Some(self.current_monitor_inner()) + } + + pub fn available_monitors(&self) -> VecDeque { + monitor::uiscreens(MainThreadMarker::new().unwrap()) + } + + pub fn primary_monitor(&self) -> Option { + #[allow(deprecated)] + Some(MonitorHandle::new(UIScreen::mainScreen( + MainThreadMarker::new().unwrap(), + ))) + } + + pub fn id(&self) -> WindowId { + self.window.id() + } + + #[cfg(feature = "rwh_06")] + pub fn raw_window_handle_rwh_06(&self) -> rwh_06::RawWindowHandle { + let mut window_handle = rwh_06::UiKitWindowHandle::new({ + let ui_view = Retained::as_ptr(&self.view) as _; + std::ptr::NonNull::new(ui_view).expect("Retained should never be null") + }); + window_handle.ui_view_controller = + std::ptr::NonNull::new(Retained::as_ptr(&self.view_controller) as _); + rwh_06::RawWindowHandle::UiKit(window_handle) + } + + pub fn theme(&self) -> Option { + warn!("`Window::theme` is ignored on iOS"); + None + } + + pub fn set_content_protected(&self, _protected: bool) {} + + pub fn has_focus(&self) -> bool { + self.window.isKeyWindow() + } + + #[inline] + pub fn set_theme(&self, _theme: Option) { + warn!("`Window::set_theme` is ignored on iOS"); + } + + pub fn title(&self) -> String { + warn!("`Window::title` is ignored on iOS"); + String::new() + } + + pub fn reset_dead_keys(&self) { + // Noop + } +} + +pub struct Window { + inner: MainThreadBound, +} + +impl Window { + pub(crate) fn new( + event_loop: &ActiveEventLoop, + window_attributes: WindowAttributes, + ) -> Result { + let mtm = event_loop.mtm; + + if window_attributes.min_inner_size.is_some() { + warn!("`WindowAttributes::min_inner_size` is ignored on iOS"); + } + if window_attributes.max_inner_size.is_some() { + warn!("`WindowAttributes::max_inner_size` is ignored on iOS"); + } + + // TODO: transparency, visible + + #[allow(deprecated)] + let main_screen = UIScreen::mainScreen(mtm); + let fullscreen = window_attributes.fullscreen.clone().map(Into::into); + let screen = match fullscreen { + Some(Fullscreen::Exclusive(ref video_mode)) => { + video_mode.monitor.ui_screen(mtm) + } + Some(Fullscreen::Borderless(Some(ref monitor))) => monitor.ui_screen(mtm), + Some(Fullscreen::Borderless(None)) | None => &main_screen, + }; + + let screen_bounds = screen.bounds(); + + let frame = match window_attributes.inner_size { + Some(dim) => { + let scale_factor = screen.scale(); + let size = dim.to_logical::(scale_factor as f64); + CGRect { + origin: screen_bounds.origin, + size: CGSize { + width: size.width as _, + height: size.height as _, + }, + } + } + None => screen_bounds, + }; + + let view = WinitView::new(mtm, &window_attributes, frame); + + let gl_or_metal_backed = view.isKindOfClass(class!(CAMetalLayer)) + || view.isKindOfClass(class!(CAEAGLLayer)); + + let view_controller = WinitViewController::new(mtm, &window_attributes, &view); + let window = WinitUIWindow::new(mtm, &window_attributes, frame, &view_controller); + + app_state::set_key_window(mtm, &window); + + // Like the Windows and macOS backends, we send a `ScaleFactorChanged` and `Resized` + // event on window creation if the DPI factor != 1.0 + let scale_factor = view.contentScaleFactor(); + let scale_factor = scale_factor as f64; + if scale_factor != 1.0 { + let bounds = view.bounds(); + let screen = window.screen(); + let screen_space = screen.coordinateSpace(); + let screen_frame = view.convertRect_toCoordinateSpace(bounds, &screen_space); + let size = LogicalSize { + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, + }; + let window_id = RootWindowId(window.id()); + app_state::handle_nonuser_events( + mtm, + std::iter::once(EventWrapper::ScaleFactorChanged( + app_state::ScaleFactorChanged { + window: window.clone(), + scale_factor, + suggested_size: size.to_physical(scale_factor), + }, + )) + .chain(std::iter::once(EventWrapper::StaticEvent( + Event::WindowEvent { + window_id, + event: WindowEvent::Resized(size.to_physical(scale_factor)), + }, + ))), + ); + } + + let inner = Inner { + window, + view_controller, + view, + gl_or_metal_backed, + }; + Ok(Window { + inner: MainThreadBound::new(inner, mtm), + }) + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Inner) + Send + 'static) { + // For now, don't actually do queuing, since it may be less predictable + self.maybe_wait_on_main(f) + } + + pub(crate) fn maybe_wait_on_main( + &self, + f: impl FnOnce(&Inner) -> R + Send, + ) -> R { + self.inner.get_on_main(|inner| f(inner)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_window_handle_rwh_06( + &self, + ) -> Result { + if let Some(mtm) = MainThreadMarker::new() { + Ok(self.inner.get(mtm).raw_window_handle_rwh_06()) + } else { + Err(rwh_06::HandleError::Unavailable) + } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::UiKit( + rwh_06::UiKitDisplayHandle::new(), + )) + } +} + +// WindowExtIOS +impl Inner { + pub fn set_scale_factor(&self, scale_factor: f64) { + assert!( + dpi::validate_scale_factor(scale_factor), + "`WindowExtIOS::set_scale_factor` received an invalid hidpi factor" + ); + let scale_factor = scale_factor as CGFloat; + self.view.setContentScaleFactor(scale_factor); + } + + pub fn set_valid_orientations(&self, valid_orientations: ValidOrientations) { + self.view_controller.set_supported_interface_orientations( + MainThreadMarker::new().unwrap(), + valid_orientations, + ); + } + + pub fn set_prefers_home_indicator_hidden(&self, hidden: bool) { + self.view_controller + .set_prefers_home_indicator_auto_hidden(hidden); + } + + pub fn set_preferred_screen_edges_deferring_system_gestures( + &self, + edges: ScreenEdge, + ) { + self.view_controller + .set_preferred_screen_edges_deferring_system_gestures(edges); + } + + pub fn set_prefers_status_bar_hidden(&self, hidden: bool) { + self.view_controller.set_prefers_status_bar_hidden(hidden); + } + + pub fn set_preferred_status_bar_style(&self, status_bar_style: StatusBarStyle) { + self.view_controller + .set_preferred_status_bar_style(status_bar_style); + } + + pub fn recognize_pinch_gesture(&self, should_recognize: bool) { + self.view.recognize_pinch_gesture(should_recognize); + } + + pub fn recognize_pan_gesture( + &self, + should_recognize: bool, + minimum_number_of_touches: u8, + maximum_number_of_touches: u8, + ) { + self.view.recognize_pan_gesture( + should_recognize, + minimum_number_of_touches, + maximum_number_of_touches, + ); + } + + pub fn recognize_doubletap_gesture(&self, should_recognize: bool) { + self.view.recognize_doubletap_gesture(should_recognize); + } + + pub fn recognize_rotation_gesture(&self, should_recognize: bool) { + self.view.recognize_rotation_gesture(should_recognize); + } +} + +impl Inner { + fn screen_frame(&self) -> CGRect { + self.rect_to_screen_space(self.window.bounds()) + } + + fn rect_to_screen_space(&self, rect: CGRect) -> CGRect { + let screen_space = self.window.screen().coordinateSpace(); + self.window + .convertRect_toCoordinateSpace(rect, &screen_space) + } + + fn rect_from_screen_space(&self, rect: CGRect) -> CGRect { + let screen_space = self.window.screen().coordinateSpace(); + self.window + .convertRect_fromCoordinateSpace(rect, &screen_space) + } + + fn safe_area_screen_space(&self) -> CGRect { + let bounds = self.window.bounds(); + if app_state::os_capabilities().safe_area { + let safe_area = self.window.safeAreaInsets(); + let safe_bounds = CGRect { + origin: CGPoint { + x: bounds.origin.x + safe_area.left, + y: bounds.origin.y + safe_area.top, + }, + size: CGSize { + width: bounds.size.width - safe_area.left - safe_area.right, + height: bounds.size.height - safe_area.top - safe_area.bottom, + }, + }; + self.rect_to_screen_space(safe_bounds) + } else { + let screen_frame = self.rect_to_screen_space(bounds); + let status_bar_frame = { + let app = + UIApplication::sharedApplication(MainThreadMarker::new().unwrap()); + #[allow(deprecated)] + app.statusBarFrame() + }; + let (y, height) = if screen_frame.origin.y > status_bar_frame.size.height { + (screen_frame.origin.y, screen_frame.size.height) + } else { + let y = status_bar_frame.size.height; + let height = screen_frame.size.height + - (status_bar_frame.size.height - screen_frame.origin.y); + (y, height) + }; + CGRect { + origin: CGPoint { + x: screen_frame.origin.x, + y, + }, + size: CGSize { + width: screen_frame.size.width, + height, + }, + } + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId { + window: *mut WinitUIWindow, +} + +impl WindowId { + pub const unsafe fn dummy() -> Self { + WindowId { + window: std::ptr::null_mut(), + } + } +} + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.window as u64 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self { + window: raw_id as _, + } + } +} + +unsafe impl Send for WindowId {} +unsafe impl Sync for WindowId {} + +impl From<&AnyObject> for WindowId { + fn from(window: &AnyObject) -> WindowId { + WindowId { + window: window as *const _ as _, + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct PlatformSpecificWindowAttributes { + pub scale_factor: Option, + pub valid_orientations: ValidOrientations, + pub prefers_home_indicator_hidden: bool, + pub prefers_status_bar_hidden: bool, + pub preferred_status_bar_style: StatusBarStyle, + pub preferred_screen_edges_deferring_system_gestures: ScreenEdge, +} diff --git a/rio-window/src/platform_impl/linux/common/mod.rs b/rio-window/src/platform_impl/linux/common/mod.rs new file mode 100644 index 00000000..ed7f82cd --- /dev/null +++ b/rio-window/src/platform_impl/linux/common/mod.rs @@ -0,0 +1 @@ +pub mod xkb; diff --git a/rio-window/src/platform_impl/linux/common/xkb/compose.rs b/rio-window/src/platform_impl/linux/common/xkb/compose.rs new file mode 100644 index 00000000..d2e01127 --- /dev/null +++ b/rio-window/src/platform_impl/linux/common/xkb/compose.rs @@ -0,0 +1,124 @@ +//! XKB compose handling. + +use std::env; +use std::ffi::CString; +use std::ops::Deref; +use std::os::unix::ffi::OsStringExt; +use std::ptr::NonNull; + +use super::{XkbContext, XKBCH}; +use smol_str::SmolStr; +use xkbcommon_dl::{ + xkb_compose_compile_flags, xkb_compose_feed_result, xkb_compose_state, + xkb_compose_state_flags, xkb_compose_status, xkb_compose_table, xkb_keysym_t, +}; + +#[derive(Debug)] +pub struct XkbComposeTable { + table: NonNull, +} + +impl XkbComposeTable { + pub fn new(context: &XkbContext) -> Option { + let locale = env::var_os("LC_ALL") + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .or_else(|| env::var_os("LC_CTYPE")) + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .or_else(|| env::var_os("LANG")) + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .unwrap_or_else(|| "C".into()); + let locale = CString::new(locale.into_vec()).unwrap(); + + let table = unsafe { + (XKBCH.xkb_compose_table_new_from_locale)( + context.as_ptr(), + locale.as_ptr(), + xkb_compose_compile_flags::XKB_COMPOSE_COMPILE_NO_FLAGS, + ) + }; + + let table = NonNull::new(table)?; + Some(Self { table }) + } + + /// Create new state with the given compose table. + pub fn new_state(&self) -> Option { + let state = unsafe { + (XKBCH.xkb_compose_state_new)( + self.table.as_ptr(), + xkb_compose_state_flags::XKB_COMPOSE_STATE_NO_FLAGS, + ) + }; + + let state = NonNull::new(state)?; + Some(XkbComposeState { state }) + } +} + +impl Deref for XkbComposeTable { + type Target = NonNull; + + fn deref(&self) -> &Self::Target { + &self.table + } +} + +impl Drop for XkbComposeTable { + fn drop(&mut self) { + unsafe { + (XKBCH.xkb_compose_table_unref)(self.table.as_ptr()); + } + } +} + +#[derive(Debug)] +pub struct XkbComposeState { + state: NonNull, +} + +impl XkbComposeState { + pub fn get_string(&mut self, scratch_buffer: &mut Vec) -> Option { + super::make_string_with(scratch_buffer, |ptr, len| unsafe { + (XKBCH.xkb_compose_state_get_utf8)(self.state.as_ptr(), ptr, len) + }) + } + + #[inline] + pub fn feed(&mut self, keysym: xkb_keysym_t) -> ComposeStatus { + let feed_result = + unsafe { (XKBCH.xkb_compose_state_feed)(self.state.as_ptr(), keysym) }; + match feed_result { + xkb_compose_feed_result::XKB_COMPOSE_FEED_IGNORED => ComposeStatus::Ignored, + xkb_compose_feed_result::XKB_COMPOSE_FEED_ACCEPTED => { + ComposeStatus::Accepted(self.status()) + } + } + } + + #[inline] + pub fn reset(&mut self) { + unsafe { + (XKBCH.xkb_compose_state_reset)(self.state.as_ptr()); + } + } + + #[inline] + pub fn status(&mut self) -> xkb_compose_status { + unsafe { (XKBCH.xkb_compose_state_get_status)(self.state.as_ptr()) } + } +} + +impl Drop for XkbComposeState { + fn drop(&mut self) { + unsafe { + (XKBCH.xkb_compose_state_unref)(self.state.as_ptr()); + }; + } +} + +#[derive(Copy, Clone, Debug)] +pub enum ComposeStatus { + Accepted(xkb_compose_status), + Ignored, + None, +} diff --git a/rio-window/src/platform_impl/linux/common/xkb/keymap.rs b/rio-window/src/platform_impl/linux/common/xkb/keymap.rs new file mode 100644 index 00000000..2baec820 --- /dev/null +++ b/rio-window/src/platform_impl/linux/common/xkb/keymap.rs @@ -0,0 +1,1055 @@ +//! XKB keymap. + +use std::ffi::c_char; +use std::ops::Deref; +use std::ptr::{self, NonNull}; + +#[cfg(x11_platform)] +use x11_dl::xlib_xcb::xcb_connection_t; +#[cfg(wayland_platform)] +use {memmap2::MmapOptions, std::os::unix::io::OwnedFd}; + +use xkb::XKB_MOD_INVALID; +use xkbcommon_dl::{ + self as xkb, xkb_keycode_t, xkb_keymap, xkb_keymap_compile_flags, xkb_keysym_t, + xkb_layout_index_t, xkb_mod_index_t, +}; + +use crate::keyboard::{ + Key, KeyCode, KeyLocation, NamedKey, NativeKey, NativeKeyCode, PhysicalKey, +}; +#[cfg(x11_platform)] +use crate::platform_impl::common::xkb::XKBXH; +use crate::platform_impl::common::xkb::{XkbContext, XKBH}; + +/// Map the raw X11-style keycode to the `KeyCode` enum. +/// +/// X11-style keycodes are offset by 8 from the keycodes the Linux kernel uses. +pub fn raw_keycode_to_physicalkey(keycode: u32) -> PhysicalKey { + scancode_to_physicalkey(keycode.saturating_sub(8)) +} + +/// Map the linux scancode to Keycode. +/// +/// Both X11 and Wayland use keys with `+ 8` offset to linux scancode. +pub fn scancode_to_physicalkey(scancode: u32) -> PhysicalKey { + // The keycode values are taken from linux/include/uapi/linux/input-event-codes.h, as + // libxkbcommon's documentation seems to suggest that the keycode values we're interested in + // are defined by the Linux kernel. If Winit programs end up being run on other Unix-likes, + // I can only hope they agree on what the keycodes mean. + // + // Some of the keycodes are likely superfluous for our purposes, and some are ones which are + // difficult to test the correctness of, or discover the purpose of. Because of this, they've + // either been commented out here, or not included at all. + PhysicalKey::Code(match scancode { + 0 => return PhysicalKey::Unidentified(NativeKeyCode::Xkb(0)), + 1 => KeyCode::Escape, + 2 => KeyCode::Digit1, + 3 => KeyCode::Digit2, + 4 => KeyCode::Digit3, + 5 => KeyCode::Digit4, + 6 => KeyCode::Digit5, + 7 => KeyCode::Digit6, + 8 => KeyCode::Digit7, + 9 => KeyCode::Digit8, + 10 => KeyCode::Digit9, + 11 => KeyCode::Digit0, + 12 => KeyCode::Minus, + 13 => KeyCode::Equal, + 14 => KeyCode::Backspace, + 15 => KeyCode::Tab, + 16 => KeyCode::KeyQ, + 17 => KeyCode::KeyW, + 18 => KeyCode::KeyE, + 19 => KeyCode::KeyR, + 20 => KeyCode::KeyT, + 21 => KeyCode::KeyY, + 22 => KeyCode::KeyU, + 23 => KeyCode::KeyI, + 24 => KeyCode::KeyO, + 25 => KeyCode::KeyP, + 26 => KeyCode::BracketLeft, + 27 => KeyCode::BracketRight, + 28 => KeyCode::Enter, + 29 => KeyCode::ControlLeft, + 30 => KeyCode::KeyA, + 31 => KeyCode::KeyS, + 32 => KeyCode::KeyD, + 33 => KeyCode::KeyF, + 34 => KeyCode::KeyG, + 35 => KeyCode::KeyH, + 36 => KeyCode::KeyJ, + 37 => KeyCode::KeyK, + 38 => KeyCode::KeyL, + 39 => KeyCode::Semicolon, + 40 => KeyCode::Quote, + 41 => KeyCode::Backquote, + 42 => KeyCode::ShiftLeft, + 43 => KeyCode::Backslash, + 44 => KeyCode::KeyZ, + 45 => KeyCode::KeyX, + 46 => KeyCode::KeyC, + 47 => KeyCode::KeyV, + 48 => KeyCode::KeyB, + 49 => KeyCode::KeyN, + 50 => KeyCode::KeyM, + 51 => KeyCode::Comma, + 52 => KeyCode::Period, + 53 => KeyCode::Slash, + 54 => KeyCode::ShiftRight, + 55 => KeyCode::NumpadMultiply, + 56 => KeyCode::AltLeft, + 57 => KeyCode::Space, + 58 => KeyCode::CapsLock, + 59 => KeyCode::F1, + 60 => KeyCode::F2, + 61 => KeyCode::F3, + 62 => KeyCode::F4, + 63 => KeyCode::F5, + 64 => KeyCode::F6, + 65 => KeyCode::F7, + 66 => KeyCode::F8, + 67 => KeyCode::F9, + 68 => KeyCode::F10, + 69 => KeyCode::NumLock, + 70 => KeyCode::ScrollLock, + 71 => KeyCode::Numpad7, + 72 => KeyCode::Numpad8, + 73 => KeyCode::Numpad9, + 74 => KeyCode::NumpadSubtract, + 75 => KeyCode::Numpad4, + 76 => KeyCode::Numpad5, + 77 => KeyCode::Numpad6, + 78 => KeyCode::NumpadAdd, + 79 => KeyCode::Numpad1, + 80 => KeyCode::Numpad2, + 81 => KeyCode::Numpad3, + 82 => KeyCode::Numpad0, + 83 => KeyCode::NumpadDecimal, + 85 => KeyCode::Lang5, + 86 => KeyCode::IntlBackslash, + 87 => KeyCode::F11, + 88 => KeyCode::F12, + 89 => KeyCode::IntlRo, + 90 => KeyCode::Lang3, + 91 => KeyCode::Lang4, + 92 => KeyCode::Convert, + 93 => KeyCode::KanaMode, + 94 => KeyCode::NonConvert, + // 95 => KeyCode::KPJPCOMMA, + 96 => KeyCode::NumpadEnter, + 97 => KeyCode::ControlRight, + 98 => KeyCode::NumpadDivide, + 99 => KeyCode::PrintScreen, + 100 => KeyCode::AltRight, + // 101 => KeyCode::LINEFEED, + 102 => KeyCode::Home, + 103 => KeyCode::ArrowUp, + 104 => KeyCode::PageUp, + 105 => KeyCode::ArrowLeft, + 106 => KeyCode::ArrowRight, + 107 => KeyCode::End, + 108 => KeyCode::ArrowDown, + 109 => KeyCode::PageDown, + 110 => KeyCode::Insert, + 111 => KeyCode::Delete, + // 112 => KeyCode::MACRO, + 113 => KeyCode::AudioVolumeMute, + 114 => KeyCode::AudioVolumeDown, + 115 => KeyCode::AudioVolumeUp, + // 116 => KeyCode::POWER, + 117 => KeyCode::NumpadEqual, + // 118 => KeyCode::KPPLUSMINUS, + 119 => KeyCode::Pause, + // 120 => KeyCode::SCALE, + 121 => KeyCode::NumpadComma, + 122 => KeyCode::Lang1, + 123 => KeyCode::Lang2, + 124 => KeyCode::IntlYen, + 125 => KeyCode::SuperLeft, + 126 => KeyCode::SuperRight, + 127 => KeyCode::ContextMenu, + // 128 => KeyCode::STOP, + // 129 => KeyCode::AGAIN, + // 130 => KeyCode::PROPS, + // 131 => KeyCode::UNDO, + // 132 => KeyCode::FRONT, + // 133 => KeyCode::COPY, + // 134 => KeyCode::OPEN, + // 135 => KeyCode::PASTE, + // 136 => KeyCode::FIND, + // 137 => KeyCode::CUT, + // 138 => KeyCode::HELP, + // 139 => KeyCode::MENU, + // 140 => KeyCode::CALC, + // 141 => KeyCode::SETUP, + // 142 => KeyCode::SLEEP, + // 143 => KeyCode::WAKEUP, + // 144 => KeyCode::FILE, + // 145 => KeyCode::SENDFILE, + // 146 => KeyCode::DELETEFILE, + // 147 => KeyCode::XFER, + // 148 => KeyCode::PROG1, + // 149 => KeyCode::PROG2, + // 150 => KeyCode::WWW, + // 151 => KeyCode::MSDOS, + // 152 => KeyCode::COFFEE, + // 153 => KeyCode::ROTATE_DISPLAY, + // 154 => KeyCode::CYCLEWINDOWS, + // 155 => KeyCode::MAIL, + // 156 => KeyCode::BOOKMARKS, + // 157 => KeyCode::COMPUTER, + // 158 => KeyCode::BACK, + // 159 => KeyCode::FORWARD, + // 160 => KeyCode::CLOSECD, + // 161 => KeyCode::EJECTCD, + // 162 => KeyCode::EJECTCLOSECD, + 163 => KeyCode::MediaTrackNext, + 164 => KeyCode::MediaPlayPause, + 165 => KeyCode::MediaTrackPrevious, + 166 => KeyCode::MediaStop, + // 167 => KeyCode::RECORD, + // 168 => KeyCode::REWIND, + // 169 => KeyCode::PHONE, + // 170 => KeyCode::ISO, + // 171 => KeyCode::CONFIG, + // 172 => KeyCode::HOMEPAGE, + // 173 => KeyCode::REFRESH, + // 174 => KeyCode::EXIT, + // 175 => KeyCode::MOVE, + // 176 => KeyCode::EDIT, + // 177 => KeyCode::SCROLLUP, + // 178 => KeyCode::SCROLLDOWN, + // 179 => KeyCode::KPLEFTPAREN, + // 180 => KeyCode::KPRIGHTPAREN, + // 181 => KeyCode::NEW, + // 182 => KeyCode::REDO, + 183 => KeyCode::F13, + 184 => KeyCode::F14, + 185 => KeyCode::F15, + 186 => KeyCode::F16, + 187 => KeyCode::F17, + 188 => KeyCode::F18, + 189 => KeyCode::F19, + 190 => KeyCode::F20, + 191 => KeyCode::F21, + 192 => KeyCode::F22, + 193 => KeyCode::F23, + 194 => KeyCode::F24, + // 200 => KeyCode::PLAYCD, + // 201 => KeyCode::PAUSECD, + // 202 => KeyCode::PROG3, + // 203 => KeyCode::PROG4, + // 204 => KeyCode::DASHBOARD, + // 205 => KeyCode::SUSPEND, + // 206 => KeyCode::CLOSE, + // 207 => KeyCode::PLAY, + // 208 => KeyCode::FASTFORWARD, + // 209 => KeyCode::BASSBOOST, + // 210 => KeyCode::PRINT, + // 211 => KeyCode::HP, + // 212 => KeyCode::CAMERA, + // 213 => KeyCode::SOUND, + // 214 => KeyCode::QUESTION, + // 215 => KeyCode::EMAIL, + // 216 => KeyCode::CHAT, + // 217 => KeyCode::SEARCH, + // 218 => KeyCode::CONNECT, + // 219 => KeyCode::FINANCE, + // 220 => KeyCode::SPORT, + // 221 => KeyCode::SHOP, + // 222 => KeyCode::ALTERASE, + // 223 => KeyCode::CANCEL, + // 224 => KeyCode::BRIGHTNESSDOW, + // 225 => KeyCode::BRIGHTNESSU, + // 226 => KeyCode::MEDIA, + // 227 => KeyCode::SWITCHVIDEOMODE, + // 228 => KeyCode::KBDILLUMTOGGLE, + // 229 => KeyCode::KBDILLUMDOWN, + // 230 => KeyCode::KBDILLUMUP, + // 231 => KeyCode::SEND, + // 232 => KeyCode::REPLY, + // 233 => KeyCode::FORWARDMAIL, + // 234 => KeyCode::SAVE, + // 235 => KeyCode::DOCUMENTS, + // 236 => KeyCode::BATTERY, + // 237 => KeyCode::BLUETOOTH, + // 238 => KeyCode::WLAN, + // 239 => KeyCode::UWB, + 240 => return PhysicalKey::Unidentified(NativeKeyCode::Unidentified), + // 241 => KeyCode::VIDEO_NEXT, + // 242 => KeyCode::VIDEO_PREV, + // 243 => KeyCode::BRIGHTNESS_CYCLE, + // 244 => KeyCode::BRIGHTNESS_AUTO, + // 245 => KeyCode::DISPLAY_OFF, + // 246 => KeyCode::WWAN, + // 247 => KeyCode::RFKILL, + // 248 => KeyCode::KEY_MICMUTE, + _ => return PhysicalKey::Unidentified(NativeKeyCode::Xkb(scancode)), + }) +} + +pub fn physicalkey_to_scancode(key: PhysicalKey) -> Option { + let code = match key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(code) => { + return match code { + NativeKeyCode::Unidentified => Some(240), + NativeKeyCode::Xkb(raw) => Some(raw), + _ => None, + }; + } + }; + + match code { + KeyCode::Escape => Some(1), + KeyCode::Digit1 => Some(2), + KeyCode::Digit2 => Some(3), + KeyCode::Digit3 => Some(4), + KeyCode::Digit4 => Some(5), + KeyCode::Digit5 => Some(6), + KeyCode::Digit6 => Some(7), + KeyCode::Digit7 => Some(8), + KeyCode::Digit8 => Some(9), + KeyCode::Digit9 => Some(10), + KeyCode::Digit0 => Some(11), + KeyCode::Minus => Some(12), + KeyCode::Equal => Some(13), + KeyCode::Backspace => Some(14), + KeyCode::Tab => Some(15), + KeyCode::KeyQ => Some(16), + KeyCode::KeyW => Some(17), + KeyCode::KeyE => Some(18), + KeyCode::KeyR => Some(19), + KeyCode::KeyT => Some(20), + KeyCode::KeyY => Some(21), + KeyCode::KeyU => Some(22), + KeyCode::KeyI => Some(23), + KeyCode::KeyO => Some(24), + KeyCode::KeyP => Some(25), + KeyCode::BracketLeft => Some(26), + KeyCode::BracketRight => Some(27), + KeyCode::Enter => Some(28), + KeyCode::ControlLeft => Some(29), + KeyCode::KeyA => Some(30), + KeyCode::KeyS => Some(31), + KeyCode::KeyD => Some(32), + KeyCode::KeyF => Some(33), + KeyCode::KeyG => Some(34), + KeyCode::KeyH => Some(35), + KeyCode::KeyJ => Some(36), + KeyCode::KeyK => Some(37), + KeyCode::KeyL => Some(38), + KeyCode::Semicolon => Some(39), + KeyCode::Quote => Some(40), + KeyCode::Backquote => Some(41), + KeyCode::ShiftLeft => Some(42), + KeyCode::Backslash => Some(43), + KeyCode::KeyZ => Some(44), + KeyCode::KeyX => Some(45), + KeyCode::KeyC => Some(46), + KeyCode::KeyV => Some(47), + KeyCode::KeyB => Some(48), + KeyCode::KeyN => Some(49), + KeyCode::KeyM => Some(50), + KeyCode::Comma => Some(51), + KeyCode::Period => Some(52), + KeyCode::Slash => Some(53), + KeyCode::ShiftRight => Some(54), + KeyCode::NumpadMultiply => Some(55), + KeyCode::AltLeft => Some(56), + KeyCode::Space => Some(57), + KeyCode::CapsLock => Some(58), + KeyCode::F1 => Some(59), + KeyCode::F2 => Some(60), + KeyCode::F3 => Some(61), + KeyCode::F4 => Some(62), + KeyCode::F5 => Some(63), + KeyCode::F6 => Some(64), + KeyCode::F7 => Some(65), + KeyCode::F8 => Some(66), + KeyCode::F9 => Some(67), + KeyCode::F10 => Some(68), + KeyCode::NumLock => Some(69), + KeyCode::ScrollLock => Some(70), + KeyCode::Numpad7 => Some(71), + KeyCode::Numpad8 => Some(72), + KeyCode::Numpad9 => Some(73), + KeyCode::NumpadSubtract => Some(74), + KeyCode::Numpad4 => Some(75), + KeyCode::Numpad5 => Some(76), + KeyCode::Numpad6 => Some(77), + KeyCode::NumpadAdd => Some(78), + KeyCode::Numpad1 => Some(79), + KeyCode::Numpad2 => Some(80), + KeyCode::Numpad3 => Some(81), + KeyCode::Numpad0 => Some(82), + KeyCode::NumpadDecimal => Some(83), + KeyCode::Lang5 => Some(85), + KeyCode::IntlBackslash => Some(86), + KeyCode::F11 => Some(87), + KeyCode::F12 => Some(88), + KeyCode::IntlRo => Some(89), + KeyCode::Lang3 => Some(90), + KeyCode::Lang4 => Some(91), + KeyCode::Convert => Some(92), + KeyCode::KanaMode => Some(93), + KeyCode::NonConvert => Some(94), + KeyCode::NumpadEnter => Some(96), + KeyCode::ControlRight => Some(97), + KeyCode::NumpadDivide => Some(98), + KeyCode::PrintScreen => Some(99), + KeyCode::AltRight => Some(100), + KeyCode::Home => Some(102), + KeyCode::ArrowUp => Some(103), + KeyCode::PageUp => Some(104), + KeyCode::ArrowLeft => Some(105), + KeyCode::ArrowRight => Some(106), + KeyCode::End => Some(107), + KeyCode::ArrowDown => Some(108), + KeyCode::PageDown => Some(109), + KeyCode::Insert => Some(110), + KeyCode::Delete => Some(111), + KeyCode::AudioVolumeMute => Some(113), + KeyCode::AudioVolumeDown => Some(114), + KeyCode::AudioVolumeUp => Some(115), + KeyCode::NumpadEqual => Some(117), + KeyCode::Pause => Some(119), + KeyCode::NumpadComma => Some(121), + KeyCode::Lang1 => Some(122), + KeyCode::Lang2 => Some(123), + KeyCode::IntlYen => Some(124), + KeyCode::SuperLeft => Some(125), + KeyCode::SuperRight => Some(126), + KeyCode::ContextMenu => Some(127), + KeyCode::MediaTrackNext => Some(163), + KeyCode::MediaPlayPause => Some(164), + KeyCode::MediaTrackPrevious => Some(165), + KeyCode::MediaStop => Some(166), + KeyCode::F13 => Some(183), + KeyCode::F14 => Some(184), + KeyCode::F15 => Some(185), + KeyCode::F16 => Some(186), + KeyCode::F17 => Some(187), + KeyCode::F18 => Some(188), + KeyCode::F19 => Some(189), + KeyCode::F20 => Some(190), + KeyCode::F21 => Some(191), + KeyCode::F22 => Some(192), + KeyCode::F23 => Some(193), + KeyCode::F24 => Some(194), + _ => None, + } +} + +pub fn keysym_to_key(keysym: u32) -> Key { + use xkbcommon_dl::keysyms; + Key::Named(match keysym { + // TTY function keys + keysyms::BackSpace => NamedKey::Backspace, + keysyms::Tab => NamedKey::Tab, + // keysyms::Linefeed => NamedKey::Linefeed, + keysyms::Clear => NamedKey::Clear, + keysyms::Return => NamedKey::Enter, + keysyms::Pause => NamedKey::Pause, + keysyms::Scroll_Lock => NamedKey::ScrollLock, + keysyms::Sys_Req => NamedKey::PrintScreen, + keysyms::Escape => NamedKey::Escape, + keysyms::Delete => NamedKey::Delete, + + // IME keys + keysyms::Multi_key => NamedKey::Compose, + keysyms::Codeinput => NamedKey::CodeInput, + keysyms::SingleCandidate => NamedKey::SingleCandidate, + keysyms::MultipleCandidate => NamedKey::AllCandidates, + keysyms::PreviousCandidate => NamedKey::PreviousCandidate, + + // Japanese keys + keysyms::Kanji => NamedKey::KanjiMode, + keysyms::Muhenkan => NamedKey::NonConvert, + keysyms::Henkan_Mode => NamedKey::Convert, + keysyms::Romaji => NamedKey::Romaji, + keysyms::Hiragana => NamedKey::Hiragana, + keysyms::Hiragana_Katakana => NamedKey::HiraganaKatakana, + keysyms::Zenkaku => NamedKey::Zenkaku, + keysyms::Hankaku => NamedKey::Hankaku, + keysyms::Zenkaku_Hankaku => NamedKey::ZenkakuHankaku, + // keysyms::Touroku => NamedKey::Touroku, + // keysyms::Massyo => NamedKey::Massyo, + keysyms::Kana_Lock => NamedKey::KanaMode, + keysyms::Kana_Shift => NamedKey::KanaMode, + keysyms::Eisu_Shift => NamedKey::Alphanumeric, + keysyms::Eisu_toggle => NamedKey::Alphanumeric, + // NOTE: The next three items are aliases for values we've already mapped. + // keysyms::Kanji_Bangou => NamedKey::CodeInput, + // keysyms::Zen_Koho => NamedKey::AllCandidates, + // keysyms::Mae_Koho => NamedKey::PreviousCandidate, + + // Cursor control & motion + keysyms::Home => NamedKey::Home, + keysyms::Left => NamedKey::ArrowLeft, + keysyms::Up => NamedKey::ArrowUp, + keysyms::Right => NamedKey::ArrowRight, + keysyms::Down => NamedKey::ArrowDown, + // keysyms::Prior => NamedKey::PageUp, + keysyms::Page_Up => NamedKey::PageUp, + // keysyms::Next => NamedKey::PageDown, + keysyms::Page_Down => NamedKey::PageDown, + keysyms::End => NamedKey::End, + // keysyms::Begin => NamedKey::Begin, + + // Misc. functions + keysyms::Select => NamedKey::Select, + keysyms::Print => NamedKey::PrintScreen, + keysyms::Execute => NamedKey::Execute, + keysyms::Insert => NamedKey::Insert, + keysyms::Undo => NamedKey::Undo, + keysyms::Redo => NamedKey::Redo, + keysyms::Menu => NamedKey::ContextMenu, + keysyms::Find => NamedKey::Find, + keysyms::Cancel => NamedKey::Cancel, + keysyms::Help => NamedKey::Help, + keysyms::Break => NamedKey::Pause, + keysyms::Mode_switch => NamedKey::ModeChange, + // keysyms::script_switch => NamedKey::ModeChange, + keysyms::Num_Lock => NamedKey::NumLock, + + // Keypad keys + // keysyms::KP_Space => return Key::Character(" "), + keysyms::KP_Tab => NamedKey::Tab, + keysyms::KP_Enter => NamedKey::Enter, + keysyms::KP_F1 => NamedKey::F1, + keysyms::KP_F2 => NamedKey::F2, + keysyms::KP_F3 => NamedKey::F3, + keysyms::KP_F4 => NamedKey::F4, + keysyms::KP_Home => NamedKey::Home, + keysyms::KP_Left => NamedKey::ArrowLeft, + keysyms::KP_Up => NamedKey::ArrowUp, + keysyms::KP_Right => NamedKey::ArrowRight, + keysyms::KP_Down => NamedKey::ArrowDown, + // keysyms::KP_Prior => NamedKey::PageUp, + keysyms::KP_Page_Up => NamedKey::PageUp, + // keysyms::KP_Next => NamedKey::PageDown, + keysyms::KP_Page_Down => NamedKey::PageDown, + keysyms::KP_End => NamedKey::End, + // This is the key labeled "5" on the numpad when NumLock is off. + // keysyms::KP_Begin => NamedKey::Begin, + keysyms::KP_Insert => NamedKey::Insert, + keysyms::KP_Delete => NamedKey::Delete, + // keysyms::KP_Equal => NamedKey::Equal, + // keysyms::KP_Multiply => NamedKey::Multiply, + // keysyms::KP_Add => NamedKey::Add, + // keysyms::KP_Separator => NamedKey::Separator, + // keysyms::KP_Subtract => NamedKey::Subtract, + // keysyms::KP_Decimal => NamedKey::Decimal, + // keysyms::KP_Divide => NamedKey::Divide, + + // keysyms::KP_0 => return Key::Character("0"), + // keysyms::KP_1 => return Key::Character("1"), + // keysyms::KP_2 => return Key::Character("2"), + // keysyms::KP_3 => return Key::Character("3"), + // keysyms::KP_4 => return Key::Character("4"), + // keysyms::KP_5 => return Key::Character("5"), + // keysyms::KP_6 => return Key::Character("6"), + // keysyms::KP_7 => return Key::Character("7"), + // keysyms::KP_8 => return Key::Character("8"), + // keysyms::KP_9 => return Key::Character("9"), + + // Function keys + keysyms::F1 => NamedKey::F1, + keysyms::F2 => NamedKey::F2, + keysyms::F3 => NamedKey::F3, + keysyms::F4 => NamedKey::F4, + keysyms::F5 => NamedKey::F5, + keysyms::F6 => NamedKey::F6, + keysyms::F7 => NamedKey::F7, + keysyms::F8 => NamedKey::F8, + keysyms::F9 => NamedKey::F9, + keysyms::F10 => NamedKey::F10, + keysyms::F11 => NamedKey::F11, + keysyms::F12 => NamedKey::F12, + keysyms::F13 => NamedKey::F13, + keysyms::F14 => NamedKey::F14, + keysyms::F15 => NamedKey::F15, + keysyms::F16 => NamedKey::F16, + keysyms::F17 => NamedKey::F17, + keysyms::F18 => NamedKey::F18, + keysyms::F19 => NamedKey::F19, + keysyms::F20 => NamedKey::F20, + keysyms::F21 => NamedKey::F21, + keysyms::F22 => NamedKey::F22, + keysyms::F23 => NamedKey::F23, + keysyms::F24 => NamedKey::F24, + keysyms::F25 => NamedKey::F25, + keysyms::F26 => NamedKey::F26, + keysyms::F27 => NamedKey::F27, + keysyms::F28 => NamedKey::F28, + keysyms::F29 => NamedKey::F29, + keysyms::F30 => NamedKey::F30, + keysyms::F31 => NamedKey::F31, + keysyms::F32 => NamedKey::F32, + keysyms::F33 => NamedKey::F33, + keysyms::F34 => NamedKey::F34, + keysyms::F35 => NamedKey::F35, + + // Modifiers + keysyms::Shift_L => NamedKey::Shift, + keysyms::Shift_R => NamedKey::Shift, + keysyms::Control_L => NamedKey::Control, + keysyms::Control_R => NamedKey::Control, + keysyms::Caps_Lock => NamedKey::CapsLock, + // keysyms::Shift_Lock => NamedKey::ShiftLock, + + // keysyms::Meta_L => NamedKey::Meta, + // keysyms::Meta_R => NamedKey::Meta, + keysyms::Alt_L => NamedKey::Alt, + keysyms::Alt_R => NamedKey::Alt, + keysyms::Super_L => NamedKey::Super, + keysyms::Super_R => NamedKey::Super, + keysyms::Hyper_L => NamedKey::Hyper, + keysyms::Hyper_R => NamedKey::Hyper, + + // XKB function and modifier keys + // keysyms::ISO_Lock => NamedKey::IsoLock, + // keysyms::ISO_Level2_Latch => NamedKey::IsoLevel2Latch, + keysyms::ISO_Level3_Shift => NamedKey::AltGraph, + keysyms::ISO_Level3_Latch => NamedKey::AltGraph, + keysyms::ISO_Level3_Lock => NamedKey::AltGraph, + // keysyms::ISO_Level5_Shift => NamedKey::IsoLevel5Shift, + // keysyms::ISO_Level5_Latch => NamedKey::IsoLevel5Latch, + // keysyms::ISO_Level5_Lock => NamedKey::IsoLevel5Lock, + // keysyms::ISO_Group_Shift => NamedKey::IsoGroupShift, + // keysyms::ISO_Group_Latch => NamedKey::IsoGroupLatch, + // keysyms::ISO_Group_Lock => NamedKey::IsoGroupLock, + keysyms::ISO_Next_Group => NamedKey::GroupNext, + // keysyms::ISO_Next_Group_Lock => NamedKey::GroupNextLock, + keysyms::ISO_Prev_Group => NamedKey::GroupPrevious, + // keysyms::ISO_Prev_Group_Lock => NamedKey::GroupPreviousLock, + keysyms::ISO_First_Group => NamedKey::GroupFirst, + // keysyms::ISO_First_Group_Lock => NamedKey::GroupFirstLock, + keysyms::ISO_Last_Group => NamedKey::GroupLast, + // keysyms::ISO_Last_Group_Lock => NamedKey::GroupLastLock, + keysyms::ISO_Left_Tab => NamedKey::Tab, + // keysyms::ISO_Move_Line_Up => NamedKey::IsoMoveLineUp, + // keysyms::ISO_Move_Line_Down => NamedKey::IsoMoveLineDown, + // keysyms::ISO_Partial_Line_Up => NamedKey::IsoPartialLineUp, + // keysyms::ISO_Partial_Line_Down => NamedKey::IsoPartialLineDown, + // keysyms::ISO_Partial_Space_Left => NamedKey::IsoPartialSpaceLeft, + // keysyms::ISO_Partial_Space_Right => NamedKey::IsoPartialSpaceRight, + // keysyms::ISO_Set_Margin_Left => NamedKey::IsoSetMarginLeft, + // keysyms::ISO_Set_Margin_Right => NamedKey::IsoSetMarginRight, + // keysyms::ISO_Release_Margin_Left => NamedKey::IsoReleaseMarginLeft, + // keysyms::ISO_Release_Margin_Right => NamedKey::IsoReleaseMarginRight, + // keysyms::ISO_Release_Both_Margins => NamedKey::IsoReleaseBothMargins, + // keysyms::ISO_Fast_Cursor_Left => NamedKey::IsoFastCursorLeft, + // keysyms::ISO_Fast_Cursor_Right => NamedKey::IsoFastCursorRight, + // keysyms::ISO_Fast_Cursor_Up => NamedKey::IsoFastCursorUp, + // keysyms::ISO_Fast_Cursor_Down => NamedKey::IsoFastCursorDown, + // keysyms::ISO_Continuous_Underline => NamedKey::IsoContinuousUnderline, + // keysyms::ISO_Discontinuous_Underline => NamedKey::IsoDiscontinuousUnderline, + // keysyms::ISO_Emphasize => NamedKey::IsoEmphasize, + // keysyms::ISO_Center_Object => NamedKey::IsoCenterObject, + keysyms::ISO_Enter => NamedKey::Enter, + + // dead_grave..dead_currency + + // dead_lowline..dead_longsolidusoverlay + + // dead_a..dead_capital_schwa + + // dead_greek + + // First_Virtual_Screen..Terminate_Server + + // AccessX_Enable..AudibleBell_Enable + + // Pointer_Left..Pointer_Drag5 + + // Pointer_EnableKeys..Pointer_DfltBtnPrev + + // ch..C_H + + // 3270 terminal keys + // keysyms::3270_Duplicate => NamedKey::Duplicate, + // keysyms::3270_FieldMark => NamedKey::FieldMark, + // keysyms::3270_Right2 => NamedKey::Right2, + // keysyms::3270_Left2 => NamedKey::Left2, + // keysyms::3270_BackTab => NamedKey::BackTab, + keysyms::_3270_EraseEOF => NamedKey::EraseEof, + // keysyms::3270_EraseInput => NamedKey::EraseInput, + // keysyms::3270_Reset => NamedKey::Reset, + // keysyms::3270_Quit => NamedKey::Quit, + // keysyms::3270_PA1 => NamedKey::Pa1, + // keysyms::3270_PA2 => NamedKey::Pa2, + // keysyms::3270_PA3 => NamedKey::Pa3, + // keysyms::3270_Test => NamedKey::Test, + keysyms::_3270_Attn => NamedKey::Attn, + // keysyms::3270_CursorBlink => NamedKey::CursorBlink, + // keysyms::3270_AltCursor => NamedKey::AltCursor, + // keysyms::3270_KeyClick => NamedKey::KeyClick, + // keysyms::3270_Jump => NamedKey::Jump, + // keysyms::3270_Ident => NamedKey::Ident, + // keysyms::3270_Rule => NamedKey::Rule, + // keysyms::3270_Copy => NamedKey::Copy, + keysyms::_3270_Play => NamedKey::Play, + // keysyms::3270_Setup => NamedKey::Setup, + // keysyms::3270_Record => NamedKey::Record, + // keysyms::3270_ChangeScreen => NamedKey::ChangeScreen, + // keysyms::3270_DeleteWord => NamedKey::DeleteWord, + keysyms::_3270_ExSelect => NamedKey::ExSel, + keysyms::_3270_CursorSelect => NamedKey::CrSel, + keysyms::_3270_PrintScreen => NamedKey::PrintScreen, + keysyms::_3270_Enter => NamedKey::Enter, + + keysyms::space => NamedKey::Space, + // exclam..Sinh_kunddaliya + + // XFree86 + // keysyms::XF86_ModeLock => NamedKey::ModeLock, + + // XFree86 - Backlight controls + keysyms::XF86_MonBrightnessUp => NamedKey::BrightnessUp, + keysyms::XF86_MonBrightnessDown => NamedKey::BrightnessDown, + // keysyms::XF86_KbdLightOnOff => NamedKey::LightOnOff, + // keysyms::XF86_KbdBrightnessUp => NamedKey::KeyboardBrightnessUp, + // keysyms::XF86_KbdBrightnessDown => NamedKey::KeyboardBrightnessDown, + + // XFree86 - "Internet" + keysyms::XF86_Standby => NamedKey::Standby, + keysyms::XF86_AudioLowerVolume => NamedKey::AudioVolumeDown, + keysyms::XF86_AudioRaiseVolume => NamedKey::AudioVolumeUp, + keysyms::XF86_AudioPlay => NamedKey::MediaPlay, + keysyms::XF86_AudioStop => NamedKey::MediaStop, + keysyms::XF86_AudioPrev => NamedKey::MediaTrackPrevious, + keysyms::XF86_AudioNext => NamedKey::MediaTrackNext, + keysyms::XF86_HomePage => NamedKey::BrowserHome, + keysyms::XF86_Mail => NamedKey::LaunchMail, + // keysyms::XF86_Start => NamedKey::Start, + keysyms::XF86_Search => NamedKey::BrowserSearch, + keysyms::XF86_AudioRecord => NamedKey::MediaRecord, + + // XFree86 - PDA + keysyms::XF86_Calculator => NamedKey::LaunchApplication2, + // keysyms::XF86_Memo => NamedKey::Memo, + // keysyms::XF86_ToDoList => NamedKey::ToDoList, + keysyms::XF86_Calendar => NamedKey::LaunchCalendar, + keysyms::XF86_PowerDown => NamedKey::Power, + // keysyms::XF86_ContrastAdjust => NamedKey::AdjustContrast, + // keysyms::XF86_RockerUp => NamedKey::RockerUp, + // keysyms::XF86_RockerDown => NamedKey::RockerDown, + // keysyms::XF86_RockerEnter => NamedKey::RockerEnter, + + // XFree86 - More "Internet" + keysyms::XF86_Back => NamedKey::BrowserBack, + keysyms::XF86_Forward => NamedKey::BrowserForward, + // keysyms::XF86_Stop => NamedKey::Stop, + keysyms::XF86_Refresh => NamedKey::BrowserRefresh, + keysyms::XF86_PowerOff => NamedKey::Power, + keysyms::XF86_WakeUp => NamedKey::WakeUp, + keysyms::XF86_Eject => NamedKey::Eject, + keysyms::XF86_ScreenSaver => NamedKey::LaunchScreenSaver, + keysyms::XF86_WWW => NamedKey::LaunchWebBrowser, + keysyms::XF86_Sleep => NamedKey::Standby, + keysyms::XF86_Favorites => NamedKey::BrowserFavorites, + keysyms::XF86_AudioPause => NamedKey::MediaPause, + // keysyms::XF86_AudioMedia => NamedKey::AudioMedia, + keysyms::XF86_MyComputer => NamedKey::LaunchApplication1, + // keysyms::XF86_VendorHome => NamedKey::VendorHome, + // keysyms::XF86_LightBulb => NamedKey::LightBulb, + // keysyms::XF86_Shop => NamedKey::BrowserShop, + // keysyms::XF86_History => NamedKey::BrowserHistory, + // keysyms::XF86_OpenURL => NamedKey::OpenUrl, + // keysyms::XF86_AddFavorite => NamedKey::AddFavorite, + // keysyms::XF86_HotLinks => NamedKey::HotLinks, + // keysyms::XF86_BrightnessAdjust => NamedKey::BrightnessAdjust, + // keysyms::XF86_Finance => NamedKey::BrowserFinance, + // keysyms::XF86_Community => NamedKey::BrowserCommunity, + keysyms::XF86_AudioRewind => NamedKey::MediaRewind, + // keysyms::XF86_BackForward => Key::???, + // XF86_Launch0..XF86_LaunchF + + // XF86_ApplicationLeft..XF86_CD + keysyms::XF86_Calculater => NamedKey::LaunchApplication2, // Nice typo, libxkbcommon :) + // XF86_Clear + keysyms::XF86_Close => NamedKey::Close, + keysyms::XF86_Copy => NamedKey::Copy, + keysyms::XF86_Cut => NamedKey::Cut, + // XF86_Display..XF86_Documents + keysyms::XF86_Excel => NamedKey::LaunchSpreadsheet, + // XF86_Explorer..XF86iTouch + keysyms::XF86_LogOff => NamedKey::LogOff, + // XF86_Market..XF86_MenuPB + keysyms::XF86_MySites => NamedKey::BrowserFavorites, + keysyms::XF86_New => NamedKey::New, + // XF86_News..XF86_OfficeHome + keysyms::XF86_Open => NamedKey::Open, + // XF86_Option + keysyms::XF86_Paste => NamedKey::Paste, + keysyms::XF86_Phone => NamedKey::LaunchPhone, + // XF86_Q + keysyms::XF86_Reply => NamedKey::MailReply, + keysyms::XF86_Reload => NamedKey::BrowserRefresh, + // XF86_RotateWindows..XF86_RotationKB + keysyms::XF86_Save => NamedKey::Save, + // XF86_ScrollUp..XF86_ScrollClick + keysyms::XF86_Send => NamedKey::MailSend, + keysyms::XF86_Spell => NamedKey::SpellCheck, + keysyms::XF86_SplitScreen => NamedKey::SplitScreenToggle, + // XF86_Support..XF86_User2KB + keysyms::XF86_Video => NamedKey::LaunchMediaPlayer, + // XF86_WheelButton + keysyms::XF86_Word => NamedKey::LaunchWordProcessor, + // XF86_Xfer + keysyms::XF86_ZoomIn => NamedKey::ZoomIn, + keysyms::XF86_ZoomOut => NamedKey::ZoomOut, + + // XF86_Away..XF86_Messenger + keysyms::XF86_WebCam => NamedKey::LaunchWebCam, + keysyms::XF86_MailForward => NamedKey::MailForward, + // XF86_Pictures + keysyms::XF86_Music => NamedKey::LaunchMusicPlayer, + + // XF86_Battery..XF86_UWB + keysyms::XF86_AudioForward => NamedKey::MediaFastForward, + // XF86_AudioRepeat + keysyms::XF86_AudioRandomPlay => NamedKey::RandomToggle, + keysyms::XF86_Subtitle => NamedKey::Subtitle, + keysyms::XF86_AudioCycleTrack => NamedKey::MediaAudioTrack, + // XF86_CycleAngle..XF86_Blue + keysyms::XF86_Suspend => NamedKey::Standby, + keysyms::XF86_Hibernate => NamedKey::Hibernate, + // XF86_TouchpadToggle..XF86_TouchpadOff + keysyms::XF86_AudioMute => NamedKey::AudioVolumeMute, + + // XF86_Switch_VT_1..XF86_Switch_VT_12 + + // XF86_Ungrab..XF86_ClearGrab + keysyms::XF86_Next_VMode => NamedKey::VideoModeNext, + // keysyms::XF86_Prev_VMode => NamedKey::VideoModePrevious, + // XF86_LogWindowTree..XF86_LogGrabInfo + + // SunFA_Grave..SunFA_Cedilla + + // keysyms::SunF36 => NamedKey::F36 | NamedKey::F11, + // keysyms::SunF37 => NamedKey::F37 | NamedKey::F12, + + // keysyms::SunSys_Req => NamedKey::PrintScreen, + // The next couple of xkb (until SunStop) are already handled. + // SunPrint_Screen..SunPageDown + + // SunUndo..SunFront + keysyms::SUN_Copy => NamedKey::Copy, + keysyms::SUN_Open => NamedKey::Open, + keysyms::SUN_Paste => NamedKey::Paste, + keysyms::SUN_Cut => NamedKey::Cut, + + // SunPowerSwitch + keysyms::SUN_AudioLowerVolume => NamedKey::AudioVolumeDown, + keysyms::SUN_AudioMute => NamedKey::AudioVolumeMute, + keysyms::SUN_AudioRaiseVolume => NamedKey::AudioVolumeUp, + // SUN_VideoDegauss + keysyms::SUN_VideoLowerBrightness => NamedKey::BrightnessDown, + keysyms::SUN_VideoRaiseBrightness => NamedKey::BrightnessUp, + // SunPowerSwitchShift + 0 => return Key::Unidentified(NativeKey::Unidentified), + _ => return Key::Unidentified(NativeKey::Xkb(keysym)), + }) +} + +pub fn keysym_location(keysym: u32) -> KeyLocation { + use xkbcommon_dl::keysyms; + match keysym { + keysyms::Shift_L + | keysyms::Control_L + | keysyms::Meta_L + | keysyms::Alt_L + | keysyms::Super_L + | keysyms::Hyper_L => KeyLocation::Left, + keysyms::Shift_R + | keysyms::Control_R + | keysyms::Meta_R + | keysyms::Alt_R + | keysyms::Super_R + | keysyms::Hyper_R => KeyLocation::Right, + keysyms::KP_0 + | keysyms::KP_1 + | keysyms::KP_2 + | keysyms::KP_3 + | keysyms::KP_4 + | keysyms::KP_5 + | keysyms::KP_6 + | keysyms::KP_7 + | keysyms::KP_8 + | keysyms::KP_9 + | keysyms::KP_Space + | keysyms::KP_Tab + | keysyms::KP_Enter + | keysyms::KP_F1 + | keysyms::KP_F2 + | keysyms::KP_F3 + | keysyms::KP_F4 + | keysyms::KP_Home + | keysyms::KP_Left + | keysyms::KP_Up + | keysyms::KP_Right + | keysyms::KP_Down + | keysyms::KP_Page_Up + | keysyms::KP_Page_Down + | keysyms::KP_End + | keysyms::KP_Begin + | keysyms::KP_Insert + | keysyms::KP_Delete + | keysyms::KP_Equal + | keysyms::KP_Multiply + | keysyms::KP_Add + | keysyms::KP_Separator + | keysyms::KP_Subtract + | keysyms::KP_Decimal + | keysyms::KP_Divide => KeyLocation::Numpad, + _ => KeyLocation::Standard, + } +} + +#[derive(Debug)] +pub struct XkbKeymap { + keymap: NonNull, + _mods_indices: ModsIndices, + pub _core_keyboard_id: i32, +} + +impl XkbKeymap { + #[cfg(wayland_platform)] + pub fn from_fd(context: &XkbContext, fd: OwnedFd, size: usize) -> Option { + let map = unsafe { MmapOptions::new().len(size).map_copy_read_only(&fd).ok()? }; + + let keymap = unsafe { + let keymap = (XKBH.xkb_keymap_new_from_string)( + (*context).as_ptr(), + map.as_ptr() as *const _, + xkb::xkb_keymap_format::XKB_KEYMAP_FORMAT_TEXT_V1, + xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS, + ); + NonNull::new(keymap)? + }; + + Some(Self::new_inner(keymap, 0)) + } + + #[cfg(x11_platform)] + pub fn from_x11_keymap( + context: &XkbContext, + xcb: *mut xcb_connection_t, + core_keyboard_id: i32, + ) -> Option { + let keymap = unsafe { + (XKBXH.xkb_x11_keymap_new_from_device)( + context.as_ptr(), + xcb, + core_keyboard_id, + xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS, + ) + }; + let keymap = NonNull::new(keymap)?; + Some(Self::new_inner(keymap, core_keyboard_id)) + } + + fn new_inner(keymap: NonNull, _core_keyboard_id: i32) -> Self { + let mods_indices = ModsIndices { + shift: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_SHIFT), + caps: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_CAPS), + ctrl: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_CTRL), + alt: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_ALT), + num: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_NUM), + mod3: mod_index_for_name(keymap, b"Mod3\0"), + logo: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_LOGO), + mod5: mod_index_for_name(keymap, b"Mod5\0"), + }; + + Self { + keymap, + _mods_indices: mods_indices, + _core_keyboard_id, + } + } + + #[cfg(x11_platform)] + pub fn mods_indices(&self) -> ModsIndices { + self._mods_indices + } + + pub fn first_keysym_by_level( + &mut self, + layout: xkb_layout_index_t, + keycode: xkb_keycode_t, + ) -> xkb_keysym_t { + unsafe { + let mut keysyms = ptr::null(); + let count = (XKBH.xkb_keymap_key_get_syms_by_level)( + self.keymap.as_ptr(), + keycode, + layout, + // NOTE: The level should be zero to ignore modifiers. + 0, + &mut keysyms, + ); + + if count == 1 { + *keysyms + } else { + 0 + } + } + } + + /// Check whether the given key repeats. + pub fn key_repeats(&mut self, keycode: xkb_keycode_t) -> bool { + unsafe { (XKBH.xkb_keymap_key_repeats)(self.keymap.as_ptr(), keycode) == 1 } + } +} + +impl Drop for XkbKeymap { + fn drop(&mut self) { + unsafe { + (XKBH.xkb_keymap_unref)(self.keymap.as_ptr()); + }; + } +} + +impl Deref for XkbKeymap { + type Target = NonNull; + + fn deref(&self) -> &Self::Target { + &self.keymap + } +} + +/// Modifier index in the keymap. +#[cfg_attr(not(x11_platform), allow(dead_code))] +#[derive(Default, Debug, Clone, Copy)] +pub struct ModsIndices { + pub shift: Option, + pub caps: Option, + pub ctrl: Option, + pub alt: Option, + pub num: Option, + pub mod3: Option, + pub logo: Option, + pub mod5: Option, +} + +fn mod_index_for_name( + keymap: NonNull, + name: &[u8], +) -> Option { + unsafe { + let mod_index = (XKBH.xkb_keymap_mod_get_index)( + keymap.as_ptr(), + name.as_ptr() as *const c_char, + ); + if mod_index == XKB_MOD_INVALID { + None + } else { + Some(mod_index) + } + } +} diff --git a/rio-window/src/platform_impl/linux/common/xkb/mod.rs b/rio-window/src/platform_impl/linux/common/xkb/mod.rs new file mode 100644 index 00000000..ddb5c20b --- /dev/null +++ b/rio-window/src/platform_impl/linux/common/xkb/mod.rs @@ -0,0 +1,480 @@ +use std::ops::Deref; +use std::os::raw::c_char; +use std::ptr::{self, NonNull}; +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::utils::Lazy; +use smol_str::SmolStr; +#[cfg(wayland_platform)] +use std::os::unix::io::OwnedFd; +use tracing::warn; +use xkbcommon_dl::{ + self as xkb, xkb_compose_status, xkb_context, xkb_context_flags, + xkbcommon_compose_handle, xkbcommon_handle, XkbCommon, XkbCommonCompose, +}; +#[cfg(x11_platform)] +use {x11_dl::xlib_xcb::xcb_connection_t, xkbcommon_dl::x11::xkbcommon_x11_handle}; + +use crate::event::{ElementState, KeyEvent}; +use crate::keyboard::{Key, KeyLocation}; +use crate::platform_impl::KeyEventExtra; + +mod compose; +mod keymap; +mod state; + +use compose::{ComposeStatus, XkbComposeState, XkbComposeTable}; +use keymap::XkbKeymap; + +#[cfg(x11_platform)] +pub use keymap::raw_keycode_to_physicalkey; +pub use keymap::{physicalkey_to_scancode, scancode_to_physicalkey}; +pub use state::XkbState; + +// TODO: Wire this up without using a static `AtomicBool`. +static RESET_DEAD_KEYS: AtomicBool = AtomicBool::new(false); + +static XKBH: Lazy<&'static XkbCommon> = Lazy::new(xkbcommon_handle); +static XKBCH: Lazy<&'static XkbCommonCompose> = Lazy::new(xkbcommon_compose_handle); +#[cfg(feature = "x11")] +static XKBXH: Lazy<&'static xkb::x11::XkbCommonX11> = Lazy::new(xkbcommon_x11_handle); + +#[inline(always)] +pub fn reset_dead_keys() { + RESET_DEAD_KEYS.store(true, Ordering::SeqCst); +} + +#[derive(Debug)] +pub enum Error { + /// libxkbcommon is not available + XKBNotFound, +} + +#[derive(Debug)] +pub struct Context { + // NOTE: field order matters. + #[cfg(x11_platform)] + pub core_keyboard_id: i32, + state: Option, + keymap: Option, + compose_state1: Option, + compose_state2: Option, + _compose_table: Option, + context: XkbContext, + scratch_buffer: Vec, +} + +impl Context { + pub fn new() -> Result { + if xkb::xkbcommon_option().is_none() { + return Err(Error::XKBNotFound); + } + + let context = XkbContext::new()?; + let mut compose_table = XkbComposeTable::new(&context); + let mut compose_state1 = + compose_table.as_ref().and_then(|table| table.new_state()); + let mut compose_state2 = + compose_table.as_ref().and_then(|table| table.new_state()); + + // Disable compose if anything compose related failed to initialize. + if compose_table.is_none() || compose_state1.is_none() || compose_state2.is_none() + { + compose_state2 = None; + compose_state1 = None; + compose_table = None; + } + + Ok(Self { + state: None, + keymap: None, + compose_state1, + compose_state2, + #[cfg(x11_platform)] + core_keyboard_id: 0, + _compose_table: compose_table, + context, + scratch_buffer: Vec::with_capacity(8), + }) + } + + #[cfg(feature = "x11")] + pub fn from_x11_xkb(xcb: *mut xcb_connection_t) -> Result { + let result = unsafe { + (XKBXH.xkb_x11_setup_xkb_extension)( + xcb, + 1, + 2, + xkbcommon_dl::x11::xkb_x11_setup_xkb_extension_flags::XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + + if result != 1 { + return Err(Error::XKBNotFound); + } + + let mut this = Self::new()?; + this.core_keyboard_id = + unsafe { (XKBXH.xkb_x11_get_core_keyboard_device_id)(xcb) }; + this.set_keymap_from_x11(xcb); + Ok(this) + } + + pub fn state_mut(&mut self) -> Option<&mut XkbState> { + self.state.as_mut() + } + + pub fn keymap_mut(&mut self) -> Option<&mut XkbKeymap> { + self.keymap.as_mut() + } + + #[cfg(wayland_platform)] + pub fn set_keymap_from_fd(&mut self, fd: OwnedFd, size: usize) { + let keymap = XkbKeymap::from_fd(&self.context, fd, size); + let state = keymap.as_ref().and_then(XkbState::new_wayland); + if keymap.is_none() || state.is_none() { + warn!("failed to update xkb keymap"); + } + self.state = state; + self.keymap = keymap; + } + + #[cfg(x11_platform)] + pub fn set_keymap_from_x11(&mut self, xcb: *mut xcb_connection_t) { + let keymap = + XkbKeymap::from_x11_keymap(&self.context, xcb, self.core_keyboard_id); + let state = keymap + .as_ref() + .and_then(|keymap| XkbState::new_x11(xcb, keymap)); + if keymap.is_none() || state.is_none() { + warn!("failed to update xkb keymap"); + } + self.state = state; + self.keymap = keymap; + } + + /// Key builder context with the user provided xkb state. + pub fn key_context(&mut self) -> Option> { + let state = self.state.as_mut()?; + let keymap = self.keymap.as_mut()?; + let compose_state1 = self.compose_state1.as_mut(); + let compose_state2 = self.compose_state2.as_mut(); + let scratch_buffer = &mut self.scratch_buffer; + Some(KeyContext { + state, + keymap, + compose_state1, + compose_state2, + scratch_buffer, + }) + } + + /// Key builder context with the user provided xkb state. + /// + /// Should be used when the original context must not be altered. + #[cfg(x11_platform)] + pub fn key_context_with_state<'a>( + &'a mut self, + state: &'a mut XkbState, + ) -> Option> { + let keymap = self.keymap.as_mut()?; + let compose_state1 = self.compose_state1.as_mut(); + let compose_state2 = self.compose_state2.as_mut(); + let scratch_buffer = &mut self.scratch_buffer; + Some(KeyContext { + state, + keymap, + compose_state1, + compose_state2, + scratch_buffer, + }) + } +} + +pub struct KeyContext<'a> { + pub state: &'a mut XkbState, + pub keymap: &'a mut XkbKeymap, + compose_state1: Option<&'a mut XkbComposeState>, + compose_state2: Option<&'a mut XkbComposeState>, + scratch_buffer: &'a mut Vec, +} + +impl<'a> KeyContext<'a> { + pub fn process_key_event( + &mut self, + keycode: u32, + state: ElementState, + repeat: bool, + ) -> KeyEvent { + let mut event = KeyEventResults::new( + self, + keycode, + !repeat && state == ElementState::Pressed, + ); + let physical_key = keymap::raw_keycode_to_physicalkey(keycode); + let (logical_key, location) = event.key(); + let text = event.text(); + let (key_without_modifiers, _) = event.key_without_modifiers(); + let text_with_all_modifiers = event.text_with_all_modifiers(); + + let platform_specific = KeyEventExtra { + text_with_all_modifiers, + key_without_modifiers, + }; + + KeyEvent { + physical_key, + logical_key, + text, + location, + state, + repeat, + platform_specific, + } + } + + fn keysym_to_utf8_raw(&mut self, keysym: u32) -> Option { + self.scratch_buffer.clear(); + self.scratch_buffer.reserve(8); + loop { + let bytes_written = unsafe { + (XKBH.xkb_keysym_to_utf8)( + keysym, + self.scratch_buffer.as_mut_ptr().cast(), + self.scratch_buffer.capacity(), + ) + }; + if bytes_written == 0 { + return None; + } else if bytes_written == -1 { + self.scratch_buffer.reserve(8); + } else { + unsafe { + self.scratch_buffer + .set_len(bytes_written.try_into().unwrap()) + }; + break; + } + } + + // Remove the null-terminator + self.scratch_buffer.pop(); + byte_slice_to_smol_str(self.scratch_buffer) + } +} + +struct KeyEventResults<'a, 'b> { + context: &'a mut KeyContext<'b>, + keycode: u32, + keysym: u32, + compose: ComposeStatus, +} + +impl<'a, 'b> KeyEventResults<'a, 'b> { + fn new(context: &'a mut KeyContext<'b>, keycode: u32, compose: bool) -> Self { + let keysym = context.state.get_one_sym_raw(keycode); + + let compose = + if let Some(state) = context.compose_state1.as_mut().filter(|_| compose) { + if RESET_DEAD_KEYS.swap(false, Ordering::SeqCst) { + state.reset(); + context.compose_state2.as_mut().unwrap().reset(); + } + state.feed(keysym) + } else { + ComposeStatus::None + }; + + KeyEventResults { + context, + keycode, + keysym, + compose, + } + } + + pub fn key(&mut self) -> (Key, KeyLocation) { + let (key, location) = match self.keysym_to_key(self.keysym) { + Ok(known) => return known, + Err(undefined) => undefined, + }; + + if let ComposeStatus::Accepted(xkb_compose_status::XKB_COMPOSE_COMPOSING) = + self.compose + { + let compose_state = self.context.compose_state2.as_mut().unwrap(); + // When pressing a dead key twice, the non-combining variant of that character will + // be produced. Since this function only concerns itself with a single keypress, we + // simulate this double press here by feeding the keysym to the compose state + // twice. + + compose_state.feed(self.keysym); + if matches!(compose_state.feed(self.keysym), ComposeStatus::Accepted(_)) { + // Extracting only a single `char` here *should* be fine, assuming that no + // dead key's non-combining variant ever occupies more than one `char`. + let text = compose_state.get_string(self.context.scratch_buffer); + let key = Key::Dead(text.and_then(|s| s.chars().next())); + (key, location) + } else { + (key, location) + } + } else { + let key = self + .composed_text() + .unwrap_or_else(|_| self.context.keysym_to_utf8_raw(self.keysym)) + .map(Key::Character) + .unwrap_or(key); + (key, location) + } + } + + pub fn key_without_modifiers(&mut self) -> (Key, KeyLocation) { + // This will become a pointer to an array which libxkbcommon owns, so we don't need to + // deallocate it. + let layout = self.context.state.layout(self.keycode); + let keysym = self + .context + .keymap + .first_keysym_by_level(layout, self.keycode); + + match self.keysym_to_key(keysym) { + Ok((key, location)) => (key, location), + Err((key, location)) => { + let key = self + .context + .keysym_to_utf8_raw(keysym) + .map(Key::Character) + .unwrap_or(key); + (key, location) + } + } + } + + fn keysym_to_key( + &self, + keysym: u32, + ) -> Result<(Key, KeyLocation), (Key, KeyLocation)> { + let location = keymap::keysym_location(keysym); + let key = keymap::keysym_to_key(keysym); + if matches!(key, Key::Unidentified(_)) { + Err((key, location)) + } else { + Ok((key, location)) + } + } + + pub fn text(&mut self) -> Option { + self.composed_text() + .unwrap_or_else(|_| self.context.keysym_to_utf8_raw(self.keysym)) + } + + // The current behaviour makes it so composing a character overrides attempts to input a + // control character with the `Ctrl` key. We can potentially add a configuration option + // if someone specifically wants the oppsite behaviour. + pub fn text_with_all_modifiers(&mut self) -> Option { + match self.composed_text() { + Ok(text) => text, + Err(_) => self + .context + .state + .get_utf8_raw(self.keycode, self.context.scratch_buffer), + } + } + + fn composed_text(&mut self) -> Result, ()> { + match self.compose { + ComposeStatus::Accepted(status) => match status { + xkb_compose_status::XKB_COMPOSE_COMPOSED => { + let state = self.context.compose_state1.as_mut().unwrap(); + Ok(state.get_string(self.context.scratch_buffer)) + } + xkb_compose_status::XKB_COMPOSE_COMPOSING + | xkb_compose_status::XKB_COMPOSE_CANCELLED => Ok(None), + xkb_compose_status::XKB_COMPOSE_NOTHING => Err(()), + }, + _ => Err(()), + } + } +} + +#[derive(Debug)] +pub struct XkbContext { + context: NonNull, +} + +impl XkbContext { + pub fn new() -> Result { + let context = + unsafe { (XKBH.xkb_context_new)(xkb_context_flags::XKB_CONTEXT_NO_FLAGS) }; + + let context = match NonNull::new(context) { + Some(context) => context, + None => return Err(Error::XKBNotFound), + }; + + Ok(Self { context }) + } +} + +impl Drop for XkbContext { + fn drop(&mut self) { + unsafe { + (XKBH.xkb_context_unref)(self.context.as_ptr()); + } + } +} + +impl Deref for XkbContext { + type Target = NonNull; + + fn deref(&self) -> &Self::Target { + &self.context + } +} + +/// Shared logic for constructing a string with `xkb_compose_state_get_utf8` and +/// `xkb_state_key_get_utf8`. +fn make_string_with(scratch_buffer: &mut Vec, mut f: F) -> Option +where + F: FnMut(*mut c_char, usize) -> i32, +{ + let size = f(ptr::null_mut(), 0); + if size == 0 { + return None; + } + let size = usize::try_from(size).unwrap(); + scratch_buffer.clear(); + // The allocated buffer must include space for the null-terminator. + scratch_buffer.reserve(size + 1); + unsafe { + let written = f( + scratch_buffer.as_mut_ptr().cast(), + scratch_buffer.capacity(), + ); + if usize::try_from(written).unwrap() != size { + // This will likely never happen. + return None; + } + scratch_buffer.set_len(size); + }; + + byte_slice_to_smol_str(scratch_buffer) +} + +// NOTE: This is track_caller so we can have more informative line numbers when logging +#[track_caller] +fn byte_slice_to_smol_str(bytes: &[u8]) -> Option { + std::str::from_utf8(bytes) + .map(SmolStr::new) + .map_err(|e| { + tracing::warn!( + "UTF-8 received from libxkbcommon ({:?}) was invalid: {e}", + bytes + ) + }) + .ok() +} diff --git a/rio-window/src/platform_impl/linux/common/xkb/state.rs b/rio-window/src/platform_impl/linux/common/xkb/state.rs new file mode 100644 index 00000000..146abad5 --- /dev/null +++ b/rio-window/src/platform_impl/linux/common/xkb/state.rs @@ -0,0 +1,194 @@ +//! XKB state. + +use std::os::raw::c_char; +use std::ptr::NonNull; + +use smol_str::SmolStr; +#[cfg(x11_platform)] +use x11_dl::xlib_xcb::xcb_connection_t; +use xkbcommon_dl::{ + self as xkb, xkb_keycode_t, xkb_keysym_t, xkb_layout_index_t, xkb_state, + xkb_state_component, +}; + +use crate::platform_impl::common::xkb::keymap::XkbKeymap; +#[cfg(x11_platform)] +use crate::platform_impl::common::xkb::XKBXH; +use crate::platform_impl::common::xkb::{make_string_with, XKBH}; + +#[derive(Debug)] +pub struct XkbState { + state: NonNull, + modifiers: ModifiersState, +} + +impl XkbState { + #[cfg(wayland_platform)] + pub fn new_wayland(keymap: &XkbKeymap) -> Option { + let state = NonNull::new(unsafe { (XKBH.xkb_state_new)(keymap.as_ptr()) })?; + Some(Self::new_inner(state)) + } + + #[cfg(x11_platform)] + pub fn new_x11(xcb: *mut xcb_connection_t, keymap: &XkbKeymap) -> Option { + let state = unsafe { + (XKBXH.xkb_x11_state_new_from_device)( + keymap.as_ptr(), + xcb, + keymap._core_keyboard_id, + ) + }; + let state = NonNull::new(state)?; + Some(Self::new_inner(state)) + } + + fn new_inner(state: NonNull) -> Self { + let modifiers = ModifiersState::default(); + let mut this = Self { state, modifiers }; + this.reload_modifiers(); + this + } + + pub fn get_one_sym_raw(&mut self, keycode: xkb_keycode_t) -> xkb_keysym_t { + unsafe { (XKBH.xkb_state_key_get_one_sym)(self.state.as_ptr(), keycode) } + } + + pub fn layout(&mut self, key: xkb_keycode_t) -> xkb_layout_index_t { + unsafe { (XKBH.xkb_state_key_get_layout)(self.state.as_ptr(), key) } + } + + #[cfg(x11_platform)] + pub fn depressed_modifiers(&mut self) -> xkb::xkb_mod_mask_t { + unsafe { + (XKBH.xkb_state_serialize_mods)( + self.state.as_ptr(), + xkb_state_component::XKB_STATE_MODS_DEPRESSED, + ) + } + } + + #[cfg(x11_platform)] + pub fn latched_modifiers(&mut self) -> xkb::xkb_mod_mask_t { + unsafe { + (XKBH.xkb_state_serialize_mods)( + self.state.as_ptr(), + xkb_state_component::XKB_STATE_MODS_LATCHED, + ) + } + } + + #[cfg(x11_platform)] + pub fn locked_modifiers(&mut self) -> xkb::xkb_mod_mask_t { + unsafe { + (XKBH.xkb_state_serialize_mods)( + self.state.as_ptr(), + xkb_state_component::XKB_STATE_MODS_LOCKED, + ) + } + } + + pub fn get_utf8_raw( + &mut self, + keycode: xkb_keycode_t, + scratch_buffer: &mut Vec, + ) -> Option { + make_string_with(scratch_buffer, |ptr, len| unsafe { + (XKBH.xkb_state_key_get_utf8)(self.state.as_ptr(), keycode, ptr, len) + }) + } + + pub fn modifiers(&self) -> ModifiersState { + self.modifiers + } + + pub fn update_modifiers( + &mut self, + mods_depressed: u32, + mods_latched: u32, + mods_locked: u32, + depressed_group: u32, + latched_group: u32, + locked_group: u32, + ) { + let mask = unsafe { + (XKBH.xkb_state_update_mask)( + self.state.as_ptr(), + mods_depressed, + mods_latched, + mods_locked, + depressed_group, + latched_group, + locked_group, + ) + }; + + if mask.contains(xkb_state_component::XKB_STATE_MODS_EFFECTIVE) { + // Effective value of mods have changed, we need to update our state. + self.reload_modifiers(); + } + } + + /// Reload the modifiers. + fn reload_modifiers(&mut self) { + self.modifiers.ctrl = self.mod_name_is_active(xkb::XKB_MOD_NAME_CTRL); + self.modifiers.alt = self.mod_name_is_active(xkb::XKB_MOD_NAME_ALT); + self.modifiers.shift = self.mod_name_is_active(xkb::XKB_MOD_NAME_SHIFT); + self.modifiers.caps_lock = self.mod_name_is_active(xkb::XKB_MOD_NAME_CAPS); + self.modifiers.logo = self.mod_name_is_active(xkb::XKB_MOD_NAME_LOGO); + self.modifiers.num_lock = self.mod_name_is_active(xkb::XKB_MOD_NAME_NUM); + } + + /// Check if the modifier is active within xkb. + fn mod_name_is_active(&mut self, name: &[u8]) -> bool { + unsafe { + (XKBH.xkb_state_mod_name_is_active)( + self.state.as_ptr(), + name.as_ptr() as *const c_char, + xkb_state_component::XKB_STATE_MODS_EFFECTIVE, + ) > 0 + } + } +} + +impl Drop for XkbState { + fn drop(&mut self) { + unsafe { + (XKBH.xkb_state_unref)(self.state.as_ptr()); + } + } +} + +/// Represents the current state of the keyboard modifiers +/// +/// Each field of this struct represents a modifier and is `true` if this modifier is active. +/// +/// For some modifiers, this means that the key is currently pressed, others are toggled +/// (like caps lock). +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct ModifiersState { + /// The "control" key + pub ctrl: bool, + /// The "alt" key + pub alt: bool, + /// The "shift" key + pub shift: bool, + /// The "Caps lock" key + pub caps_lock: bool, + /// The "logo" key + /// + /// Also known as the "windows" key on most keyboards + pub logo: bool, + /// The "Num lock" key + pub num_lock: bool, +} + +impl From for crate::keyboard::ModifiersState { + fn from(mods: ModifiersState) -> crate::keyboard::ModifiersState { + let mut to_mods = crate::keyboard::ModifiersState::empty(); + to_mods.set(crate::keyboard::ModifiersState::SHIFT, mods.shift); + to_mods.set(crate::keyboard::ModifiersState::CONTROL, mods.ctrl); + to_mods.set(crate::keyboard::ModifiersState::ALT, mods.alt); + to_mods.set(crate::keyboard::ModifiersState::SUPER, mods.logo); + to_mods + } +} diff --git a/rio-window/src/platform_impl/linux/mod.rs b/rio-window/src/platform_impl/linux/mod.rs new file mode 100644 index 00000000..d2c41c7f --- /dev/null +++ b/rio-window/src/platform_impl/linux/mod.rs @@ -0,0 +1,1015 @@ +#![cfg(free_unix)] + +#[cfg(all(not(x11_platform), not(wayland_platform)))] +compile_error!("Please select a feature to build for unix: `x11`, `wayland`"); + +use std::collections::VecDeque; +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; +use std::sync::Arc; +use std::time::Duration; +use std::{env, fmt}; +#[cfg(x11_platform)] +use std::{ffi::CStr, mem::MaybeUninit, os::raw::*, sync::Mutex}; + +#[cfg(x11_platform)] +use crate::utils::Lazy; +use smol_str::SmolStr; + +#[cfg(x11_platform)] +use self::x11::{X11Error, XConnection, XError, XNotSupported}; +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ + EventLoopError, ExternalError, NotSupportedError, OsError as RootOsError, +}; +use crate::event_loop::{ + ActiveEventLoop as RootELW, AsyncRequestSerial, ControlFlow, DeviceEvents, + EventLoopClosed, +}; +use crate::icon::Icon; +use crate::keyboard::Key; +use crate::platform::pump_events::PumpStatus; +#[cfg(x11_platform)] +use crate::platform::x11::{WindowType as XWindowType, XlibErrorHook}; +use crate::window::{ + ActivationToken, Cursor, CursorGrabMode, CustomCursor, CustomCursorSource, + ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowButtons, WindowLevel, +}; + +pub(crate) use self::common::xkb::{physicalkey_to_scancode, scancode_to_physicalkey}; +pub(crate) use crate::cursor::OnlyCursorImageSource as PlatformCustomCursorSource; +pub(crate) use crate::icon::RgbaIcon as PlatformIcon; +pub(crate) use crate::platform_impl::Fullscreen; + +pub(crate) mod common; +#[cfg(wayland_platform)] +pub(crate) mod wayland; +#[cfg(x11_platform)] +pub(crate) mod x11; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) enum Backend { + #[cfg(x11_platform)] + X, + #[cfg(wayland_platform)] + Wayland, +} + +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes { + pub(crate) forced_backend: Option, + pub(crate) any_thread: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ApplicationName { + pub general: String, + pub instance: String, +} + +impl ApplicationName { + pub fn new(general: String, instance: String) -> Self { + Self { general, instance } + } +} + +#[derive(Clone, Debug)] +pub struct PlatformSpecificWindowAttributes { + pub name: Option, + pub activation_token: Option, + #[cfg(x11_platform)] + pub x11: X11WindowAttributes, +} + +#[derive(Clone, Debug)] +#[cfg(x11_platform)] +pub struct X11WindowAttributes { + pub visual_id: Option, + pub screen_id: Option, + pub base_size: Option, + pub override_redirect: bool, + pub x11_window_types: Vec, + + /// The parent window to embed this window into. + pub embed_window: Option, +} + +#[cfg_attr(not(x11_platform), allow(clippy::derivable_impls))] +impl Default for PlatformSpecificWindowAttributes { + fn default() -> Self { + Self { + name: None, + activation_token: None, + #[cfg(x11_platform)] + x11: X11WindowAttributes { + visual_id: None, + screen_id: None, + base_size: None, + override_redirect: false, + x11_window_types: vec![XWindowType::Normal], + embed_window: None, + }, + } + } +} + +#[cfg(x11_platform)] +pub(crate) static X11_BACKEND: Lazy, XNotSupported>>> = + Lazy::new(|| Mutex::new(XConnection::new(Some(x_error_callback)).map(Arc::new))); + +#[derive(Debug, Clone)] +pub enum OsError { + Misc(&'static str), + #[cfg(x11_platform)] + XNotSupported(XNotSupported), + #[cfg(x11_platform)] + XError(Arc), + #[cfg(wayland_platform)] + WaylandError(Arc), +} + +impl fmt::Display for OsError { + fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match *self { + OsError::Misc(e) => _f.pad(e), + #[cfg(x11_platform)] + OsError::XNotSupported(ref e) => fmt::Display::fmt(e, _f), + #[cfg(x11_platform)] + OsError::XError(ref e) => fmt::Display::fmt(e, _f), + #[cfg(wayland_platform)] + OsError::WaylandError(ref e) => fmt::Display::fmt(e, _f), + } + } +} + +pub(crate) enum Window { + #[cfg(x11_platform)] + X(x11::Window), + #[cfg(wayland_platform)] + Wayland(wayland::Window), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(u64); + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id) + } +} + +impl WindowId { + pub const unsafe fn dummy() -> Self { + Self(0) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DeviceId { + #[cfg(x11_platform)] + X(x11::DeviceId), + #[cfg(wayland_platform)] + Wayland(wayland::DeviceId), +} + +impl DeviceId { + pub const unsafe fn dummy() -> Self { + #[cfg(wayland_platform)] + return DeviceId::Wayland(unsafe { wayland::DeviceId::dummy() }); + #[cfg(all(not(wayland_platform), x11_platform))] + return DeviceId::X(unsafe { x11::DeviceId::dummy() }); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum MonitorHandle { + #[cfg(x11_platform)] + X(x11::MonitorHandle), + #[cfg(wayland_platform)] + Wayland(wayland::MonitorHandle), +} + +/// `x11_or_wayland!(match expr; Enum(foo) => foo.something())` +/// expands to the equivalent of +/// ```ignore +/// match self { +/// Enum::X(foo) => foo.something(), +/// Enum::Wayland(foo) => foo.something(), +/// } +/// ``` +/// The result can be converted to another enum by adding `; as AnotherEnum` +macro_rules! x11_or_wayland { + (match $what:expr; $enum:ident ( $($c1:tt)* ) => $x:expr; as $enum2:ident ) => { + match $what { + #[cfg(x11_platform)] + $enum::X($($c1)*) => $enum2::X($x), + #[cfg(wayland_platform)] + $enum::Wayland($($c1)*) => $enum2::Wayland($x), + } + }; + (match $what:expr; $enum:ident ( $($c1:tt)* ) => $x:expr) => { + match $what { + #[cfg(x11_platform)] + $enum::X($($c1)*) => $x, + #[cfg(wayland_platform)] + $enum::Wayland($($c1)*) => $x, + } + }; +} + +impl MonitorHandle { + #[inline] + pub fn name(&self) -> Option { + x11_or_wayland!(match self; MonitorHandle(m) => m.name()) + } + + #[inline] + pub fn native_identifier(&self) -> u32 { + x11_or_wayland!(match self; MonitorHandle(m) => m.native_identifier()) + } + + #[inline] + pub fn size(&self) -> PhysicalSize { + x11_or_wayland!(match self; MonitorHandle(m) => m.size()) + } + + #[inline] + pub fn position(&self) -> PhysicalPosition { + x11_or_wayland!(match self; MonitorHandle(m) => m.position()) + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + x11_or_wayland!(match self; MonitorHandle(m) => m.refresh_rate_millihertz()) + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + x11_or_wayland!(match self; MonitorHandle(m) => m.scale_factor() as _) + } + + #[inline] + pub fn video_modes(&self) -> Box> { + x11_or_wayland!(match self; MonitorHandle(m) => Box::new(m.video_modes())) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum VideoModeHandle { + #[cfg(x11_platform)] + X(x11::VideoModeHandle), + #[cfg(wayland_platform)] + Wayland(wayland::VideoModeHandle), +} + +impl VideoModeHandle { + #[inline] + pub fn size(&self) -> PhysicalSize { + x11_or_wayland!(match self; VideoModeHandle(m) => m.size()) + } + + #[inline] + pub fn bit_depth(&self) -> u16 { + x11_or_wayland!(match self; VideoModeHandle(m) => m.bit_depth()) + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> u32 { + x11_or_wayland!(match self; VideoModeHandle(m) => m.refresh_rate_millihertz()) + } + + #[inline] + pub fn monitor(&self) -> MonitorHandle { + x11_or_wayland!(match self; VideoModeHandle(m) => m.monitor(); as MonitorHandle) + } +} + +impl Window { + #[inline] + pub(crate) fn new( + window_target: &ActiveEventLoop, + attribs: WindowAttributes, + ) -> Result { + match *window_target { + #[cfg(wayland_platform)] + ActiveEventLoop::Wayland(ref window_target) => { + wayland::Window::new(window_target, attribs).map(Window::Wayland) + } + #[cfg(x11_platform)] + ActiveEventLoop::X(ref window_target) => { + x11::Window::new(window_target, attribs).map(Window::X) + } + } + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) { + f(self) + } + + pub(crate) fn maybe_wait_on_main( + &self, + f: impl FnOnce(&Self) -> R + Send, + ) -> R { + f(self) + } + + #[inline] + pub fn id(&self) -> WindowId { + x11_or_wayland!(match self; Window(w) => w.id()) + } + + #[inline] + pub fn set_title(&self, title: &str) { + x11_or_wayland!(match self; Window(w) => w.set_title(title)); + } + + #[inline] + pub fn set_transparent(&self, transparent: bool) { + x11_or_wayland!(match self; Window(w) => w.set_transparent(transparent)); + } + + #[inline] + pub fn set_blur(&self, blur: bool) { + x11_or_wayland!(match self; Window(w) => w.set_blur(blur)); + } + + #[inline] + pub fn set_visible(&self, visible: bool) { + x11_or_wayland!(match self; Window(w) => w.set_visible(visible)) + } + + #[inline] + pub fn is_visible(&self) -> Option { + x11_or_wayland!(match self; Window(w) => w.is_visible()) + } + + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + x11_or_wayland!(match self; Window(w) => w.outer_position()) + } + + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + x11_or_wayland!(match self; Window(w) => w.inner_position()) + } + + #[inline] + pub fn set_outer_position(&self, position: Position) { + x11_or_wayland!(match self; Window(w) => w.set_outer_position(position)) + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + x11_or_wayland!(match self; Window(w) => w.inner_size()) + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + x11_or_wayland!(match self; Window(w) => w.outer_size()) + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + x11_or_wayland!(match self; Window(w) => w.request_inner_size(size)) + } + + #[inline] + pub(crate) fn request_activation_token( + &self, + ) -> Result { + x11_or_wayland!(match self; Window(w) => w.request_activation_token()) + } + + #[inline] + pub fn set_min_inner_size(&self, dimensions: Option) { + x11_or_wayland!(match self; Window(w) => w.set_min_inner_size(dimensions)) + } + + #[inline] + pub fn set_max_inner_size(&self, dimensions: Option) { + x11_or_wayland!(match self; Window(w) => w.set_max_inner_size(dimensions)) + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + x11_or_wayland!(match self; Window(w) => w.resize_increments()) + } + + #[inline] + pub fn set_resize_increments(&self, increments: Option) { + x11_or_wayland!(match self; Window(w) => w.set_resize_increments(increments)) + } + + #[inline] + pub fn set_resizable(&self, resizable: bool) { + x11_or_wayland!(match self; Window(w) => w.set_resizable(resizable)) + } + + #[inline] + pub fn is_resizable(&self) -> bool { + x11_or_wayland!(match self; Window(w) => w.is_resizable()) + } + + #[inline] + pub fn set_enabled_buttons(&self, buttons: WindowButtons) { + x11_or_wayland!(match self; Window(w) => w.set_enabled_buttons(buttons)) + } + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + x11_or_wayland!(match self; Window(w) => w.enabled_buttons()) + } + + #[inline] + pub fn set_cursor(&self, cursor: Cursor) { + x11_or_wayland!(match self; Window(w) => w.set_cursor(cursor)) + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(window) => window.set_cursor_grab(mode)) + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + x11_or_wayland!(match self; Window(window) => window.set_cursor_visible(visible)) + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(window) => window.drag_window()) + } + + #[inline] + pub fn drag_resize_window( + &self, + direction: ResizeDirection, + ) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(window) => window.drag_resize_window(direction)) + } + + #[inline] + pub fn show_window_menu(&self, position: Position) { + x11_or_wayland!(match self; Window(w) => w.show_window_menu(position)) + } + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(w) => w.set_cursor_hittest(hittest)) + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + x11_or_wayland!(match self; Window(w) => w.scale_factor()) + } + + #[inline] + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(w) => w.set_cursor_position(position)) + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + x11_or_wayland!(match self; Window(w) => w.set_maximized(maximized)) + } + + #[inline] + pub fn is_maximized(&self) -> bool { + x11_or_wayland!(match self; Window(w) => w.is_maximized()) + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + x11_or_wayland!(match self; Window(w) => w.set_minimized(minimized)) + } + + #[inline] + pub fn is_minimized(&self) -> Option { + x11_or_wayland!(match self; Window(w) => w.is_minimized()) + } + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + x11_or_wayland!(match self; Window(w) => w.fullscreen()) + } + + #[inline] + pub(crate) fn set_fullscreen(&self, monitor: Option) { + x11_or_wayland!(match self; Window(w) => w.set_fullscreen(monitor)) + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + x11_or_wayland!(match self; Window(w) => w.set_decorations(decorations)) + } + + #[inline] + pub fn is_decorated(&self) -> bool { + x11_or_wayland!(match self; Window(w) => w.is_decorated()) + } + + #[inline] + pub fn set_window_level(&self, level: WindowLevel) { + x11_or_wayland!(match self; Window(w) => w.set_window_level(level)) + } + + #[inline] + pub fn set_window_icon(&self, window_icon: Option) { + x11_or_wayland!(match self; Window(w) => w.set_window_icon(window_icon.map(|icon| icon.inner))) + } + + #[inline] + pub fn set_ime_cursor_area(&self, position: Position, size: Size) { + x11_or_wayland!(match self; Window(w) => w.set_ime_cursor_area(position, size)) + } + + #[inline] + pub fn reset_dead_keys(&self) { + common::xkb::reset_dead_keys() + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + x11_or_wayland!(match self; Window(w) => w.set_ime_allowed(allowed)) + } + + #[inline] + pub fn set_ime_purpose(&self, purpose: ImePurpose) { + x11_or_wayland!(match self; Window(w) => w.set_ime_purpose(purpose)) + } + + #[inline] + pub fn focus_window(&self) { + x11_or_wayland!(match self; Window(w) => w.focus_window()) + } + + pub fn request_user_attention(&self, request_type: Option) { + x11_or_wayland!(match self; Window(w) => w.request_user_attention(request_type)) + } + + #[inline] + pub fn request_redraw(&self) { + x11_or_wayland!(match self; Window(w) => w.request_redraw()) + } + + #[inline] + pub fn pre_present_notify(&self) { + x11_or_wayland!(match self; Window(w) => w.pre_present_notify()) + } + + #[inline] + pub fn current_monitor(&self) -> Option { + Some( + x11_or_wayland!(match self; Window(w) => w.current_monitor()?; as MonitorHandle), + ) + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + match self { + #[cfg(x11_platform)] + Window::X(ref window) => window + .available_monitors() + .into_iter() + .map(MonitorHandle::X) + .collect(), + #[cfg(wayland_platform)] + Window::Wayland(ref window) => window + .available_monitors() + .into_iter() + .map(MonitorHandle::Wayland) + .collect(), + } + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + Some( + x11_or_wayland!(match self; Window(w) => w.primary_monitor()?; as MonitorHandle), + ) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06( + &self, + ) -> Result { + x11_or_wayland!(match self; Window(window) => window.raw_window_handle_rwh_06()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + x11_or_wayland!(match self; Window(window) => window.raw_display_handle_rwh_06()) + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + x11_or_wayland!(match self; Window(window) => window.set_theme(theme)) + } + + #[inline] + pub fn theme(&self) -> Option { + x11_or_wayland!(match self; Window(window) => window.theme()) + } + + pub fn set_content_protected(&self, protected: bool) { + x11_or_wayland!(match self; Window(window) => window.set_content_protected(protected)) + } + + #[inline] + pub fn has_focus(&self) -> bool { + x11_or_wayland!(match self; Window(window) => window.has_focus()) + } + + pub fn title(&self) -> String { + x11_or_wayland!(match self; Window(window) => window.title()) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifiers: Option, + pub key_without_modifiers: Key, +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub(crate) enum PlatformCustomCursor { + #[cfg(wayland_platform)] + Wayland(wayland::CustomCursor), + #[cfg(x11_platform)] + X(x11::CustomCursor), +} + +/// Hooks for X11 errors. +#[cfg(x11_platform)] +pub(crate) static mut XLIB_ERROR_HOOKS: Mutex> = + Mutex::new(Vec::new()); + +#[cfg(x11_platform)] +unsafe extern "C" fn x_error_callback( + display: *mut x11::ffi::Display, + event: *mut x11::ffi::XErrorEvent, +) -> c_int { + let xconn_lock = X11_BACKEND.lock().unwrap_or_else(|e| e.into_inner()); + if let Ok(ref xconn) = *xconn_lock { + // Call all the hooks. + let mut error_handled = false; + for hook in unsafe { XLIB_ERROR_HOOKS.lock() }.unwrap().iter() { + error_handled |= hook(display as *mut _, event as *mut _); + } + + // `assume_init` is safe here because the array consists of `MaybeUninit` values, + // which do not require initialization. + let mut buf: [MaybeUninit; 1024] = + unsafe { MaybeUninit::uninit().assume_init() }; + unsafe { + (xconn.xlib.XGetErrorText)( + display, + (*event).error_code as c_int, + buf.as_mut_ptr() as *mut c_char, + buf.len() as c_int, + ) + }; + let description = + unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }.to_string_lossy(); + + let error = unsafe { + XError { + description: description.into_owned(), + error_code: (*event).error_code, + request_code: (*event).request_code, + minor_code: (*event).minor_code, + } + }; + + // Don't log error. + if !error_handled { + tracing::error!("X11 error: {:#?}", error); + // XXX only update the error, if it wasn't handled by any of the hooks. + *xconn.latest_error.lock().unwrap() = Some(error); + } + } + // Fun fact: this return value is completely ignored. + 0 +} + +pub enum EventLoop { + #[cfg(wayland_platform)] + Wayland(Box>), + #[cfg(x11_platform)] + X(x11::EventLoop), +} + +pub enum EventLoopProxy { + #[cfg(x11_platform)] + X(x11::EventLoopProxy), + #[cfg(wayland_platform)] + Wayland(wayland::EventLoopProxy), +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + x11_or_wayland!(match self; EventLoopProxy(proxy) => proxy.clone(); as EventLoopProxy) + } +} + +impl EventLoop { + pub(crate) fn new( + attributes: &PlatformSpecificEventLoopAttributes, + ) -> Result { + if !attributes.any_thread && !is_main_thread() { + panic!( + "Initializing the event loop outside of the main thread is a significant \ + cross-platform compatibility hazard. If you absolutely need to create an \ + EventLoop on a different thread, you can use the \ + `EventLoopBuilderExtX11::any_thread` or `EventLoopBuilderExtWayland::any_thread` \ + functions." + ); + } + + // NOTE: Wayland first because of X11 could be present under Wayland as well. Empty + // variables are also treated as not set. + let backend = match ( + attributes.forced_backend, + env::var("WAYLAND_DISPLAY") + .ok() + .filter(|var| !var.is_empty()) + .or_else(|| env::var("WAYLAND_SOCKET").ok()) + .filter(|var| !var.is_empty()) + .is_some(), + env::var("DISPLAY") + .map(|var| !var.is_empty()) + .unwrap_or(false), + ) { + // User is forcing a backend. + (Some(backend), ..) => backend, + // Wayland is present. + #[cfg(wayland_platform)] + (None, true, _) => Backend::Wayland, + // X11 is present. + #[cfg(x11_platform)] + (None, _, true) => Backend::X, + // No backend is present. + (_, wayland_display, x11_display) => { + let msg = if wayland_display && !cfg!(wayland_platform) { + "DISPLAY is not set; note: enable the `winit/wayland` feature to support \ + Wayland" + } else if x11_display && !cfg!(x11_platform) { + "neither WAYLAND_DISPLAY nor WAYLAND_SOCKET is set; note: enable the \ + `winit/x11` feature to support X11" + } else { + "neither WAYLAND_DISPLAY nor WAYLAND_SOCKET nor DISPLAY is set." + }; + return Err(EventLoopError::Os(os_error!(OsError::Misc(msg)))); + } + }; + + // Create the display based on the backend. + match backend { + #[cfg(wayland_platform)] + Backend::Wayland => EventLoop::new_wayland_any_thread().map_err(Into::into), + #[cfg(x11_platform)] + Backend::X => EventLoop::new_x11_any_thread().map_err(Into::into), + } + } + + #[cfg(wayland_platform)] + fn new_wayland_any_thread() -> Result, EventLoopError> { + wayland::EventLoop::new().map(|evlp| EventLoop::Wayland(Box::new(evlp))) + } + + #[cfg(x11_platform)] + fn new_x11_any_thread() -> Result, EventLoopError> { + let xconn = match X11_BACKEND + .lock() + .unwrap_or_else(|e| e.into_inner()) + .as_ref() + { + Ok(xconn) => xconn.clone(), + Err(err) => { + return Err(EventLoopError::Os(os_error!(OsError::XNotSupported( + err.clone() + )))) + } + }; + + Ok(EventLoop::X(x11::EventLoop::new(xconn))) + } + + pub fn create_proxy(&self) -> EventLoopProxy { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.create_proxy(); as EventLoopProxy) + } + + pub fn run(mut self, callback: F) -> Result<(), EventLoopError> + where + F: FnMut(crate::event::Event, &RootELW), + { + self.run_on_demand(callback) + } + + pub fn run_on_demand(&mut self, callback: F) -> Result<(), EventLoopError> + where + F: FnMut(crate::event::Event, &RootELW), + { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.run_on_demand(callback)) + } + + pub fn pump_events(&mut self, timeout: Option, callback: F) -> PumpStatus + where + F: FnMut(crate::event::Event, &RootELW), + { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.pump_events(timeout, callback)) + } + + pub fn window_target(&self) -> &crate::event_loop::ActiveEventLoop { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.window_target()) + } +} + +impl AsFd for EventLoop { + fn as_fd(&self) -> BorrowedFd<'_> { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.as_fd()) + } +} + +impl AsRawFd for EventLoop { + fn as_raw_fd(&self) -> RawFd { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.as_raw_fd()) + } +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + x11_or_wayland!(match self; EventLoopProxy(proxy) => proxy.send_event(event)) + } +} + +pub enum ActiveEventLoop { + #[cfg(wayland_platform)] + Wayland(wayland::ActiveEventLoop), + #[cfg(x11_platform)] + X(x11::ActiveEventLoop), +} + +impl ActiveEventLoop { + #[inline] + pub fn is_wayland(&self) -> bool { + match *self { + #[cfg(wayland_platform)] + ActiveEventLoop::Wayland(_) => true, + #[cfg(x11_platform)] + _ => false, + } + } + + pub fn create_custom_cursor(&self, cursor: CustomCursorSource) -> CustomCursor { + x11_or_wayland!(match self; ActiveEventLoop(evlp) => evlp.create_custom_cursor(cursor)) + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + match *self { + #[cfg(wayland_platform)] + ActiveEventLoop::Wayland(ref evlp) => evlp + .available_monitors() + .map(MonitorHandle::Wayland) + .collect(), + #[cfg(x11_platform)] + ActiveEventLoop::X(ref evlp) => { + evlp.available_monitors().map(MonitorHandle::X).collect() + } + } + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + Some( + x11_or_wayland!(match self; ActiveEventLoop(evlp) => evlp.primary_monitor()?; as MonitorHandle), + ) + } + + #[inline] + pub fn listen_device_events(&self, allowed: DeviceEvents) { + x11_or_wayland!(match self; Self(evlp) => evlp.listen_device_events(allowed)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + x11_or_wayland!(match self; Self(evlp) => evlp.raw_display_handle_rwh_06()) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + x11_or_wayland!(match self; Self(evlp) => evlp.set_control_flow(control_flow)) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + x11_or_wayland!(match self; Self(evlp) => evlp.control_flow()) + } + + pub(crate) fn clear_exit(&self) { + x11_or_wayland!(match self; Self(evlp) => evlp.clear_exit()) + } + + pub(crate) fn exit(&self) { + x11_or_wayland!(match self; Self(evlp) => evlp.exit()) + } + + pub(crate) fn exiting(&self) -> bool { + x11_or_wayland!(match self; Self(evlp) => evlp.exiting()) + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + match self { + #[cfg(x11_platform)] + Self::X(conn) => OwnedDisplayHandle::X(conn.x_connection().clone()), + #[cfg(wayland_platform)] + Self::Wayland(conn) => OwnedDisplayHandle::Wayland(conn.connection.clone()), + } + } + + #[allow(dead_code)] + fn set_exit_code(&self, code: i32) { + x11_or_wayland!(match self; Self(evlp) => evlp.set_exit_code(code)) + } + + #[allow(dead_code)] + fn exit_code(&self) -> Option { + x11_or_wayland!(match self; Self(evlp) => evlp.exit_code()) + } +} + +#[derive(Clone)] +#[allow(dead_code)] +pub(crate) enum OwnedDisplayHandle { + #[cfg(x11_platform)] + X(Arc), + #[cfg(wayland_platform)] + Wayland(wayland_client::Connection), +} + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + use std::ptr::NonNull; + + match self { + #[cfg(x11_platform)] + Self::X(xconn) => Ok(rwh_06::XlibDisplayHandle::new( + NonNull::new(xconn.display.cast()), + xconn.default_screen_index() as _, + ) + .into()), + + #[cfg(wayland_platform)] + Self::Wayland(conn) => { + use sctk::reexports::client::Proxy; + + Ok(rwh_06::WaylandDisplayHandle::new( + NonNull::new(conn.display().id().as_ptr().cast()).unwrap(), + ) + .into()) + } + } + } +} + +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| { + b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout))) + }) +} + +#[cfg(target_os = "linux")] +fn is_main_thread() -> bool { + rustix::thread::gettid() == rustix::process::getpid() +} + +#[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd"))] +fn is_main_thread() -> bool { + use libc::pthread_main_np; + + unsafe { pthread_main_np() == 1 } +} + +#[cfg(target_os = "netbsd")] +fn is_main_thread() -> bool { + std::thread::current().name() == Some("main") +} diff --git a/rio-window/src/platform_impl/linux/wayland/event_loop/mod.rs b/rio-window/src/platform_impl/linux/wayland/event_loop/mod.rs new file mode 100644 index 00000000..35214f8b --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/event_loop/mod.rs @@ -0,0 +1,750 @@ +//! The event-loop routines. + +use std::cell::{Cell, RefCell}; +use std::io::Result as IOResult; +use std::marker::PhantomData; +use std::mem; +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; +use std::rc::Rc; +use std::sync::atomic::Ordering; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use sctk::reexports::calloop::Error as CalloopError; +use sctk::reexports::calloop_wayland_source::WaylandSource; +use sctk::reexports::client::{globals, Connection, QueueHandle}; + +use crate::cursor::OnlyCursorImage; +use crate::dpi::LogicalSize; +use crate::error::{EventLoopError, OsError as RootOsError}; +use crate::event::{Event, InnerSizeWriter, StartCause, WindowEvent}; +use crate::event_loop::{ + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, +}; +use crate::platform::pump_events::PumpStatus; +use crate::platform_impl::platform::min_timeout; +use crate::platform_impl::{ + ActiveEventLoop as PlatformActiveEventLoop, OsError, PlatformCustomCursor, +}; +use crate::window::{CustomCursor as RootCustomCursor, CustomCursorSource}; + +mod proxy; +pub mod sink; + +pub use proxy::EventLoopProxy; +use sink::EventSink; + +use super::state::{WindowCompositorUpdate, WinitState}; +use super::window::state::FrameCallbackState; +use super::{logical_to_physical_rounded, DeviceId, WaylandError, WindowId}; + +type WaylandDispatcher = + calloop::Dispatcher<'static, WaylandSource, WinitState>; + +/// The Wayland event loop. +pub struct EventLoop { + /// Has `run` or `run_on_demand` been called or a call to `pump_events` that starts the loop + loop_running: bool, + + buffer_sink: EventSink, + compositor_updates: Vec, + window_ids: Vec, + + /// Sender of user events. + user_events_sender: calloop::channel::Sender, + + // XXX can't remove RefCell out of here, unless we can plumb generics into the `Window`, which + // we don't really want, since it'll break public API by a lot. + /// Pending events from the user. + pending_user_events: Rc>>, + + /// The Wayland dispatcher to has raw access to the queue when needed, such as + /// when creating a new window. + wayland_dispatcher: WaylandDispatcher, + + /// Connection to the wayland server. + connection: Connection, + + /// Event loop window target. + window_target: RootActiveEventLoop, + + // XXX drop after everything else, just to be safe. + /// Calloop's event loop. + event_loop: calloop::EventLoop<'static, WinitState>, +} + +impl EventLoop { + pub fn new() -> Result, EventLoopError> { + macro_rules! map_err { + ($e:expr, $err:expr) => { + $e.map_err(|error| os_error!($err(error).into())) + }; + } + + let connection = + map_err!(Connection::connect_to_env(), WaylandError::Connection)?; + + let (globals, mut event_queue) = map_err!( + globals::registry_queue_init(&connection), + WaylandError::Global + )?; + let queue_handle = event_queue.handle(); + + let event_loop = map_err!( + calloop::EventLoop::::try_new(), + WaylandError::Calloop + )?; + + let mut winit_state = + WinitState::new(&globals, &queue_handle, event_loop.handle()) + .map_err(|error| os_error!(error))?; + + // NOTE: do a roundtrip after binding the globals to prevent potential + // races with the server. + map_err!( + event_queue.roundtrip(&mut winit_state), + WaylandError::Dispatch + )?; + + // Register Wayland source. + let wayland_source = WaylandSource::new(connection.clone(), event_queue); + let wayland_dispatcher = calloop::Dispatcher::new( + wayland_source, + |_, queue, winit_state: &mut WinitState| { + let result = queue.dispatch_pending(winit_state); + if result.is_ok() + && (!winit_state.events_sink.is_empty() + || !winit_state.window_compositor_updates.is_empty()) + { + winit_state.dispatched_events = true; + } + result + }, + ); + + map_err!( + event_loop + .handle() + .register_dispatcher(wayland_dispatcher.clone()), + WaylandError::Calloop + )?; + + // Setup the user proxy. + let pending_user_events = Rc::new(RefCell::new(Vec::new())); + let pending_user_events_clone = pending_user_events.clone(); + let (user_events_sender, user_events_channel) = calloop::channel::channel(); + let result = event_loop + .handle() + .insert_source( + user_events_channel, + move |event, _, winit_state: &mut WinitState| { + if let calloop::channel::Event::Msg(msg) = event { + winit_state.dispatched_events = true; + pending_user_events_clone.borrow_mut().push(msg); + } + }, + ) + .map_err(|error| error.error); + map_err!(result, WaylandError::Calloop)?; + + // An event's loop awakener to wake up for window events from winit's windows. + let (event_loop_awakener, event_loop_awakener_source) = map_err!( + calloop::ping::make_ping() + .map_err(|error| CalloopError::OtherError(Box::new(error).into())), + WaylandError::Calloop + )?; + + let result = event_loop + .handle() + .insert_source( + event_loop_awakener_source, + move |_, _, winit_state: &mut WinitState| { + // Mark that we have something to dispatch. + winit_state.dispatched_events = true; + }, + ) + .map_err(|error| error.error); + map_err!(result, WaylandError::Calloop)?; + + let window_target = ActiveEventLoop { + connection: connection.clone(), + wayland_dispatcher: wayland_dispatcher.clone(), + event_loop_awakener, + queue_handle, + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(None), + state: RefCell::new(winit_state), + }; + + let event_loop = Self { + loop_running: false, + compositor_updates: Vec::new(), + buffer_sink: EventSink::default(), + window_ids: Vec::new(), + connection, + wayland_dispatcher, + user_events_sender, + pending_user_events, + event_loop, + window_target: RootActiveEventLoop { + p: PlatformActiveEventLoop::Wayland(window_target), + _marker: PhantomData, + }, + }; + + Ok(event_loop) + } + + pub fn run_on_demand(&mut self, mut event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootActiveEventLoop), + { + let exit = loop { + match self.pump_events(None, &mut event_handler) { + PumpStatus::Exit(0) => { + break Ok(()); + } + PumpStatus::Exit(code) => { + break Err(EventLoopError::ExitFailure(code)); + } + _ => { + continue; + } + } + }; + + // Applications aren't allowed to carry windows between separate + // `run_on_demand` calls but if they have only just dropped their + // windows we need to make sure those last requests are sent to the + // compositor. + let _ = self.roundtrip().map_err(EventLoopError::Os); + + exit + } + + pub fn pump_events( + &mut self, + timeout: Option, + mut callback: F, + ) -> PumpStatus + where + F: FnMut(Event, &RootActiveEventLoop), + { + if !self.loop_running { + self.loop_running = true; + + // Run the initial loop iteration. + self.single_iteration(&mut callback, StartCause::Init); + } + + // Consider the possibility that the `StartCause::Init` iteration could + // request to Exit. + if !self.exiting() { + self.poll_events_with_timeout(timeout, &mut callback); + } + if let Some(code) = self.exit_code() { + self.loop_running = false; + + callback(Event::LoopExiting, self.window_target()); + + PumpStatus::Exit(code) + } else { + PumpStatus::Continue + } + } + + pub fn poll_events_with_timeout( + &mut self, + mut timeout: Option, + mut callback: F, + ) where + F: FnMut(Event, &RootActiveEventLoop), + { + let cause = loop { + let start = Instant::now(); + + timeout = { + let control_flow_timeout = match self.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + Some(wait_deadline.saturating_duration_since(start)) + } + }; + min_timeout(control_flow_timeout, timeout) + }; + + // NOTE Ideally we should flush as the last thing we do before polling + // to wait for events, and this should be done by the calloop + // WaylandSource but we currently need to flush writes manually. + // + // Checking for flush error is essential to perform an exit with error, since + // once we have a protocol error, we could get stuck retrying... + if self.connection.flush().is_err() { + self.set_exit_code(1); + return; + } + + if let Err(error) = self.loop_dispatch(timeout) { + // NOTE We exit on errors from dispatches, since if we've got protocol error + // libwayland-client/wayland-rs will inform us anyway, but crashing downstream is + // not really an option. Instead we inform that the event loop got + // destroyed. We may communicate an error that something was + // terminated, but winit doesn't provide us with an API to do that + // via some event. Still, we set the exit code to the error's OS + // error code, or to 1 if not possible. + let exit_code = error.raw_os_error().unwrap_or(1); + self.set_exit_code(exit_code); + return; + } + + // NB: `StartCause::Init` is handled as a special case and doesn't need + // to be considered here + let cause = match self.control_flow() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { + start, + requested_resume: None, + }, + ControlFlow::WaitUntil(deadline) => { + if Instant::now() < deadline { + StartCause::WaitCancelled { + start, + requested_resume: Some(deadline), + } + } else { + StartCause::ResumeTimeReached { + start, + requested_resume: deadline, + } + } + } + }; + + // Reduce spurious wake-ups. + let dispatched_events = self.with_state(|state| state.dispatched_events); + if matches!(cause, StartCause::WaitCancelled { .. }) && !dispatched_events { + continue; + } + + break cause; + }; + + self.single_iteration(&mut callback, cause); + } + + fn single_iteration(&mut self, callback: &mut F, cause: StartCause) + where + F: FnMut(Event, &RootActiveEventLoop), + { + // NOTE currently just indented to simplify the diff + + // We retain these grow-only scratch buffers as part of the EventLoop + // for the sake of avoiding lots of reallocs. We take them here to avoid + // trying to mutably borrow `self` more than once and we swap them back + // when finished. + let mut compositor_updates = std::mem::take(&mut self.compositor_updates); + let mut buffer_sink = std::mem::take(&mut self.buffer_sink); + let mut window_ids = std::mem::take(&mut self.window_ids); + + callback(Event::NewEvents(cause), &self.window_target); + + // NB: For consistency all platforms must emit a 'resumed' event even though Wayland + // applications don't themselves have a formal suspend/resume lifecycle. + if cause == StartCause::Init { + callback(Event::Resumed, &self.window_target); + } + + // Handle pending user events. We don't need back buffer, since we can't dispatch + // user events indirectly via callback to the user. + for user_event in self.pending_user_events.borrow_mut().drain(..) { + callback(Event::UserEvent(user_event), &self.window_target); + } + + // Drain the pending compositor updates. + self.with_state(|state| { + compositor_updates.append(&mut state.window_compositor_updates) + }); + + for mut compositor_update in compositor_updates.drain(..) { + let window_id = compositor_update.window_id; + if compositor_update.scale_changed { + let (physical_size, scale_factor) = self.with_state(|state| { + let windows = state.windows.get_mut(); + let window = windows.get(&window_id).unwrap().lock().unwrap(); + let scale_factor = window.scale_factor(); + let size = + logical_to_physical_rounded(window.inner_size(), scale_factor); + (size, scale_factor) + }); + + // Stash the old window size. + let old_physical_size = physical_size; + + let new_inner_size = Arc::new(Mutex::new(physical_size)); + callback( + Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade( + &new_inner_size, + )), + }, + }, + &self.window_target, + ); + + let physical_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); + + // Resize the window when user altered the size. + if old_physical_size != physical_size { + self.with_state(|state| { + let windows = state.windows.get_mut(); + let mut window = windows.get(&window_id).unwrap().lock().unwrap(); + + let new_logical_size: LogicalSize = + physical_size.to_logical(scale_factor); + window.request_inner_size(new_logical_size.into()); + }); + + // Make it queue resize. + compositor_update.resized = true; + } + } + + // NOTE: Rescale changed the physical size which winit operates in, thus we should + // resize. + if compositor_update.resized || compositor_update.scale_changed { + let physical_size = self.with_state(|state| { + let windows = state.windows.get_mut(); + let window = windows.get(&window_id).unwrap().lock().unwrap(); + + let scale_factor = window.scale_factor(); + let size = + logical_to_physical_rounded(window.inner_size(), scale_factor); + + // Mark the window as needed a redraw. + state + .window_requests + .get_mut() + .get_mut(&window_id) + .unwrap() + .redraw_requested + .store(true, Ordering::Relaxed); + + size + }); + + callback( + Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::Resized(physical_size), + }, + &self.window_target, + ); + } + + if compositor_update.close_window { + callback( + Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::CloseRequested, + }, + &self.window_target, + ); + } + } + + // Push the events directly from the window. + self.with_state(|state| { + buffer_sink.append(&mut state.window_events_sink.lock().unwrap()); + }); + for event in buffer_sink.drain() { + let event = event.map_nonuser_event().unwrap(); + callback(event, &self.window_target); + } + + // Handle non-synthetic events. + self.with_state(|state| { + buffer_sink.append(&mut state.events_sink); + }); + for event in buffer_sink.drain() { + let event = event.map_nonuser_event().unwrap(); + callback(event, &self.window_target); + } + + // Collect the window ids + self.with_state(|state| { + window_ids.extend(state.window_requests.get_mut().keys()); + }); + + for window_id in window_ids.iter() { + let event = self.with_state(|state| { + let window_requests = state.window_requests.get_mut(); + if window_requests.get(window_id).unwrap().take_closed() { + mem::drop(window_requests.remove(window_id)); + mem::drop(state.windows.get_mut().remove(window_id)); + return Some(WindowEvent::Destroyed); + } + + let mut window = state + .windows + .get_mut() + .get_mut(window_id) + .unwrap() + .lock() + .unwrap(); + + if window.frame_callback_state() == FrameCallbackState::Requested { + return None; + } + + // Reset the frame callbacks state. + window.frame_callback_reset(); + let mut redraw_requested = window_requests + .get(window_id) + .unwrap() + .take_redraw_requested(); + + // Redraw the frame while at it. + redraw_requested |= window.refresh_frame(); + + redraw_requested.then_some(WindowEvent::RedrawRequested) + }); + + if let Some(event) = event { + callback( + Event::WindowEvent { + window_id: crate::window::WindowId(*window_id), + event, + }, + &self.window_target, + ); + } + } + + // Reset the hint that we've dispatched events. + self.with_state(|state| { + state.dispatched_events = false; + }); + + // This is always the last event we dispatch before poll again + callback(Event::AboutToWait, &self.window_target); + + // Update the window frames and schedule redraws. + let mut wake_up = false; + for window_id in window_ids.drain(..) { + wake_up |= self.with_state(|state| { + match state.windows.get_mut().get_mut(&window_id) { + Some(window) => { + let refresh = window.lock().unwrap().refresh_frame(); + if refresh { + state + .window_requests + .get_mut() + .get_mut(&window_id) + .unwrap() + .redraw_requested + .store(true, Ordering::Relaxed); + } + + refresh + } + None => false, + } + }); + } + + // Wakeup event loop if needed. + // + // If the user draws from the `AboutToWait` this is likely not required, however + // we can't do much about it. + if wake_up { + match &self.window_target.p { + PlatformActiveEventLoop::Wayland(window_target) => { + window_target.event_loop_awakener.ping(); + } + #[cfg(x11_platform)] + PlatformActiveEventLoop::X(_) => unreachable!(), + } + } + + std::mem::swap(&mut self.compositor_updates, &mut compositor_updates); + std::mem::swap(&mut self.buffer_sink, &mut buffer_sink); + std::mem::swap(&mut self.window_ids, &mut window_ids); + } + + #[inline] + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.user_events_sender.clone()) + } + + #[inline] + pub fn window_target(&self) -> &RootActiveEventLoop { + &self.window_target + } + + fn with_state<'a, U: 'a, F: FnOnce(&'a mut WinitState) -> U>( + &'a mut self, + callback: F, + ) -> U { + let state = match &mut self.window_target.p { + PlatformActiveEventLoop::Wayland(window_target) => { + window_target.state.get_mut() + } + #[cfg(x11_platform)] + _ => unreachable!(), + }; + + callback(state) + } + + fn loop_dispatch>>( + &mut self, + timeout: D, + ) -> IOResult<()> { + let state = match &mut self.window_target.p { + PlatformActiveEventLoop::Wayland(window_target) => { + window_target.state.get_mut() + } + #[cfg(feature = "x11")] + _ => unreachable!(), + }; + + self.event_loop.dispatch(timeout, state).map_err(|error| { + tracing::error!("Error dispatching event loop: {}", error); + error.into() + }) + } + + fn roundtrip(&mut self) -> Result { + let state = match &mut self.window_target.p { + PlatformActiveEventLoop::Wayland(window_target) => { + window_target.state.get_mut() + } + #[cfg(feature = "x11")] + _ => unreachable!(), + }; + + let mut wayland_source = self.wayland_dispatcher.as_source_mut(); + let event_queue = wayland_source.queue(); + event_queue.roundtrip(state).map_err(|error| { + os_error!(OsError::WaylandError(Arc::new(WaylandError::Dispatch( + error + )))) + }) + } + + fn control_flow(&self) -> ControlFlow { + self.window_target.p.control_flow() + } + + fn exiting(&self) -> bool { + self.window_target.p.exiting() + } + + fn set_exit_code(&self, code: i32) { + self.window_target.p.set_exit_code(code) + } + + fn exit_code(&self) -> Option { + self.window_target.p.exit_code() + } +} + +impl AsFd for EventLoop { + fn as_fd(&self) -> BorrowedFd<'_> { + self.event_loop.as_fd() + } +} + +impl AsRawFd for EventLoop { + fn as_raw_fd(&self) -> RawFd { + self.event_loop.as_raw_fd() + } +} + +pub struct ActiveEventLoop { + /// The event loop wakeup source. + pub event_loop_awakener: calloop::ping::Ping, + + /// The main queue used by the event loop. + pub queue_handle: QueueHandle, + + /// The application's latest control_flow state + pub(crate) control_flow: Cell, + + /// The application's exit state. + pub(crate) exit: Cell>, + + // TODO remove that RefCell once we can pass `&mut` in `Window::new`. + /// Winit state. + pub state: RefCell, + + /// Dispatcher of Wayland events. + pub wayland_dispatcher: WaylandDispatcher, + + /// Connection to the wayland server. + pub connection: Connection, +} + +impl ActiveEventLoop { + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub(crate) fn exit(&self) { + self.exit.set(Some(0)) + } + + pub(crate) fn clear_exit(&self) { + self.exit.set(None) + } + + pub(crate) fn exiting(&self) -> bool { + self.exit.get().is_some() + } + + pub(crate) fn set_exit_code(&self, code: i32) { + self.exit.set(Some(code)) + } + + pub(crate) fn exit_code(&self) -> Option { + self.exit.get() + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + pub(crate) fn create_custom_cursor( + &self, + cursor: CustomCursorSource, + ) -> RootCustomCursor { + RootCustomCursor { + inner: PlatformCustomCursor::Wayland(OnlyCursorImage(Arc::from( + cursor.inner.0, + ))), + } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + use sctk::reexports::client::Proxy; + + Ok(rwh_06::WaylandDisplayHandle::new({ + let ptr = self.connection.display().id().as_ptr(); + std::ptr::NonNull::new(ptr as *mut _) + .expect("wl_display should never be null") + }) + .into()) + } +} diff --git a/rio-window/src/platform_impl/linux/wayland/event_loop/proxy.rs b/rio-window/src/platform_impl/linux/wayland/event_loop/proxy.rs new file mode 100644 index 00000000..dad64ef2 --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/event_loop/proxy.rs @@ -0,0 +1,32 @@ +//! An event loop proxy. + +use std::sync::mpsc::SendError; + +use sctk::reexports::calloop::channel::Sender; + +use crate::event_loop::EventLoopClosed; + +/// A handle that can be sent across the threads and used to wake up the `EventLoop`. +pub struct EventLoopProxy { + user_events_sender: Sender, +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy { + user_events_sender: self.user_events_sender.clone(), + } + } +} + +impl EventLoopProxy { + pub fn new(user_events_sender: Sender) -> Self { + Self { user_events_sender } + } + + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.user_events_sender + .send(event) + .map_err(|SendError(error)| EventLoopClosed(error)) + } +} diff --git a/rio-window/src/platform_impl/linux/wayland/event_loop/sink.rs b/rio-window/src/platform_impl/linux/wayland/event_loop/sink.rs new file mode 100644 index 00000000..13daeee3 --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/event_loop/sink.rs @@ -0,0 +1,56 @@ +//! An event loop's sink to deliver events from the Wayland event callbacks. + +use std::vec::Drain; + +use crate::event::{DeviceEvent, DeviceId as RootDeviceId, Event, WindowEvent}; +use crate::platform_impl::platform::DeviceId as PlatformDeviceId; +use crate::window::WindowId as RootWindowId; + +use super::{DeviceId, WindowId}; + +/// An event loop's sink to deliver events from the Wayland event callbacks +/// to the winit's user. +#[derive(Default)] +pub struct EventSink { + pub window_events: Vec>, +} + +impl EventSink { + pub fn new() -> Self { + Default::default() + } + + /// Return `true` if there're pending events. + #[inline] + pub fn is_empty(&self) -> bool { + self.window_events.is_empty() + } + + /// Add new device event to a queue. + #[inline] + pub fn push_device_event(&mut self, event: DeviceEvent, device_id: DeviceId) { + self.window_events.push(Event::DeviceEvent { + event, + device_id: RootDeviceId(PlatformDeviceId::Wayland(device_id)), + }); + } + + /// Add new window event to a queue. + #[inline] + pub fn push_window_event(&mut self, event: WindowEvent, window_id: WindowId) { + self.window_events.push(Event::WindowEvent { + event, + window_id: RootWindowId(window_id), + }); + } + + #[inline] + pub fn append(&mut self, other: &mut Self) { + self.window_events.append(&mut other.window_events); + } + + #[inline] + pub fn drain(&mut self) -> Drain<'_, Event<()>> { + self.window_events.drain(..) + } +} diff --git a/rio-window/src/platform_impl/linux/wayland/mod.rs b/rio-window/src/platform_impl/linux/wayland/mod.rs new file mode 100644 index 00000000..48e959d0 --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/mod.rs @@ -0,0 +1,90 @@ +#![cfg(wayland_platform)] + +//! Winit's Wayland backend. + +use std::fmt::Display; +use std::sync::Arc; + +use sctk::reexports::client::globals::{BindError, GlobalError}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{self, ConnectError, DispatchError, Proxy}; + +pub(super) use crate::cursor::OnlyCursorImage as CustomCursor; +use crate::dpi::{LogicalSize, PhysicalSize}; +pub use crate::platform_impl::platform::{OsError, WindowId}; +pub use event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy}; +pub use output::{MonitorHandle, VideoModeHandle}; +pub use window::Window; + +mod event_loop; +mod output; +mod seat; +mod state; +mod types; +mod window; + +#[derive(Debug)] +pub enum WaylandError { + /// Error connecting to the socket. + Connection(ConnectError), + + /// Error binding the global. + Global(GlobalError), + + // Bind error. + Bind(BindError), + + /// Error during the dispatching the event queue. + Dispatch(DispatchError), + + /// Calloop error. + Calloop(calloop::Error), + + /// Wayland + Wire(client::backend::WaylandError), +} + +impl Display for WaylandError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WaylandError::Connection(error) => error.fmt(f), + WaylandError::Global(error) => error.fmt(f), + WaylandError::Bind(error) => error.fmt(f), + WaylandError::Dispatch(error) => error.fmt(f), + WaylandError::Calloop(error) => error.fmt(f), + WaylandError::Wire(error) => error.fmt(f), + } + } +} + +impl From for OsError { + fn from(value: WaylandError) -> Self { + Self::WaylandError(Arc::new(value)) + } +} + +/// Dummy device id, since Wayland doesn't have device events. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId; + +impl DeviceId { + pub const unsafe fn dummy() -> Self { + DeviceId + } +} + +/// Get the WindowId out of the surface. +#[inline] +fn make_wid(surface: &WlSurface) -> WindowId { + WindowId(surface.id().as_ptr() as u64) +} + +/// The default routine does floor, but we need round on Wayland. +fn logical_to_physical_rounded( + size: LogicalSize, + scale_factor: f64, +) -> PhysicalSize { + let width = size.width as f64 * scale_factor; + let height = size.height as f64 * scale_factor; + (width.round(), height.round()).into() +} diff --git a/rio-window/src/platform_impl/linux/wayland/output.rs b/rio-window/src/platform_impl/linux/wayland/output.rs new file mode 100644 index 00000000..666c1b38 --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/output.rs @@ -0,0 +1,171 @@ +use sctk::reexports::client::protocol::wl_output::WlOutput; +use sctk::reexports::client::Proxy; + +use sctk::output::OutputData; + +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; +use crate::platform_impl::platform::VideoModeHandle as PlatformVideoModeHandle; + +use super::event_loop::ActiveEventLoop; + +impl ActiveEventLoop { + #[inline] + pub fn available_monitors(&self) -> impl Iterator { + self.state + .borrow() + .output_state + .outputs() + .map(MonitorHandle::new) + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + // There's no primary monitor on Wayland. + None + } +} + +#[derive(Clone, Debug)] +pub struct MonitorHandle { + pub(crate) proxy: WlOutput, +} + +impl MonitorHandle { + #[inline] + pub(crate) fn new(proxy: WlOutput) -> Self { + Self { proxy } + } + + #[inline] + pub fn name(&self) -> Option { + let output_data = self.proxy.data::().unwrap(); + output_data.with_output_info(|info| info.name.clone()) + } + + #[inline] + pub fn native_identifier(&self) -> u32 { + let output_data = self.proxy.data::().unwrap(); + output_data.with_output_info(|info| info.id) + } + + #[inline] + pub fn size(&self) -> PhysicalSize { + let output_data = self.proxy.data::().unwrap(); + let dimensions = output_data.with_output_info(|info| { + info.modes + .iter() + .find_map(|mode| mode.current.then_some(mode.dimensions)) + }); + + match dimensions { + Some((width, height)) => (width as u32, height as u32), + _ => (0, 0), + } + .into() + } + + #[inline] + pub fn position(&self) -> PhysicalPosition { + let output_data = self.proxy.data::().unwrap(); + output_data.with_output_info(|info| { + info.logical_position.map_or_else( + || { + LogicalPosition::::from(info.location) + .to_physical(info.scale_factor as f64) + }, + |logical_position| { + LogicalPosition::::from(logical_position) + .to_physical(info.scale_factor as f64) + }, + ) + }) + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + let output_data = self.proxy.data::().unwrap(); + output_data.with_output_info(|info| { + info.modes + .iter() + .find_map(|mode| mode.current.then_some(mode.refresh_rate as u32)) + }) + } + + #[inline] + pub fn scale_factor(&self) -> i32 { + let output_data = self.proxy.data::().unwrap(); + output_data.scale_factor() + } + + #[inline] + pub fn video_modes(&self) -> impl Iterator { + let output_data = self.proxy.data::().unwrap(); + let modes = output_data.with_output_info(|info| info.modes.clone()); + + let monitor = self.clone(); + + modes.into_iter().map(move |mode| { + PlatformVideoModeHandle::Wayland(VideoModeHandle { + size: (mode.dimensions.0 as u32, mode.dimensions.1 as u32).into(), + refresh_rate_millihertz: mode.refresh_rate as u32, + bit_depth: 32, + monitor: monitor.clone(), + }) + }) + } +} + +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + self.native_identifier() == other.native_identifier() + } +} + +impl Eq for MonitorHandle {} + +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MonitorHandle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.native_identifier().cmp(&other.native_identifier()) + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + self.native_identifier().hash(state); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct VideoModeHandle { + pub(crate) size: PhysicalSize, + pub(crate) bit_depth: u16, + pub(crate) refresh_rate_millihertz: u32, + pub(crate) monitor: MonitorHandle, +} + +impl VideoModeHandle { + #[inline] + pub fn size(&self) -> PhysicalSize { + self.size + } + + #[inline] + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } +} diff --git a/rio-window/src/platform_impl/linux/wayland/seat/keyboard/mod.rs b/rio-window/src/platform_impl/linux/wayland/seat/keyboard/mod.rs new file mode 100644 index 00000000..533ab6d0 --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/seat/keyboard/mod.rs @@ -0,0 +1,429 @@ +//! The keyboard input handling. + +use std::sync::Mutex; +use std::time::Duration; + +use calloop::timer::{TimeoutAction, Timer}; +use calloop::{LoopHandle, RegistrationToken}; +use tracing::warn; + +use sctk::reexports::client::protocol::wl_keyboard::{ + Event as WlKeyboardEvent, KeyState as WlKeyState, KeymapFormat as WlKeymapFormat, + WlKeyboard, +}; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::{Connection, Dispatch, Proxy, QueueHandle, WEnum}; + +use crate::event::{ElementState, WindowEvent}; +use crate::keyboard::ModifiersState; + +use crate::platform_impl::common::xkb::Context; +use crate::platform_impl::wayland::event_loop::sink::EventSink; +use crate::platform_impl::wayland::state::WinitState; +use crate::platform_impl::wayland::{self, DeviceId, WindowId}; + +impl Dispatch for WinitState { + fn event( + state: &mut WinitState, + wl_keyboard: &WlKeyboard, + event: ::Event, + data: &KeyboardData, + _: &Connection, + _: &QueueHandle, + ) { + let seat_state = match state.seats.get_mut(&data.seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received keyboard event {event:?} without seat"); + return; + } + }; + let keyboard_state = match seat_state.keyboard_state.as_mut() { + Some(keyboard_state) => keyboard_state, + None => { + warn!("Received keyboard event {event:?} without keyboard"); + return; + } + }; + + match event { + WlKeyboardEvent::Keymap { format, fd, size } => match format { + WEnum::Value(format) => match format { + WlKeymapFormat::NoKeymap => { + warn!("non-xkb compatible keymap") + } + WlKeymapFormat::XkbV1 => { + let context = &mut keyboard_state.xkb_context; + context.set_keymap_from_fd(fd, size as usize); + } + _ => unreachable!(), + }, + WEnum::Unknown(value) => { + warn!("unknown keymap format 0x{:x}", value) + } + }, + WlKeyboardEvent::Enter { surface, .. } => { + let window_id = wayland::make_wid(&surface); + + // Mark the window as focused. + let was_unfocused = match state.windows.get_mut().get(&window_id) { + Some(window) => { + let mut window = window.lock().unwrap(); + let was_unfocused = !window.has_focus(); + window.add_seat_focus(data.seat.id()); + was_unfocused + } + None => return, + }; + + // Drop the repeat, if there were any. + keyboard_state.current_repeat = None; + if let Some(token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(token); + } + + *data.window_id.lock().unwrap() = Some(window_id); + + // The keyboard focus is considered as general focus. + if was_unfocused { + state + .events_sink + .push_window_event(WindowEvent::Focused(true), window_id); + } + + // HACK: this is just for GNOME not fixing their ordering issue of modifiers. + if std::mem::take(&mut seat_state.modifiers_pending) { + state.events_sink.push_window_event( + WindowEvent::ModifiersChanged(seat_state.modifiers.into()), + window_id, + ); + } + } + WlKeyboardEvent::Leave { surface, .. } => { + let window_id = wayland::make_wid(&surface); + + // NOTE: we should drop the repeat regardless whethere it was for the present + // window of for the window which just went gone. + keyboard_state.current_repeat = None; + if let Some(token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(token); + } + + // NOTE: The check whether the window exists is essential as we might get a + // nil surface, regardless of what protocol says. + let focused = match state.windows.get_mut().get(&window_id) { + Some(window) => { + let mut window = window.lock().unwrap(); + window.remove_seat_focus(&data.seat.id()); + window.has_focus() + } + None => return, + }; + + // We don't need to update it above, because the next `Enter` will overwrite + // anyway. + *data.window_id.lock().unwrap() = None; + + if !focused { + // Notify that no modifiers are being pressed. + state.events_sink.push_window_event( + WindowEvent::ModifiersChanged(ModifiersState::empty().into()), + window_id, + ); + + state + .events_sink + .push_window_event(WindowEvent::Focused(false), window_id); + } + } + WlKeyboardEvent::Key { + key, + state: WEnum::Value(WlKeyState::Pressed), + .. + } => { + let key = key + 8; + + key_input( + keyboard_state, + &mut state.events_sink, + data, + key, + ElementState::Pressed, + false, + ); + + let delay = match keyboard_state.repeat_info { + RepeatInfo::Repeat { delay, .. } => delay, + RepeatInfo::Disable => return, + }; + + if !keyboard_state + .xkb_context + .keymap_mut() + .unwrap() + .key_repeats(key) + { + return; + } + + keyboard_state.current_repeat = Some(key); + + // NOTE terminate ongoing timer and start a new timer. + + if let Some(token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(token); + } + + let timer = Timer::from_duration(delay); + let wl_keyboard = wl_keyboard.clone(); + keyboard_state.repeat_token = keyboard_state + .loop_handle + .insert_source(timer, move |_, _, state| { + // Required to handle the wakeups from the repeat sources. + state.dispatched_events = true; + + let data = wl_keyboard.data::().unwrap(); + let seat_state = match state.seats.get_mut(&data.seat.id()) { + Some(seat_state) => seat_state, + None => return TimeoutAction::Drop, + }; + + let keyboard_state = match seat_state.keyboard_state.as_mut() { + Some(keyboard_state) => keyboard_state, + None => return TimeoutAction::Drop, + }; + + // NOTE: The removed on event source is batched, but key change to `None` + // is instant. + let repeat_keycode = match keyboard_state.current_repeat { + Some(repeat_keycode) => repeat_keycode, + None => return TimeoutAction::Drop, + }; + + key_input( + keyboard_state, + &mut state.events_sink, + data, + repeat_keycode, + ElementState::Pressed, + true, + ); + + // NOTE: the gap could change dynamically while repeat is going. + match keyboard_state.repeat_info { + RepeatInfo::Repeat { gap, .. } => { + TimeoutAction::ToDuration(gap) + } + RepeatInfo::Disable => TimeoutAction::Drop, + } + }) + .ok(); + } + WlKeyboardEvent::Key { + key, + state: WEnum::Value(WlKeyState::Released), + .. + } => { + let key = key + 8; + + key_input( + keyboard_state, + &mut state.events_sink, + data, + key, + ElementState::Released, + false, + ); + + if keyboard_state.repeat_info != RepeatInfo::Disable + && keyboard_state + .xkb_context + .keymap_mut() + .unwrap() + .key_repeats(key) + && Some(key) == keyboard_state.current_repeat + { + keyboard_state.current_repeat = None; + if let Some(token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(token); + } + } + } + WlKeyboardEvent::Modifiers { + mods_depressed, + mods_latched, + mods_locked, + group, + .. + } => { + let xkb_context = &mut keyboard_state.xkb_context; + let xkb_state = match xkb_context.state_mut() { + Some(state) => state, + None => return, + }; + + xkb_state.update_modifiers( + mods_depressed, + mods_latched, + mods_locked, + 0, + 0, + group, + ); + seat_state.modifiers = xkb_state.modifiers().into(); + + // HACK: part of the workaround from `WlKeyboardEvent::Enter`. + let window_id = match *data.window_id.lock().unwrap() { + Some(window_id) => window_id, + None => { + seat_state.modifiers_pending = true; + return; + } + }; + + state.events_sink.push_window_event( + WindowEvent::ModifiersChanged(seat_state.modifiers.into()), + window_id, + ); + } + WlKeyboardEvent::RepeatInfo { rate, delay } => { + keyboard_state.repeat_info = if rate == 0 { + // Stop the repeat once we get a disable event. + keyboard_state.current_repeat = None; + if let Some(repeat_token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(repeat_token); + } + RepeatInfo::Disable + } else { + let gap = Duration::from_micros(1_000_000 / rate as u64); + let delay = Duration::from_millis(delay as u64); + RepeatInfo::Repeat { gap, delay } + }; + } + _ => unreachable!(), + } + } +} + +/// The state of the keyboard on the current seat. +#[derive(Debug)] +pub struct KeyboardState { + /// The underlying WlKeyboard. + pub keyboard: WlKeyboard, + + /// Loop handle to handle key repeat. + pub loop_handle: LoopHandle<'static, WinitState>, + + /// The state of the keyboard. + pub xkb_context: Context, + + /// The information about the repeat rate obtained from the compositor. + pub repeat_info: RepeatInfo, + + /// The token of the current handle inside the calloop's event loop. + pub repeat_token: Option, + + /// The current repeat raw key. + pub current_repeat: Option, +} + +impl KeyboardState { + pub fn new( + keyboard: WlKeyboard, + loop_handle: LoopHandle<'static, WinitState>, + ) -> Self { + Self { + keyboard, + loop_handle, + xkb_context: Context::new().unwrap(), + repeat_info: RepeatInfo::default(), + repeat_token: None, + current_repeat: None, + } + } +} + +impl Drop for KeyboardState { + fn drop(&mut self) { + if self.keyboard.version() >= 3 { + self.keyboard.release(); + } + + if let Some(token) = self.repeat_token.take() { + self.loop_handle.remove(token); + } + } +} + +/// The rate at which a pressed key is repeated. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RepeatInfo { + /// Keys will be repeated at the specified rate and delay. + Repeat { + /// The time between the key repeats. + gap: Duration, + + /// Delay (in milliseconds) between a key press and the start of repetition. + delay: Duration, + }, + + /// Keys should not be repeated. + Disable, +} + +impl Default for RepeatInfo { + /// The default repeat rate is 25 keys per second with the delay of 200ms. + /// + /// The values are picked based on the default in various compositors and Xorg. + fn default() -> Self { + Self::Repeat { + gap: Duration::from_millis(40), + delay: Duration::from_millis(200), + } + } +} + +/// Keyboard user data. +#[derive(Debug)] +pub struct KeyboardData { + /// The currently focused window surface. Could be `None` on bugged compositors, like mutter. + window_id: Mutex>, + + /// The seat used to create this keyboard. + seat: WlSeat, +} + +impl KeyboardData { + pub fn new(seat: WlSeat) -> Self { + Self { + window_id: Default::default(), + seat, + } + } +} + +fn key_input( + keyboard_state: &mut KeyboardState, + event_sink: &mut EventSink, + data: &KeyboardData, + keycode: u32, + state: ElementState, + repeat: bool, +) { + let window_id = match *data.window_id.lock().unwrap() { + Some(window_id) => window_id, + None => return, + }; + + let device_id = + crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(DeviceId)); + if let Some(mut key_context) = keyboard_state.xkb_context.key_context() { + let event = key_context.process_key_event(keycode, state, repeat); + let event = WindowEvent::KeyboardInput { + device_id, + event, + is_synthetic: false, + }; + event_sink.push_window_event(event, window_id); + } +} diff --git a/rio-window/src/platform_impl/linux/wayland/seat/mod.rs b/rio-window/src/platform_impl/linux/wayland/seat/mod.rs new file mode 100644 index 00000000..6e51571f --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/seat/mod.rs @@ -0,0 +1,238 @@ +//! Seat handling. + +use std::sync::Arc; + +use ahash::AHashMap; +use tracing::warn; + +use sctk::reexports::client::backend::ObjectId; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::protocol::wl_touch::WlTouch; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; +use sctk::reexports::protocols::wp::relative_pointer::zv1::client::zwp_relative_pointer_v1::ZwpRelativePointerV1; +use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3; + +use sctk::seat::pointer::{ThemeSpec, ThemedPointer}; +use sctk::seat::{Capability as SeatCapability, SeatHandler, SeatState}; + +use crate::event::WindowEvent; +use crate::keyboard::ModifiersState; +use crate::platform_impl::wayland::state::WinitState; + +mod keyboard; +mod pointer; +mod text_input; +mod touch; + +pub use pointer::relative_pointer::RelativePointerState; +pub use pointer::{PointerConstraintsState, WinitPointerData, WinitPointerDataExt}; +pub use text_input::{TextInputState, ZwpTextInputV3Ext}; + +use keyboard::{KeyboardData, KeyboardState}; +use text_input::TextInputData; +use touch::TouchPoint; + +#[derive(Debug, Default)] +pub struct WinitSeatState { + /// The pointer bound on the seat. + pointer: Option>>, + + /// The touch bound on the seat. + touch: Option, + + /// The mapping from touched points to the surfaces they're present. + touch_map: AHashMap, + + /// The text input bound on the seat. + text_input: Option>, + + /// The relative pointer bound on the seat. + relative_pointer: Option, + + /// The keyboard bound on the seat. + keyboard_state: Option, + + /// The current modifiers state on the seat. + modifiers: ModifiersState, + + /// Whether we have pending modifiers. + modifiers_pending: bool, +} + +impl WinitSeatState { + pub fn new() -> Self { + Default::default() + } +} + +impl SeatHandler for WinitState { + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn new_capability( + &mut self, + _: &Connection, + queue_handle: &QueueHandle, + seat: WlSeat, + capability: SeatCapability, + ) { + let seat_state = match self.seats.get_mut(&seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_seat::new_capability for unknown seat"); + return; + } + }; + + match capability { + SeatCapability::Touch if seat_state.touch.is_none() => { + seat_state.touch = self.seat_state.get_touch(queue_handle, &seat).ok(); + } + SeatCapability::Keyboard if seat_state.keyboard_state.is_none() => { + let keyboard = + seat.get_keyboard(queue_handle, KeyboardData::new(seat.clone())); + seat_state.keyboard_state = + Some(KeyboardState::new(keyboard, self.loop_handle.clone())); + } + SeatCapability::Pointer if seat_state.pointer.is_none() => { + let surface = self.compositor_state.create_surface(queue_handle); + let surface_id = surface.id(); + let pointer_data = WinitPointerData::new(seat.clone()); + let themed_pointer = self + .seat_state + .get_pointer_with_theme_and_data( + queue_handle, + &seat, + self.shm.wl_shm(), + surface, + ThemeSpec::System, + pointer_data, + ) + .expect("failed to create pointer with present capability."); + + seat_state.relative_pointer = + self.relative_pointer.as_ref().map(|manager| { + manager.get_relative_pointer( + themed_pointer.pointer(), + queue_handle, + sctk::globals::GlobalData, + ) + }); + + let themed_pointer = Arc::new(themed_pointer); + + // Register cursor surface. + self.pointer_surfaces + .insert(surface_id, themed_pointer.clone()); + + seat_state.pointer = Some(themed_pointer); + } + _ => (), + } + + if let Some(text_input_state) = seat_state + .text_input + .is_none() + .then_some(self.text_input_state.as_ref()) + .flatten() + { + seat_state.text_input = Some(Arc::new(text_input_state.get_text_input( + &seat, + queue_handle, + TextInputData::default(), + ))); + } + } + + fn remove_capability( + &mut self, + _: &Connection, + _queue_handle: &QueueHandle, + seat: WlSeat, + capability: SeatCapability, + ) { + let seat_state = match self.seats.get_mut(&seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_seat::remove_capability for unknown seat"); + return; + } + }; + + if let Some(text_input) = seat_state.text_input.take() { + text_input.destroy(); + } + + match capability { + SeatCapability::Touch => { + if let Some(touch) = seat_state.touch.take() { + if touch.version() >= 3 { + touch.release(); + } + } + } + SeatCapability::Pointer => { + if let Some(relative_pointer) = seat_state.relative_pointer.take() { + relative_pointer.destroy(); + } + + if let Some(pointer) = seat_state.pointer.take() { + let pointer_data = pointer.pointer().winit_data(); + + // Remove the cursor from the mapping. + let surface_id = pointer.surface().id(); + let _ = self.pointer_surfaces.remove(&surface_id); + + // Remove the inner locks/confines before dropping the pointer. + pointer_data.unlock_pointer(); + pointer_data.unconfine_pointer(); + + if pointer.pointer().version() >= 3 { + pointer.pointer().release(); + } + } + } + SeatCapability::Keyboard => { + seat_state.keyboard_state = None; + self.on_keyboard_destroy(&seat.id()); + } + _ => (), + } + } + + fn new_seat( + &mut self, + _connection: &Connection, + _queue_handle: &QueueHandle, + seat: WlSeat, + ) { + self.seats.insert(seat.id(), WinitSeatState::new()); + } + + fn remove_seat( + &mut self, + _connection: &Connection, + _queue_handle: &QueueHandle, + seat: WlSeat, + ) { + let _ = self.seats.remove(&seat.id()); + self.on_keyboard_destroy(&seat.id()); + } +} + +impl WinitState { + fn on_keyboard_destroy(&mut self, seat: &ObjectId) { + for (window_id, window) in self.windows.get_mut() { + let mut window = window.lock().unwrap(); + let had_focus = window.has_focus(); + window.remove_seat_focus(seat); + if had_focus != window.has_focus() { + self.events_sink + .push_window_event(WindowEvent::Focused(false), *window_id); + } + } + } +} + +sctk::delegate_seat!(WinitState); diff --git a/rio-window/src/platform_impl/linux/wayland/seat/pointer/mod.rs b/rio-window/src/platform_impl/linux/wayland/seat/pointer/mod.rs new file mode 100644 index 00000000..c94b701f --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/seat/pointer/mod.rs @@ -0,0 +1,532 @@ +//! The pointer events. + +use std::ops::Deref; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use tracing::warn; + +use sctk::reexports::client::delegate_dispatch; +use sctk::reexports::client::protocol::wl_pointer::WlPointer; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{Connection, Proxy, QueueHandle, Dispatch}; +use sctk::reexports::protocols::wp::pointer_constraints::zv1::client::zwp_confined_pointer_v1::ZwpConfinedPointerV1; +use sctk::reexports::protocols::wp::pointer_constraints::zv1::client::zwp_locked_pointer_v1::ZwpLockedPointerV1; +use sctk::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::WpCursorShapeDeviceV1; +use sctk::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_manager_v1::WpCursorShapeManagerV1; +use sctk::reexports::protocols::wp::pointer_constraints::zv1::client::zwp_pointer_constraints_v1::{Lifetime, ZwpPointerConstraintsV1}; +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::csd_frame::FrameClick; + +use sctk::compositor::SurfaceData; +use sctk::globals::GlobalData; +use sctk::seat::pointer::{ + PointerData, PointerDataExt, PointerEvent, PointerEventKind, PointerHandler, +}; +use sctk::seat::SeatState; + +use crate::dpi::{LogicalPosition, PhysicalPosition}; +use crate::event::{ + ElementState, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent, +}; + +use crate::platform_impl::wayland::state::WinitState; +use crate::platform_impl::wayland::{self, DeviceId, WindowId}; + +pub mod relative_pointer; + +impl PointerHandler for WinitState { + fn pointer_frame( + &mut self, + connection: &Connection, + _: &QueueHandle, + pointer: &WlPointer, + events: &[PointerEvent], + ) { + let seat = pointer.winit_data().seat(); + let seat_state = match self.seats.get(&seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received pointer event without seat"); + return; + } + }; + + let themed_pointer = match seat_state.pointer.as_ref() { + Some(pointer) => pointer, + None => { + warn!("Received pointer event without pointer"); + return; + } + }; + + let device_id = + crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(DeviceId)); + + for event in events { + let surface = &event.surface; + + // The parent surface. + let parent_surface = match event.surface.data::() { + Some(data) => data.parent_surface().unwrap_or(surface), + None => continue, + }; + + let window_id = wayland::make_wid(parent_surface); + + // Ensure that window exists. + let mut window = match self.windows.get_mut().get_mut(&window_id) { + Some(window) => window.lock().unwrap(), + None => continue, + }; + + let scale_factor = window.scale_factor(); + let position: PhysicalPosition = + LogicalPosition::new(event.position.0, event.position.1) + .to_physical(scale_factor); + + match event.kind { + // Pointer movements on decorations. + PointerEventKind::Enter { .. } | PointerEventKind::Motion { .. } + if parent_surface != surface => + { + if let Some(icon) = window.frame_point_moved( + seat, + surface, + Duration::ZERO, + event.position.0, + event.position.1, + ) { + let _ = themed_pointer.set_cursor(connection, icon); + } + } + PointerEventKind::Leave { .. } if parent_surface != surface => { + window.frame_point_left(); + } + ref kind @ PointerEventKind::Press { + button, + serial, + time, + } + | ref kind @ PointerEventKind::Release { + button, + serial, + time, + } if parent_surface != surface => { + let click = match wayland_button_to_winit(button) { + MouseButton::Left => FrameClick::Normal, + MouseButton::Right => FrameClick::Alternate, + _ => continue, + }; + let pressed = matches!(kind, PointerEventKind::Press { .. }); + + // Emulate click on the frame. + window.frame_click( + click, + pressed, + seat, + serial, + Duration::from_millis(time as u64), + window_id, + &mut self.window_compositor_updates, + ); + } + // Regular events on the main surface. + PointerEventKind::Enter { .. } => { + self.events_sink.push_window_event( + WindowEvent::CursorEntered { device_id }, + window_id, + ); + + window.pointer_entered(Arc::downgrade(themed_pointer)); + + // Set the currently focused surface. + pointer.winit_data().inner.lock().unwrap().surface = Some(window_id); + + self.events_sink.push_window_event( + WindowEvent::CursorMoved { + device_id, + position, + }, + window_id, + ); + } + PointerEventKind::Leave { .. } => { + window.pointer_left(Arc::downgrade(themed_pointer)); + + // Remove the active surface. + pointer.winit_data().inner.lock().unwrap().surface = None; + + self.events_sink.push_window_event( + WindowEvent::CursorLeft { device_id }, + window_id, + ); + } + PointerEventKind::Motion { .. } => { + self.events_sink.push_window_event( + WindowEvent::CursorMoved { + device_id, + position, + }, + window_id, + ); + } + ref kind @ PointerEventKind::Press { button, serial, .. } + | ref kind @ PointerEventKind::Release { button, serial, .. } => { + // Update the last button serial. + pointer + .winit_data() + .inner + .lock() + .unwrap() + .latest_button_serial = serial; + + let button = wayland_button_to_winit(button); + let state = if matches!(kind, PointerEventKind::Press { .. }) { + ElementState::Pressed + } else { + ElementState::Released + }; + self.events_sink.push_window_event( + WindowEvent::MouseInput { + device_id, + state, + button, + }, + window_id, + ); + } + PointerEventKind::Axis { + horizontal, + vertical, + .. + } => { + // Get the current phase. + let mut pointer_data = pointer.winit_data().inner.lock().unwrap(); + + let has_discrete_scroll = + horizontal.discrete != 0 || vertical.discrete != 0; + + // Figure out what to do about start/ended phases here. + // + // Figure out how to deal with `Started`. Also the `Ended` is not guaranteed + // to be sent for mouse wheels. + let phase = if horizontal.stop || vertical.stop { + TouchPhase::Ended + } else { + match pointer_data.phase { + // Discrete scroll only results in moved events. + _ if has_discrete_scroll => TouchPhase::Moved, + TouchPhase::Started | TouchPhase::Moved => TouchPhase::Moved, + _ => TouchPhase::Started, + } + }; + + // Update the phase. + pointer_data.phase = phase; + + // Mice events have both pixel and discrete delta's at the same time. So prefer + // the descrite values if they are present. + let delta = if has_discrete_scroll { + // NOTE: Wayland sign convention is the inverse of winit. + MouseScrollDelta::LineDelta( + (-horizontal.discrete) as f32, + (-vertical.discrete) as f32, + ) + } else { + // NOTE: Wayland sign convention is the inverse of winit. + MouseScrollDelta::PixelDelta( + LogicalPosition::new( + -horizontal.absolute, + -vertical.absolute, + ) + .to_physical(scale_factor), + ) + }; + + self.events_sink.push_window_event( + WindowEvent::MouseWheel { + device_id, + delta, + phase, + }, + window_id, + ) + } + } + } + } +} + +#[derive(Debug)] +pub struct WinitPointerData { + /// The inner winit data associated with the pointer. + inner: Mutex, + + /// The data required by the sctk. + sctk_data: PointerData, +} + +impl WinitPointerData { + pub fn new(seat: WlSeat) -> Self { + Self { + inner: Mutex::new(WinitPointerDataInner::default()), + sctk_data: PointerData::new(seat), + } + } + + pub fn lock_pointer( + &self, + pointer_constraints: &PointerConstraintsState, + surface: &WlSurface, + pointer: &WlPointer, + queue_handle: &QueueHandle, + ) { + let mut inner = self.inner.lock().unwrap(); + if inner.locked_pointer.is_none() { + inner.locked_pointer = Some(pointer_constraints.lock_pointer( + surface, + pointer, + None, + Lifetime::Persistent, + queue_handle, + GlobalData, + )); + } + } + + pub fn unlock_pointer(&self) { + let mut inner = self.inner.lock().unwrap(); + if let Some(locked_pointer) = inner.locked_pointer.take() { + locked_pointer.destroy(); + } + } + + pub fn confine_pointer( + &self, + pointer_constraints: &PointerConstraintsState, + surface: &WlSurface, + pointer: &WlPointer, + queue_handle: &QueueHandle, + ) { + self.inner.lock().unwrap().confined_pointer = + Some(pointer_constraints.confine_pointer( + surface, + pointer, + None, + Lifetime::Persistent, + queue_handle, + GlobalData, + )); + } + + pub fn unconfine_pointer(&self) { + let inner = self.inner.lock().unwrap(); + if let Some(confined_pointer) = inner.confined_pointer.as_ref() { + confined_pointer.destroy(); + } + } + + /// Seat associated with this pointer. + pub fn seat(&self) -> &WlSeat { + self.sctk_data.seat() + } + + /// Active window. + pub fn focused_window(&self) -> Option { + self.inner.lock().unwrap().surface + } + + /// Last button serial. + pub fn latest_button_serial(&self) -> u32 { + self.sctk_data.latest_button_serial().unwrap_or_default() + } + + /// Last enter serial. + pub fn latest_enter_serial(&self) -> u32 { + self.sctk_data.latest_enter_serial().unwrap_or_default() + } + + pub fn set_locked_cursor_position(&self, surface_x: f64, surface_y: f64) { + let inner = self.inner.lock().unwrap(); + if let Some(locked_pointer) = inner.locked_pointer.as_ref() { + locked_pointer.set_cursor_position_hint(surface_x, surface_y); + } + } +} + +impl PointerDataExt for WinitPointerData { + fn pointer_data(&self) -> &PointerData { + &self.sctk_data + } +} + +#[derive(Debug)] +pub struct WinitPointerDataInner { + /// The associated locked pointer. + locked_pointer: Option, + + /// The associated confined pointer. + confined_pointer: Option, + + /// Serial of the last button event. + latest_button_serial: u32, + + /// Currently focused window. + surface: Option, + + /// Current axis phase. + phase: TouchPhase, +} + +impl Drop for WinitPointerDataInner { + fn drop(&mut self) { + if let Some(locked_pointer) = self.locked_pointer.take() { + locked_pointer.destroy(); + } + + if let Some(confined_pointer) = self.confined_pointer.take() { + confined_pointer.destroy(); + } + } +} + +impl Default for WinitPointerDataInner { + fn default() -> Self { + Self { + surface: None, + locked_pointer: None, + confined_pointer: None, + latest_button_serial: 0, + phase: TouchPhase::Ended, + } + } +} + +/// Convert the Wayland button into winit. +fn wayland_button_to_winit(button: u32) -> MouseButton { + // These values are coming from . + const BTN_LEFT: u32 = 0x110; + const BTN_RIGHT: u32 = 0x111; + const BTN_MIDDLE: u32 = 0x112; + const BTN_SIDE: u32 = 0x113; + const BTN_EXTRA: u32 = 0x114; + const BTN_FORWARD: u32 = 0x115; + const BTN_BACK: u32 = 0x116; + + match button { + BTN_LEFT => MouseButton::Left, + BTN_RIGHT => MouseButton::Right, + BTN_MIDDLE => MouseButton::Middle, + BTN_BACK | BTN_SIDE => MouseButton::Back, + BTN_FORWARD | BTN_EXTRA => MouseButton::Forward, + button => MouseButton::Other(button as u16), + } +} + +pub trait WinitPointerDataExt { + fn winit_data(&self) -> &WinitPointerData; +} + +impl WinitPointerDataExt for WlPointer { + fn winit_data(&self) -> &WinitPointerData { + self.data::() + .expect("failed to get pointer data.") + } +} + +pub struct PointerConstraintsState { + pointer_constraints: ZwpPointerConstraintsV1, +} + +impl PointerConstraintsState { + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let pointer_constraints = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { + pointer_constraints, + }) + } +} + +impl Deref for PointerConstraintsState { + type Target = ZwpPointerConstraintsV1; + + fn deref(&self) -> &Self::Target { + &self.pointer_constraints + } +} + +impl Dispatch + for PointerConstraintsState +{ + fn event( + _state: &mut WinitState, + _proxy: &ZwpPointerConstraintsV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for PointerConstraintsState { + fn event( + _state: &mut WinitState, + _proxy: &ZwpLockedPointerV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for PointerConstraintsState { + fn event( + _state: &mut WinitState, + _proxy: &ZwpConfinedPointerV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for SeatState { + fn event( + _: &mut WinitState, + _: &WpCursorShapeDeviceV1, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("wp_cursor_shape_manager has no events") + } +} + +impl Dispatch for SeatState { + fn event( + _: &mut WinitState, + _: &WpCursorShapeManagerV1, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("wp_cursor_device_manager has no events") + } +} + +delegate_dispatch!(WinitState: [ WlPointer: WinitPointerData] => SeatState); +delegate_dispatch!(WinitState: [ WpCursorShapeManagerV1: GlobalData] => SeatState); +delegate_dispatch!(WinitState: [ WpCursorShapeDeviceV1: GlobalData] => SeatState); +delegate_dispatch!(WinitState: [ZwpPointerConstraintsV1: GlobalData] => PointerConstraintsState); +delegate_dispatch!(WinitState: [ZwpLockedPointerV1: GlobalData] => PointerConstraintsState); +delegate_dispatch!(WinitState: [ZwpConfinedPointerV1: GlobalData] => PointerConstraintsState); diff --git a/rio-window/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs b/rio-window/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs new file mode 100644 index 00000000..8347618d --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs @@ -0,0 +1,97 @@ +//! Relative pointer. + +use std::ops::Deref; + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::{delegate_dispatch, Dispatch}; +use sctk::reexports::client::{Connection, QueueHandle}; +use sctk::reexports::protocols::wp::relative_pointer::zv1::{ + client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, + client::zwp_relative_pointer_v1::{self, ZwpRelativePointerV1}, +}; + +use sctk::globals::GlobalData; + +use crate::event::DeviceEvent; +use crate::platform_impl::wayland::state::WinitState; + +/// Wrapper around the relative pointer. +pub struct RelativePointerState { + manager: ZwpRelativePointerManagerV1, +} + +impl RelativePointerState { + /// Create new relative pointer manager. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { manager }) + } +} + +impl Deref for RelativePointerState { + type Target = ZwpRelativePointerManagerV1; + + fn deref(&self) -> &Self::Target { + &self.manager + } +} + +impl Dispatch + for RelativePointerState +{ + fn event( + _state: &mut WinitState, + _proxy: &ZwpRelativePointerManagerV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for RelativePointerState { + fn event( + state: &mut WinitState, + _proxy: &ZwpRelativePointerV1, + event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + let (dx_unaccel, dy_unaccel) = match event { + zwp_relative_pointer_v1::Event::RelativeMotion { + dx_unaccel, + dy_unaccel, + .. + } => (dx_unaccel, dy_unaccel), + _ => return, + }; + state.events_sink.push_device_event( + DeviceEvent::Motion { + axis: 0, + value: dx_unaccel, + }, + super::DeviceId, + ); + state.events_sink.push_device_event( + DeviceEvent::Motion { + axis: 1, + value: dy_unaccel, + }, + super::DeviceId, + ); + state.events_sink.push_device_event( + DeviceEvent::MouseMotion { + delta: (dx_unaccel, dy_unaccel), + }, + super::DeviceId, + ); + } +} + +delegate_dispatch!(WinitState: [ZwpRelativePointerV1: GlobalData] => RelativePointerState); +delegate_dispatch!(WinitState: [ZwpRelativePointerManagerV1: GlobalData] => RelativePointerState); diff --git a/rio-window/src/platform_impl/linux/wayland/seat/text_input/mod.rs b/rio-window/src/platform_impl/linux/wayland/seat/text_input/mod.rs new file mode 100644 index 00000000..4939e138 --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/seat/text_input/mod.rs @@ -0,0 +1,213 @@ +use std::ops::Deref; + +use sctk::globals::GlobalData; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{delegate_dispatch, Dispatch}; +use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; +use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::{ + ContentHint, ContentPurpose, Event as TextInputEvent, ZwpTextInputV3, +}; + +use crate::event::{Ime, WindowEvent}; +use crate::platform_impl::wayland; +use crate::platform_impl::wayland::state::WinitState; +use crate::window::ImePurpose; + +pub struct TextInputState { + text_input_manager: ZwpTextInputManagerV3, +} + +impl TextInputState { + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let text_input_manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { text_input_manager }) + } +} + +impl Deref for TextInputState { + type Target = ZwpTextInputManagerV3; + + fn deref(&self) -> &Self::Target { + &self.text_input_manager + } +} + +impl Dispatch for TextInputState { + fn event( + _state: &mut WinitState, + _proxy: &ZwpTextInputManagerV3, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for TextInputState { + fn event( + state: &mut WinitState, + text_input: &ZwpTextInputV3, + event: ::Event, + data: &TextInputData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + let windows = state.windows.get_mut(); + let mut text_input_data = data.inner.lock().unwrap(); + match event { + TextInputEvent::Enter { surface } => { + let window_id = wayland::make_wid(&surface); + text_input_data.surface = Some(surface); + + let mut window = match windows.get(&window_id) { + Some(window) => window.lock().unwrap(), + None => return, + }; + + if window.ime_allowed() { + text_input.enable(); + text_input.set_content_type_by_purpose(window.ime_purpose()); + text_input.commit(); + state + .events_sink + .push_window_event(WindowEvent::Ime(Ime::Enabled), window_id); + } + + window.text_input_entered(text_input); + } + TextInputEvent::Leave { surface } => { + text_input_data.surface = None; + + // Always issue a disable. + text_input.disable(); + text_input.commit(); + + let window_id = wayland::make_wid(&surface); + + // XXX this check is essential, because `leave` could have a + // reference to nil surface... + let mut window = match windows.get(&window_id) { + Some(window) => window.lock().unwrap(), + None => return, + }; + + window.text_input_left(text_input); + + state + .events_sink + .push_window_event(WindowEvent::Ime(Ime::Disabled), window_id); + } + TextInputEvent::PreeditString { + text, + cursor_begin, + cursor_end, + } => { + let text = text.unwrap_or_default(); + let cursor_begin = usize::try_from(cursor_begin) + .ok() + .and_then(|idx| text.is_char_boundary(idx).then_some(idx)); + let cursor_end = usize::try_from(cursor_end) + .ok() + .and_then(|idx| text.is_char_boundary(idx).then_some(idx)); + + text_input_data.pending_preedit = Some(Preedit { + text, + cursor_begin, + cursor_end, + }) + } + TextInputEvent::CommitString { text } => { + text_input_data.pending_preedit = None; + text_input_data.pending_commit = text; + } + TextInputEvent::Done { .. } => { + let window_id = match text_input_data.surface.as_ref() { + Some(surface) => wayland::make_wid(surface), + None => return, + }; + + // Clear preedit at the start of `Done`. + state.events_sink.push_window_event( + WindowEvent::Ime(Ime::Preedit(String::new(), None)), + window_id, + ); + + // Send `Commit`. + if let Some(text) = text_input_data.pending_commit.take() { + state.events_sink.push_window_event( + WindowEvent::Ime(Ime::Commit(text)), + window_id, + ); + } + + // Send preedit. + if let Some(preedit) = text_input_data.pending_preedit.take() { + let cursor_range = preedit + .cursor_begin + .map(|b| (b, preedit.cursor_end.unwrap_or(b))); + + state.events_sink.push_window_event( + WindowEvent::Ime(Ime::Preedit(preedit.text, cursor_range)), + window_id, + ); + } + } + TextInputEvent::DeleteSurroundingText { .. } => { + // Not handled. + } + _ => {} + } + } +} + +pub trait ZwpTextInputV3Ext { + fn set_content_type_by_purpose(&self, purpose: ImePurpose); +} + +impl ZwpTextInputV3Ext for ZwpTextInputV3 { + fn set_content_type_by_purpose(&self, purpose: ImePurpose) { + let (hint, purpose) = match purpose { + ImePurpose::Normal => (ContentHint::None, ContentPurpose::Normal), + ImePurpose::Password => { + (ContentHint::SensitiveData, ContentPurpose::Password) + } + ImePurpose::Terminal => (ContentHint::None, ContentPurpose::Terminal), + }; + self.set_content_type(hint, purpose); + } +} + +/// The Data associated with the text input. +#[derive(Default)] +pub struct TextInputData { + inner: std::sync::Mutex, +} + +#[derive(Default)] +pub struct TextInputDataInner { + /// The `WlSurface` we're performing input to. + surface: Option, + + /// The commit to submit on `done`. + pending_commit: Option, + + /// The preedit to submit on `done`. + pending_preedit: Option, +} + +/// The state of the preedit. +struct Preedit { + text: String, + cursor_begin: Option, + cursor_end: Option, +} + +delegate_dispatch!(WinitState: [ZwpTextInputManagerV3: GlobalData] => TextInputState); +delegate_dispatch!(WinitState: [ZwpTextInputV3: TextInputData] => TextInputState); diff --git a/rio-window/src/platform_impl/linux/wayland/seat/touch/mod.rs b/rio-window/src/platform_impl/linux/wayland/seat/touch/mod.rs new file mode 100644 index 00000000..9cd2abc6 --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/seat/touch/mod.rs @@ -0,0 +1,231 @@ +//! Touch handling. + +use tracing::warn; + +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::protocol::wl_touch::WlTouch; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; + +use sctk::seat::touch::{TouchData, TouchHandler}; + +use crate::dpi::LogicalPosition; +use crate::event::{Touch, TouchPhase, WindowEvent}; + +use crate::platform_impl::wayland::state::WinitState; +use crate::platform_impl::wayland::{self, DeviceId}; + +impl TouchHandler for WinitState { + fn down( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _: u32, + _: u32, + surface: WlSurface, + id: i32, + position: (f64, f64), + ) { + let window_id = wayland::make_wid(&surface); + let scale_factor = match self.windows.get_mut().get(&window_id) { + Some(window) => window.lock().unwrap().scale_factor(), + None => return, + }; + + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::down without seat"); + return; + } + }; + + // Update the state of the point. + let location = LogicalPosition::::from(position); + seat_state + .touch_map + .insert(id, TouchPoint { surface, location }); + + self.events_sink.push_window_event( + WindowEvent::Touch(Touch { + device_id: crate::event::DeviceId( + crate::platform_impl::DeviceId::Wayland(DeviceId), + ), + phase: TouchPhase::Started, + location: location.to_physical(scale_factor), + force: None, + id: id as u64, + }), + window_id, + ); + } + + fn up( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _: u32, + _: u32, + id: i32, + ) { + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::up without seat"); + return; + } + }; + + // Remove the touch point. + let touch_point = match seat_state.touch_map.remove(&id) { + Some(touch_point) => touch_point, + None => return, + }; + + let window_id = wayland::make_wid(&touch_point.surface); + let scale_factor = match self.windows.get_mut().get(&window_id) { + Some(window) => window.lock().unwrap().scale_factor(), + None => return, + }; + + self.events_sink.push_window_event( + WindowEvent::Touch(Touch { + device_id: crate::event::DeviceId( + crate::platform_impl::DeviceId::Wayland(DeviceId), + ), + phase: TouchPhase::Ended, + location: touch_point.location.to_physical(scale_factor), + force: None, + id: id as u64, + }), + window_id, + ); + } + + fn motion( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _: u32, + id: i32, + position: (f64, f64), + ) { + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::motion without seat"); + return; + } + }; + + // Remove the touch point. + let touch_point = match seat_state.touch_map.get_mut(&id) { + Some(touch_point) => touch_point, + None => return, + }; + + let window_id = wayland::make_wid(&touch_point.surface); + let scale_factor = match self.windows.get_mut().get(&window_id) { + Some(window) => window.lock().unwrap().scale_factor(), + None => return, + }; + + touch_point.location = LogicalPosition::::from(position); + + self.events_sink.push_window_event( + WindowEvent::Touch(Touch { + device_id: crate::event::DeviceId( + crate::platform_impl::DeviceId::Wayland(DeviceId), + ), + phase: TouchPhase::Moved, + location: touch_point.location.to_physical(scale_factor), + force: None, + id: id as u64, + }), + window_id, + ); + } + + fn cancel(&mut self, _: &Connection, _: &QueueHandle, touch: &WlTouch) { + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::cancel without seat"); + return; + } + }; + + for (id, touch_point) in seat_state.touch_map.drain() { + let window_id = wayland::make_wid(&touch_point.surface); + let scale_factor = match self.windows.get_mut().get(&window_id) { + Some(window) => window.lock().unwrap().scale_factor(), + None => return, + }; + + let location = touch_point.location.to_physical(scale_factor); + + self.events_sink.push_window_event( + WindowEvent::Touch(Touch { + device_id: crate::event::DeviceId( + crate::platform_impl::DeviceId::Wayland(DeviceId), + ), + phase: TouchPhase::Cancelled, + location, + force: None, + id: id as u64, + }), + window_id, + ); + } + } + + fn shape( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlTouch, + _: i32, + _: f64, + _: f64, + ) { + // Blank. + } + + fn orientation( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlTouch, + _: i32, + _: f64, + ) { + // Blank. + } +} + +/// The state of the touch point. +#[derive(Debug)] +pub struct TouchPoint { + /// The surface on which the point is present. + pub surface: WlSurface, + + /// The location of the point on the surface. + pub location: LogicalPosition, +} + +pub trait TouchDataExt { + fn seat(&self) -> &WlSeat; +} + +impl TouchDataExt for WlTouch { + fn seat(&self) -> &WlSeat { + self.data::() + .expect("failed to get touch data.") + .seat() + } +} + +sctk::delegate_touch!(WinitState); diff --git a/rio-window/src/platform_impl/linux/wayland/state.rs b/rio-window/src/platform_impl/linux/wayland/state.rs new file mode 100644 index 00000000..ff624069 --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/state.rs @@ -0,0 +1,468 @@ +use std::cell::RefCell; +use std::sync::atomic::Ordering; +use std::sync::{Arc, Mutex}; + +use ahash::AHashMap; + +use sctk::reexports::calloop::LoopHandle; +use sctk::reexports::client::backend::ObjectId; +use sctk::reexports::client::globals::GlobalList; +use sctk::reexports::client::protocol::wl_output::WlOutput; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; + +use sctk::compositor::{CompositorHandler, CompositorState}; +use sctk::output::{OutputHandler, OutputState}; +use sctk::registry::{ProvidesRegistryState, RegistryState}; +use sctk::seat::pointer::ThemedPointer; +use sctk::seat::SeatState; +use sctk::shell::xdg::window::{Window, WindowConfigure, WindowHandler}; +use sctk::shell::xdg::XdgShell; +use sctk::shell::WaylandSurface; +use sctk::shm::slot::SlotPool; +use sctk::shm::{Shm, ShmHandler}; +use sctk::subcompositor::SubcompositorState; + +use crate::platform_impl::wayland::event_loop::sink::EventSink; +use crate::platform_impl::wayland::output::MonitorHandle; +use crate::platform_impl::wayland::seat::{ + PointerConstraintsState, RelativePointerState, TextInputState, WinitPointerData, + WinitPointerDataExt, WinitSeatState, +}; +use crate::platform_impl::wayland::types::kwin_blur::KWinBlurManager; +use crate::platform_impl::wayland::types::wp_fractional_scaling::FractionalScalingManager; +use crate::platform_impl::wayland::types::wp_viewporter::ViewporterState; +use crate::platform_impl::wayland::types::xdg_activation::XdgActivationState; +use crate::platform_impl::wayland::window::{WindowRequests, WindowState}; +use crate::platform_impl::wayland::{WaylandError, WindowId}; +use crate::platform_impl::OsError; + +/// Winit's Wayland state. +pub struct WinitState { + /// The WlRegistry. + pub registry_state: RegistryState, + + /// The state of the WlOutput handling. + pub output_state: OutputState, + + /// The compositor state which is used to create new windows and regions. + pub compositor_state: Arc, + + /// The state of the subcompositor. + pub subcompositor_state: Option>, + + /// The seat state responsible for all sorts of input. + pub seat_state: SeatState, + + /// The shm for software buffers, such as cursors. + pub shm: Shm, + + /// The pool where custom cursors are allocated. + pub custom_cursor_pool: Arc>, + + /// The XDG shell that is used for windows. + pub xdg_shell: XdgShell, + + /// The currently present windows. + pub windows: RefCell>>>, + + /// The requests from the `Window` to EventLoop, such as close operations and redraw requests. + pub window_requests: RefCell>>, + + /// The events that were generated directly from the window. + pub window_events_sink: Arc>, + + /// The update for the `windows` coming from the compositor. + pub window_compositor_updates: Vec, + + /// Currently handled seats. + pub seats: AHashMap, + + /// Currently present cursor surfaces. + pub pointer_surfaces: AHashMap>>, + + /// The state of the text input on the client. + pub text_input_state: Option, + + /// Observed monitors. + pub monitors: Arc>>, + + /// Sink to accumulate window events from the compositor, which is latter dispatched in + /// event loop run. + pub events_sink: EventSink, + + /// Xdg activation. + pub xdg_activation: Option, + + /// Relative pointer. + pub relative_pointer: Option, + + /// Pointer constraints to handle pointer locking and confining. + pub pointer_constraints: Option>, + + /// Viewporter state on the given window. + pub viewporter_state: Option, + + /// Fractional scaling manager. + pub fractional_scaling_manager: Option, + + /// KWin blur manager. + pub kwin_blur_manager: Option, + + /// Loop handle to re-register event sources, such as keyboard repeat. + pub loop_handle: LoopHandle<'static, Self>, + + /// Whether we have dispatched events to the user thus we want to + /// send `AboutToWait` and normally wakeup the user. + pub dispatched_events: bool, +} + +impl WinitState { + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + loop_handle: LoopHandle<'static, WinitState>, + ) -> Result { + let registry_state = RegistryState::new(globals); + let compositor_state = + CompositorState::bind(globals, queue_handle).map_err(WaylandError::Bind)?; + let subcompositor_state = match SubcompositorState::bind( + compositor_state.wl_compositor().clone(), + globals, + queue_handle, + ) { + Ok(c) => Some(c), + Err(e) => { + tracing::warn!( + "Subcompositor protocol not available, ignoring CSD: {e:?}" + ); + None + } + }; + + let output_state = OutputState::new(globals, queue_handle); + let monitors = output_state.outputs().map(MonitorHandle::new).collect(); + + let seat_state = SeatState::new(globals, queue_handle); + + let mut seats = AHashMap::default(); + for seat in seat_state.seats() { + seats.insert(seat.id(), WinitSeatState::new()); + } + + let (viewporter_state, fractional_scaling_manager) = + if let Ok(fsm) = FractionalScalingManager::new(globals, queue_handle) { + (ViewporterState::new(globals, queue_handle).ok(), Some(fsm)) + } else { + (None, None) + }; + + let shm = Shm::bind(globals, queue_handle).map_err(WaylandError::Bind)?; + let custom_cursor_pool = Arc::new(Mutex::new(SlotPool::new(2, &shm).unwrap())); + + Ok(Self { + registry_state, + compositor_state: Arc::new(compositor_state), + subcompositor_state: subcompositor_state.map(Arc::new), + output_state, + seat_state, + shm, + custom_cursor_pool, + + xdg_shell: XdgShell::bind(globals, queue_handle) + .map_err(WaylandError::Bind)?, + xdg_activation: XdgActivationState::bind(globals, queue_handle).ok(), + + windows: Default::default(), + window_requests: Default::default(), + window_compositor_updates: Vec::new(), + window_events_sink: Default::default(), + viewporter_state, + fractional_scaling_manager, + kwin_blur_manager: KWinBlurManager::new(globals, queue_handle).ok(), + + seats, + text_input_state: TextInputState::new(globals, queue_handle).ok(), + + relative_pointer: RelativePointerState::new(globals, queue_handle).ok(), + pointer_constraints: PointerConstraintsState::new(globals, queue_handle) + .map(Arc::new) + .ok(), + pointer_surfaces: Default::default(), + + monitors: Arc::new(Mutex::new(monitors)), + events_sink: EventSink::new(), + loop_handle, + // Make it true by default. + dispatched_events: true, + }) + } + + pub fn scale_factor_changed( + &mut self, + surface: &WlSurface, + scale_factor: f64, + is_legacy: bool, + ) { + // Check if the cursor surface. + let window_id = super::make_wid(surface); + + if let Some(window) = self.windows.get_mut().get(&window_id) { + // Don't update the scaling factor, when legacy method is used. + if is_legacy && self.fractional_scaling_manager.is_some() { + return; + } + + // The scale factor change is for the window. + let pos = if let Some(pos) = self + .window_compositor_updates + .iter() + .position(|update| update.window_id == window_id) + { + pos + } else { + self.window_compositor_updates + .push(WindowCompositorUpdate::new(window_id)); + self.window_compositor_updates.len() - 1 + }; + + // Update the scale factor right away. + window.lock().unwrap().set_scale_factor(scale_factor); + self.window_compositor_updates[pos].scale_changed = true; + } else if let Some(pointer) = self.pointer_surfaces.get(&surface.id()) { + // Get the window, where the pointer resides right now. + let focused_window = match pointer.pointer().winit_data().focused_window() { + Some(focused_window) => focused_window, + None => return, + }; + + if let Some(window_state) = self.windows.get_mut().get(&focused_window) { + window_state.lock().unwrap().reload_cursor_style() + } + } + } + + pub fn queue_close(updates: &mut Vec, window_id: WindowId) { + let pos = if let Some(pos) = updates + .iter() + .position(|update| update.window_id == window_id) + { + pos + } else { + updates.push(WindowCompositorUpdate::new(window_id)); + updates.len() - 1 + }; + + updates[pos].close_window = true; + } +} + +impl ShmHandler for WinitState { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm + } +} + +impl WindowHandler for WinitState { + fn request_close(&mut self, _: &Connection, _: &QueueHandle, window: &Window) { + let window_id = super::make_wid(window.wl_surface()); + Self::queue_close(&mut self.window_compositor_updates, window_id); + } + + fn configure( + &mut self, + _: &Connection, + _: &QueueHandle, + window: &Window, + configure: WindowConfigure, + _serial: u32, + ) { + let window_id = super::make_wid(window.wl_surface()); + + let pos = if let Some(pos) = self + .window_compositor_updates + .iter() + .position(|update| update.window_id == window_id) + { + pos + } else { + self.window_compositor_updates + .push(WindowCompositorUpdate::new(window_id)); + self.window_compositor_updates.len() - 1 + }; + + // Populate the configure to the window. + self.window_compositor_updates[pos].resized |= self + .windows + .get_mut() + .get_mut(&window_id) + .expect("got configure for dead window.") + .lock() + .unwrap() + .configure(configure, &self.shm, &self.subcompositor_state); + + // NOTE: configure demands wl_surface::commit, however winit doesn't commit on behalf of the + // users, since it can break a lot of things, thus it'll ask users to redraw instead. + self.window_requests + .get_mut() + .get(&window_id) + .unwrap() + .redraw_requested + .store(true, Ordering::Relaxed); + + // Manually mark that we've got an event, since configure may not generate a resize. + self.dispatched_events = true; + } +} + +impl OutputHandler for WinitState { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + + fn new_output(&mut self, _: &Connection, _: &QueueHandle, output: WlOutput) { + self.monitors + .lock() + .unwrap() + .push(MonitorHandle::new(output)); + } + + fn update_output( + &mut self, + _: &Connection, + _: &QueueHandle, + updated: WlOutput, + ) { + let mut monitors = self.monitors.lock().unwrap(); + let updated = MonitorHandle::new(updated); + if let Some(pos) = monitors.iter().position(|output| output == &updated) { + monitors[pos] = updated + } else { + monitors.push(updated) + } + } + + fn output_destroyed( + &mut self, + _: &Connection, + _: &QueueHandle, + removed: WlOutput, + ) { + let mut monitors = self.monitors.lock().unwrap(); + let removed = MonitorHandle::new(removed); + if let Some(pos) = monitors.iter().position(|output| output == &removed) { + monitors.remove(pos); + } + } +} + +impl CompositorHandler for WinitState { + fn transform_changed( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlSurface, + _: wayland_client::protocol::wl_output::Transform, + ) { + // TODO(kchibisov) we need to expose it somehow in winit. + } + + fn surface_enter( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlSurface, + _: &WlOutput, + ) { + } + + fn surface_leave( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlSurface, + _: &WlOutput, + ) { + } + + fn scale_factor_changed( + &mut self, + _: &Connection, + _: &QueueHandle, + surface: &WlSurface, + scale_factor: i32, + ) { + self.scale_factor_changed(surface, scale_factor as f64, true) + } + + fn frame( + &mut self, + _: &Connection, + _: &QueueHandle, + surface: &WlSurface, + _: u32, + ) { + let window_id = super::make_wid(surface); + let window = match self.windows.get_mut().get(&window_id) { + Some(window) => window, + None => return, + }; + + // In case we have a redraw requested we must indicate the wake up. + if self + .window_requests + .get_mut() + .get(&window_id) + .unwrap() + .redraw_requested + .load(Ordering::Relaxed) + { + self.dispatched_events = true; + } + + window.lock().unwrap().frame_callback_received(); + } +} + +impl ProvidesRegistryState for WinitState { + sctk::registry_handlers![OutputState, SeatState]; + + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } +} + +// The window update coming from the compositor. +#[derive(Debug, Clone, Copy)] +pub struct WindowCompositorUpdate { + /// The id of the window this updates belongs to. + pub window_id: WindowId, + + /// New window size. + pub resized: bool, + + /// New scale factor. + pub scale_changed: bool, + + /// Close the window. + pub close_window: bool, +} + +impl WindowCompositorUpdate { + fn new(window_id: WindowId) -> Self { + Self { + window_id, + resized: false, + scale_changed: false, + close_window: false, + } + } +} + +sctk::delegate_subcompositor!(WinitState); +sctk::delegate_compositor!(WinitState); +sctk::delegate_output!(WinitState); +sctk::delegate_registry!(WinitState); +sctk::delegate_shm!(WinitState); +sctk::delegate_xdg_shell!(WinitState); +sctk::delegate_xdg_window!(WinitState); diff --git a/rio-window/src/platform_impl/linux/wayland/types/cursor.rs b/rio-window/src/platform_impl/linux/wayland/types/cursor.rs new file mode 100644 index 00000000..cd326444 --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/types/cursor.rs @@ -0,0 +1,61 @@ +use cursor_icon::CursorIcon; + +use sctk::reexports::client::protocol::wl_shm::Format; +use sctk::shm::slot::{Buffer, SlotPool}; + +use crate::cursor::CursorImage; + +#[derive(Debug)] +pub enum SelectedCursor { + Named(CursorIcon), + Custom(CustomCursor), +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Named(Default::default()) + } +} + +#[derive(Debug)] +pub struct CustomCursor { + pub buffer: Buffer, + pub w: i32, + pub h: i32, + pub hotspot_x: i32, + pub hotspot_y: i32, +} + +impl CustomCursor { + pub(crate) fn new(pool: &mut SlotPool, image: &CursorImage) -> Self { + let (buffer, canvas) = pool + .create_buffer( + image.width as i32, + image.height as i32, + 4 * (image.width as i32), + Format::Argb8888, + ) + .unwrap(); + + for (canvas_chunk, rgba) in + canvas.chunks_exact_mut(4).zip(image.rgba.chunks_exact(4)) + { + // Alpha in buffer is premultiplied. + let alpha = rgba[3] as f32 / 255.; + let r = (rgba[0] as f32 * alpha) as u32; + let g = (rgba[1] as f32 * alpha) as u32; + let b = (rgba[2] as f32 * alpha) as u32; + let color = ((rgba[3] as u32) << 24) + (r << 16) + (g << 8) + b; + let array: &mut [u8; 4] = canvas_chunk.try_into().unwrap(); + *array = color.to_le_bytes(); + } + + CustomCursor { + buffer, + w: image.width as i32, + h: image.height as i32, + hotspot_x: image.hotspot_x as i32, + hotspot_y: image.hotspot_y as i32, + } + } +} diff --git a/rio-window/src/platform_impl/linux/wayland/types/kwin_blur.rs b/rio-window/src/platform_impl/linux/wayland/types/kwin_blur.rs new file mode 100644 index 00000000..f12eafea --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/types/kwin_blur.rs @@ -0,0 +1,70 @@ +//! Handling of KDE-compatible blur. + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{ + delegate_dispatch, Connection, Dispatch, Proxy, QueueHandle, +}; +use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur; +use wayland_protocols_plasma::blur::client::org_kde_kwin_blur_manager::OrgKdeKwinBlurManager; + +use sctk::globals::GlobalData; + +use crate::platform_impl::wayland::state::WinitState; + +/// KWin blur manager. +#[derive(Debug, Clone)] +pub struct KWinBlurManager { + manager: OrgKdeKwinBlurManager, +} + +impl KWinBlurManager { + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { manager }) + } + + pub fn blur( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle, + ) -> OrgKdeKwinBlur { + self.manager.create(surface, queue_handle, ()) + } + + pub fn unset(&self, surface: &WlSurface) { + self.manager.unset(surface) + } +} + +impl Dispatch for KWinBlurManager { + fn event( + _: &mut WinitState, + _: &OrgKdeKwinBlurManager, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("no events defined for org_kde_kwin_blur_manager"); + } +} + +impl Dispatch for KWinBlurManager { + fn event( + _: &mut WinitState, + _: &OrgKdeKwinBlur, + _: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("no events defined for org_kde_kwin_blur"); + } +} + +delegate_dispatch!(WinitState: [OrgKdeKwinBlurManager: GlobalData] => KWinBlurManager); +delegate_dispatch!(WinitState: [OrgKdeKwinBlur: ()] => KWinBlurManager); diff --git a/rio-window/src/platform_impl/linux/wayland/types/mod.rs b/rio-window/src/platform_impl/linux/wayland/types/mod.rs new file mode 100644 index 00000000..77e67f48 --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/types/mod.rs @@ -0,0 +1,7 @@ +//! Wayland protocol implementation boilerplate. + +pub mod cursor; +pub mod kwin_blur; +pub mod wp_fractional_scaling; +pub mod wp_viewporter; +pub mod xdg_activation; diff --git a/rio-window/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs b/rio-window/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs new file mode 100644 index 00000000..3dba4327 --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs @@ -0,0 +1,89 @@ +//! Handling of the fractional scaling. + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{delegate_dispatch, Connection, Dispatch, Proxy, QueueHandle}; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::{ + Event as FractionalScalingEvent, WpFractionalScaleV1, +}; + +use sctk::globals::GlobalData; + +use crate::platform_impl::wayland::state::WinitState; + +/// The scaling factor denominator. +const SCALE_DENOMINATOR: f64 = 120.; + +/// Fractional scaling manager. +#[derive(Debug)] +pub struct FractionalScalingManager { + manager: WpFractionalScaleManagerV1, +} + +pub struct FractionalScaling { + /// The surface used for scaling. + surface: WlSurface, +} + +impl FractionalScalingManager { + /// Create new viewporter. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { manager }) + } + + pub fn fractional_scaling( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle, + ) -> WpFractionalScaleV1 { + let data = FractionalScaling { + surface: surface.clone(), + }; + self.manager + .get_fractional_scale(surface, queue_handle, data) + } +} + +impl Dispatch + for FractionalScalingManager +{ + fn event( + _: &mut WinitState, + _: &WpFractionalScaleManagerV1, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + // No events. + } +} + +impl Dispatch + for FractionalScalingManager +{ + fn event( + state: &mut WinitState, + _: &WpFractionalScaleV1, + event: ::Event, + data: &FractionalScaling, + _: &Connection, + _: &QueueHandle, + ) { + if let FractionalScalingEvent::PreferredScale { scale } = event { + state.scale_factor_changed( + &data.surface, + scale as f64 / SCALE_DENOMINATOR, + false, + ); + } + } +} + +delegate_dispatch!(WinitState: [WpFractionalScaleManagerV1: GlobalData] => FractionalScalingManager); +delegate_dispatch!(WinitState: [WpFractionalScaleV1: FractionalScaling] => FractionalScalingManager); diff --git a/rio-window/src/platform_impl/linux/wayland/types/wp_viewporter.rs b/rio-window/src/platform_impl/linux/wayland/types/wp_viewporter.rs new file mode 100644 index 00000000..47f946db --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/types/wp_viewporter.rs @@ -0,0 +1,68 @@ +//! Handling of the wp-viewporter. + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{ + delegate_dispatch, Connection, Dispatch, Proxy, QueueHandle, +}; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewporter::WpViewporter; + +use sctk::globals::GlobalData; + +use crate::platform_impl::wayland::state::WinitState; + +/// Viewporter. +#[derive(Debug)] +pub struct ViewporterState { + viewporter: WpViewporter, +} + +impl ViewporterState { + /// Create new viewporter. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let viewporter = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { viewporter }) + } + + /// Get the viewport for the given object. + pub fn get_viewport( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle, + ) -> WpViewport { + self.viewporter + .get_viewport(surface, queue_handle, GlobalData) + } +} + +impl Dispatch for ViewporterState { + fn event( + _: &mut WinitState, + _: &WpViewporter, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + // No events. + } +} +impl Dispatch for ViewporterState { + fn event( + _: &mut WinitState, + _: &WpViewport, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + // No events. + } +} + +delegate_dispatch!(WinitState: [WpViewporter: GlobalData] => ViewporterState); +delegate_dispatch!(WinitState: [WpViewport: GlobalData] => ViewporterState); diff --git a/rio-window/src/platform_impl/linux/wayland/types/xdg_activation.rs b/rio-window/src/platform_impl/linux/wayland/types/xdg_activation.rs new file mode 100644 index 00000000..fe366bab --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/types/xdg_activation.rs @@ -0,0 +1,108 @@ +//! Handling of xdg activation, which is used for user attention requests. + +use std::sync::atomic::AtomicBool; +use std::sync::Weak; + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{ + delegate_dispatch, Connection, Dispatch, Proxy, QueueHandle, +}; +use sctk::reexports::protocols::xdg::activation::v1::client::xdg_activation_token_v1::{ + Event as ActivationTokenEvent, XdgActivationTokenV1, +}; +use sctk::reexports::protocols::xdg::activation::v1::client::xdg_activation_v1::XdgActivationV1; + +use sctk::globals::GlobalData; + +use crate::event_loop::AsyncRequestSerial; +use crate::platform_impl::wayland::state::WinitState; +use crate::platform_impl::WindowId; +use crate::window::ActivationToken; + +pub struct XdgActivationState { + xdg_activation: XdgActivationV1, +} + +impl XdgActivationState { + pub fn bind( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let xdg_activation = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { xdg_activation }) + } + + pub fn global(&self) -> &XdgActivationV1 { + &self.xdg_activation + } +} + +impl Dispatch for XdgActivationState { + fn event( + _state: &mut WinitState, + _proxy: &XdgActivationV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch + for XdgActivationState +{ + fn event( + state: &mut WinitState, + proxy: &XdgActivationTokenV1, + event: ::Event, + data: &XdgActivationTokenData, + _: &Connection, + _: &QueueHandle, + ) { + let token = match event { + ActivationTokenEvent::Done { token } => token, + _ => return, + }; + + let global = state + .xdg_activation + .as_ref() + .expect("got xdg_activation event without global.") + .global(); + + match data { + XdgActivationTokenData::Attention((surface, fence)) => { + global.activate(token, surface); + // Mark that no request attention is in process. + if let Some(attention_requested) = fence.upgrade() { + attention_requested + .store(false, std::sync::atomic::Ordering::Relaxed); + } + } + XdgActivationTokenData::Obtain((window_id, serial)) => { + state.events_sink.push_window_event( + crate::event::WindowEvent::ActivationTokenDone { + serial: *serial, + token: ActivationToken::_new(token), + }, + *window_id, + ); + } + } + + proxy.destroy(); + } +} + +/// The data associated with the activation request. +pub enum XdgActivationTokenData { + /// Request user attention for the given surface. + Attention((WlSurface, Weak)), + /// Get a token to be passed outside of the winit. + Obtain((WindowId, AsyncRequestSerial)), +} + +delegate_dispatch!(WinitState: [ XdgActivationV1: GlobalData] => XdgActivationState); +delegate_dispatch!(WinitState: [ XdgActivationTokenV1: XdgActivationTokenData] => XdgActivationState); diff --git a/rio-window/src/platform_impl/linux/wayland/window/mod.rs b/rio-window/src/platform_impl/linux/wayland/window/mod.rs new file mode 100644 index 00000000..331c412a --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/window/mod.rs @@ -0,0 +1,768 @@ +//! The Wayland window. + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +use sctk::reexports::client::protocol::wl_display::WlDisplay; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{Proxy, QueueHandle}; + +use sctk::compositor::{CompositorState, Region, SurfaceData}; +use sctk::reexports::protocols::xdg::activation::v1::client::xdg_activation_v1::XdgActivationV1; +use sctk::shell::xdg::window::{Window as SctkWindow, WindowDecorations}; +use sctk::shell::WaylandSurface; + +use tracing::warn; + +use crate::dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event::{Ime, WindowEvent}; +use crate::event_loop::AsyncRequestSerial; +use crate::platform_impl::{ + Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, PlatformIcon, +}; +use crate::window::{ + Cursor, CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowLevel, +}; + +use super::event_loop::sink::EventSink; +use super::output::MonitorHandle; +use super::state::WinitState; +use super::types::xdg_activation::XdgActivationTokenData; +use super::{ActiveEventLoop, WaylandError, WindowId}; + +pub(crate) mod state; + +pub use state::WindowState; + +/// The Wayland window. +pub struct Window { + /// Reference to the underlying SCTK window. + window: SctkWindow, + + /// Window id. + window_id: WindowId, + + /// The state of the window. + window_state: Arc>, + + /// Compositor to handle WlRegion stuff. + compositor: Arc, + + /// The wayland display used solely for raw window handle. + #[allow(dead_code)] + display: WlDisplay, + + /// Xdg activation to request user attention. + xdg_activation: Option, + + /// The state of the requested attention from the `xdg_activation`. + attention_requested: Arc, + + /// Handle to the main queue to perform requests. + queue_handle: QueueHandle, + + /// Window requests to the event loop. + window_requests: Arc, + + /// Observed monitors. + monitors: Arc>>, + + /// Source to wake-up the event-loop for window requests. + event_loop_awakener: calloop::ping::Ping, + + /// The event sink to deliver synthetic events. + window_events_sink: Arc>, +} + +impl Window { + pub(crate) fn new( + event_loop_window_target: &ActiveEventLoop, + attributes: WindowAttributes, + ) -> Result { + let queue_handle = event_loop_window_target.queue_handle.clone(); + let mut state = event_loop_window_target.state.borrow_mut(); + + let monitors = state.monitors.clone(); + + let surface = state.compositor_state.create_surface(&queue_handle); + let compositor = state.compositor_state.clone(); + let xdg_activation = state + .xdg_activation + .as_ref() + .map(|activation_state| activation_state.global().clone()); + let display = event_loop_window_target.connection.display(); + + let size: Size = attributes + .inner_size + .unwrap_or(LogicalSize::new(800., 600.).into()); + + // We prefer server side decorations, however to not have decorations we ask for client + // side decorations instead. + let default_decorations = if attributes.decorations { + WindowDecorations::RequestServer + } else { + WindowDecorations::RequestClient + }; + + let window = state.xdg_shell.create_window( + surface.clone(), + default_decorations, + &queue_handle, + ); + + let mut window_state = WindowState::new( + event_loop_window_target.connection.clone(), + &event_loop_window_target.queue_handle, + &state, + size, + window.clone(), + attributes.preferred_theme, + ); + + // Set transparency hint. + window_state.set_transparent(attributes.transparent); + + window_state.set_blur(attributes.blur); + + // Set the decorations hint. + window_state.set_decorate(attributes.decorations); + + // Set the app_id. + if let Some(name) = attributes.platform_specific.name.map(|name| name.general) { + window.set_app_id(name); + } + + // Set the window title. + window_state.set_title(attributes.title); + + // Set the min and max sizes. We must set the hints upon creating a window, so + // we use the default `1.` scaling... + let min_size = attributes.min_inner_size.map(|size| size.to_logical(1.)); + let max_size = attributes.max_inner_size.map(|size| size.to_logical(1.)); + window_state.set_min_inner_size(min_size); + window_state.set_max_inner_size(max_size); + + // Non-resizable implies that the min and max sizes are set to the same value. + window_state.set_resizable(attributes.resizable); + + // Set startup mode. + match attributes.fullscreen.map(Into::into) { + Some(Fullscreen::Exclusive(_)) => { + warn!("`Fullscreen::Exclusive` is ignored on Wayland"); + } + #[cfg_attr(not(x11_platform), allow(clippy::bind_instead_of_map))] + Some(Fullscreen::Borderless(monitor)) => { + let output = monitor.and_then(|monitor| match monitor { + PlatformMonitorHandle::Wayland(monitor) => Some(monitor.proxy), + #[cfg(x11_platform)] + PlatformMonitorHandle::X(_) => None, + }); + + window.set_fullscreen(output.as_ref()) + } + _ if attributes.maximized => window.set_maximized(), + _ => (), + }; + + match attributes.cursor { + Cursor::Icon(icon) => window_state.set_cursor(icon), + Cursor::Custom(cursor) => window_state.set_custom_cursor(cursor), + } + + // Activate the window when the token is passed. + if let (Some(xdg_activation), Some(token)) = ( + xdg_activation.as_ref(), + attributes.platform_specific.activation_token, + ) { + xdg_activation.activate(token._token, &surface); + } + + // XXX Do initial commit. + window.commit(); + + // Add the window and window requests into the state. + let window_state = Arc::new(Mutex::new(window_state)); + let window_id = super::make_wid(&surface); + state + .windows + .get_mut() + .insert(window_id, window_state.clone()); + + let window_requests = WindowRequests { + redraw_requested: AtomicBool::new(true), + closed: AtomicBool::new(false), + }; + let window_requests = Arc::new(window_requests); + state + .window_requests + .get_mut() + .insert(window_id, window_requests.clone()); + + // Setup the event sync to insert `WindowEvents` right from the window. + let window_events_sink = state.window_events_sink.clone(); + + let mut wayland_source = + event_loop_window_target.wayland_dispatcher.as_source_mut(); + let event_queue = wayland_source.queue(); + + // Do a roundtrip. + event_queue.roundtrip(&mut state).map_err(|error| { + os_error!(OsError::WaylandError(Arc::new(WaylandError::Dispatch( + error + )))) + })?; + + // XXX Wait for the initial configure to arrive. + while !window_state.lock().unwrap().is_configured() { + event_queue.blocking_dispatch(&mut state).map_err(|error| { + os_error!(OsError::WaylandError(Arc::new(WaylandError::Dispatch( + error + )))) + })?; + } + + // Wake-up event loop, so it'll send initial redraw requested. + let event_loop_awakener = event_loop_window_target.event_loop_awakener.clone(); + event_loop_awakener.ping(); + + Ok(Self { + window, + display, + monitors, + window_id, + compositor, + window_state, + queue_handle, + xdg_activation, + attention_requested: Arc::new(AtomicBool::new(false)), + event_loop_awakener, + window_requests, + window_events_sink, + }) + } +} + +impl Window { + #[inline] + pub fn id(&self) -> WindowId { + self.window_id + } + + #[inline] + pub fn set_title(&self, title: impl ToString) { + let new_title = title.to_string(); + self.window_state.lock().unwrap().set_title(new_title); + } + + #[inline] + pub fn set_visible(&self, _visible: bool) { + // Not possible on Wayland. + } + + #[inline] + pub fn is_visible(&self) -> Option { + None + } + + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + Err(NotSupportedError::new()) + } + + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + Err(NotSupportedError::new()) + } + + #[inline] + pub fn set_outer_position(&self, _: Position) { + // Not possible on Wayland. + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let window_state = self.window_state.lock().unwrap(); + let scale_factor = window_state.scale_factor(); + super::logical_to_physical_rounded(window_state.inner_size(), scale_factor) + } + + #[inline] + pub fn request_redraw(&self) { + // NOTE: try to not wake up the loop when the event was already scheduled and not yet + // processed by the loop, because if at this point the value was `true` it could only + // mean that the loop still haven't dispatched the value to the client and will do + // eventually, resetting it to `false`. + if self + .window_requests + .redraw_requested + .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + self.event_loop_awakener.ping(); + } + } + + #[inline] + pub fn pre_present_notify(&self) { + self.window_state.lock().unwrap().request_frame_callback(); + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + let window_state = self.window_state.lock().unwrap(); + let scale_factor = window_state.scale_factor(); + super::logical_to_physical_rounded(window_state.outer_size(), scale_factor) + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let mut window_state = self.window_state.lock().unwrap(); + let new_size = window_state.request_inner_size(size); + self.request_redraw(); + Some(new_size) + } + + /// Set the minimum inner size for the window. + #[inline] + pub fn set_min_inner_size(&self, min_size: Option) { + let scale_factor = self.scale_factor(); + let min_size = min_size.map(|size| size.to_logical(scale_factor)); + self.window_state + .lock() + .unwrap() + .set_min_inner_size(min_size); + // NOTE: Requires commit to be applied. + self.request_redraw(); + } + + /// Set the maximum inner size for the window. + #[inline] + pub fn set_max_inner_size(&self, max_size: Option) { + let scale_factor = self.scale_factor(); + let max_size = max_size.map(|size| size.to_logical(scale_factor)); + self.window_state + .lock() + .unwrap() + .set_max_inner_size(max_size); + // NOTE: Requires commit to be applied. + self.request_redraw(); + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + None + } + + #[inline] + pub fn set_resize_increments(&self, _increments: Option) { + warn!("`set_resize_increments` is not implemented for Wayland"); + } + + #[inline] + pub fn set_transparent(&self, transparent: bool) { + self.window_state + .lock() + .unwrap() + .set_transparent(transparent); + } + + #[inline] + pub fn has_focus(&self) -> bool { + self.window_state.lock().unwrap().has_focus() + } + + #[inline] + pub fn is_minimized(&self) -> Option { + // XXX clients don't know whether they are minimized or not. + None + } + + #[inline] + pub fn show_window_menu(&self, position: Position) { + let scale_factor = self.scale_factor(); + let position = position.to_logical(scale_factor); + self.window_state.lock().unwrap().show_window_menu(position); + } + + #[inline] + pub fn drag_resize_window( + &self, + direction: ResizeDirection, + ) -> Result<(), ExternalError> { + self.window_state + .lock() + .unwrap() + .drag_resize_window(direction) + } + + #[inline] + pub fn set_resizable(&self, resizable: bool) { + if self.window_state.lock().unwrap().set_resizable(resizable) { + // NOTE: Requires commit to be applied. + self.request_redraw(); + } + } + + #[inline] + pub fn is_resizable(&self) -> bool { + self.window_state.lock().unwrap().resizable() + } + + #[inline] + pub fn set_enabled_buttons(&self, _buttons: WindowButtons) { + // TODO(kchibisov) v5 of the xdg_shell allows that. + } + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + // TODO(kchibisov) v5 of the xdg_shell allows that. + WindowButtons::all() + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.window_state.lock().unwrap().scale_factor() + } + + #[inline] + pub fn set_blur(&self, blur: bool) { + self.window_state.lock().unwrap().set_blur(blur); + } + + #[inline] + pub fn set_decorations(&self, decorate: bool) { + self.window_state.lock().unwrap().set_decorate(decorate) + } + + #[inline] + pub fn is_decorated(&self) -> bool { + self.window_state.lock().unwrap().is_decorated() + } + + #[inline] + pub fn set_window_level(&self, _level: WindowLevel) {} + + #[inline] + pub(crate) fn set_window_icon(&self, _window_icon: Option) {} + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + // You can't unminimize the window on Wayland. + if !minimized { + warn!("Unminimizing is ignored on Wayland."); + return; + } + + self.window.set_minimized(); + } + + #[inline] + pub fn is_maximized(&self) -> bool { + self.window_state + .lock() + .unwrap() + .last_configure + .as_ref() + .map(|last_configure| last_configure.is_maximized()) + .unwrap_or_default() + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + if maximized { + self.window.set_maximized() + } else { + self.window.unset_maximized() + } + } + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + let is_fullscreen = self + .window_state + .lock() + .unwrap() + .last_configure + .as_ref() + .map(|last_configure| last_configure.is_fullscreen()) + .unwrap_or_default(); + + if is_fullscreen { + let current_monitor = + self.current_monitor().map(PlatformMonitorHandle::Wayland); + Some(Fullscreen::Borderless(current_monitor)) + } else { + None + } + } + + #[inline] + pub(crate) fn set_fullscreen(&self, fullscreen: Option) { + match fullscreen { + Some(Fullscreen::Exclusive(_)) => { + warn!("`Fullscreen::Exclusive` is ignored on Wayland"); + } + #[cfg_attr(not(x11_platform), allow(clippy::bind_instead_of_map))] + Some(Fullscreen::Borderless(monitor)) => { + let output = monitor.and_then(|monitor| match monitor { + PlatformMonitorHandle::Wayland(monitor) => Some(monitor.proxy), + #[cfg(x11_platform)] + PlatformMonitorHandle::X(_) => None, + }); + + self.window.set_fullscreen(output.as_ref()) + } + None => self.window.unset_fullscreen(), + } + } + + #[inline] + pub fn set_cursor(&self, cursor: Cursor) { + let window_state = &mut self.window_state.lock().unwrap(); + + match cursor { + Cursor::Icon(icon) => window_state.set_cursor(icon), + Cursor::Custom(cursor) => window_state.set_custom_cursor(cursor), + } + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + self.window_state + .lock() + .unwrap() + .set_cursor_visible(visible); + } + + pub fn request_user_attention(&self, request_type: Option) { + let xdg_activation = match self.xdg_activation.as_ref() { + Some(xdg_activation) => xdg_activation, + None => { + warn!("`request_user_attention` isn't supported"); + return; + } + }; + + // Urgency is only removed by the compositor and there's no need to raise urgency when it + // was already raised. + if request_type.is_none() || self.attention_requested.load(Ordering::Relaxed) { + return; + } + + self.attention_requested.store(true, Ordering::Relaxed); + let surface = self.surface().clone(); + let data = XdgActivationTokenData::Attention(( + surface.clone(), + Arc::downgrade(&self.attention_requested), + )); + let xdg_activation_token = + xdg_activation.get_activation_token(&self.queue_handle, data); + xdg_activation_token.set_surface(&surface); + xdg_activation_token.commit(); + } + + pub fn request_activation_token( + &self, + ) -> Result { + let xdg_activation = match self.xdg_activation.as_ref() { + Some(xdg_activation) => xdg_activation, + None => return Err(NotSupportedError::new()), + }; + + let serial = AsyncRequestSerial::get(); + + let data = XdgActivationTokenData::Obtain((self.window_id, serial)); + let xdg_activation_token = + xdg_activation.get_activation_token(&self.queue_handle, data); + xdg_activation_token.set_surface(self.surface()); + xdg_activation_token.commit(); + + Ok(serial) + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + self.window_state.lock().unwrap().set_cursor_grab(mode) + } + + #[inline] + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + let scale_factor = self.scale_factor(); + let position = position.to_logical(scale_factor); + self.window_state + .lock() + .unwrap() + .set_cursor_position(position) + // Request redraw on success, since the state is double buffered. + .map(|_| self.request_redraw()) + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + self.window_state.lock().unwrap().drag_window() + } + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + let surface = self.window.wl_surface(); + + if hittest { + surface.set_input_region(None); + Ok(()) + } else { + let region = Region::new(&*self.compositor).map_err(|_| { + ExternalError::Os(os_error!(OsError::Misc("failed to set input region."))) + })?; + region.add(0, 0, 0, 0); + surface.set_input_region(Some(region.wl_region())); + Ok(()) + } + } + + #[inline] + pub fn set_ime_cursor_area(&self, position: Position, size: Size) { + let window_state = self.window_state.lock().unwrap(); + if window_state.ime_allowed() { + let scale_factor = window_state.scale_factor(); + let position = position.to_logical(scale_factor); + let size = size.to_logical(scale_factor); + window_state.set_ime_cursor_area(position, size); + } + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + let mut window_state = self.window_state.lock().unwrap(); + + if window_state.ime_allowed() != allowed && window_state.set_ime_allowed(allowed) + { + let event = + WindowEvent::Ime(if allowed { Ime::Enabled } else { Ime::Disabled }); + self.window_events_sink + .lock() + .unwrap() + .push_window_event(event, self.window_id); + self.event_loop_awakener.ping(); + } + } + + #[inline] + pub fn set_ime_purpose(&self, purpose: ImePurpose) { + self.window_state.lock().unwrap().set_ime_purpose(purpose); + } + + #[inline] + pub fn focus_window(&self) {} + + #[inline] + pub fn surface(&self) -> &WlSurface { + self.window.wl_surface() + } + + #[inline] + pub fn current_monitor(&self) -> Option { + let data = self.window.wl_surface().data::()?; + data.outputs().next().map(MonitorHandle::new) + } + + #[inline] + pub fn available_monitors(&self) -> Vec { + self.monitors.lock().unwrap().clone() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + // XXX there's no such concept on Wayland. + None + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::WaylandWindowHandle::new({ + let ptr = self.window.wl_surface().id().as_ptr(); + std::ptr::NonNull::new(ptr as *mut _).expect("wl_surface will never be null") + }) + .into()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::WaylandDisplayHandle::new({ + let ptr = self.display.id().as_ptr(); + std::ptr::NonNull::new(ptr as *mut _).expect("wl_proxy should never be null") + }) + .into()) + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + self.window_state.lock().unwrap().set_theme(theme) + } + + #[inline] + pub fn theme(&self) -> Option { + self.window_state.lock().unwrap().theme() + } + + pub fn set_content_protected(&self, _protected: bool) {} + + #[inline] + pub fn title(&self) -> String { + self.window_state.lock().unwrap().title().to_owned() + } +} + +impl Drop for Window { + fn drop(&mut self) { + self.window_requests.closed.store(true, Ordering::Relaxed); + self.event_loop_awakener.ping(); + } +} + +/// The request from the window to the event loop. +#[derive(Debug)] +pub struct WindowRequests { + /// The window was closed. + pub closed: AtomicBool, + + /// Redraw Requested. + pub redraw_requested: AtomicBool, +} + +impl WindowRequests { + pub fn take_closed(&self) -> bool { + self.closed.swap(false, Ordering::Relaxed) + } + + pub fn take_redraw_requested(&self) -> bool { + self.redraw_requested.swap(false, Ordering::Relaxed) + } +} + +impl TryFrom<&str> for Theme { + type Error = (); + + /// ``` + /// use winit::window::Theme; + /// + /// assert_eq!("dark".try_into(), Ok(Theme::Dark)); + /// assert_eq!("lIghT".try_into(), Ok(Theme::Light)); + /// ``` + fn try_from(theme: &str) -> Result { + if theme.eq_ignore_ascii_case("dark") { + Ok(Self::Dark) + } else if theme.eq_ignore_ascii_case("light") { + Ok(Self::Light) + } else { + Err(()) + } + } +} diff --git a/rio-window/src/platform_impl/linux/wayland/window/state.rs b/rio-window/src/platform_impl/linux/wayland/window/state.rs new file mode 100644 index 00000000..e52caaf6 --- /dev/null +++ b/rio-window/src/platform_impl/linux/wayland/window/state.rs @@ -0,0 +1,1221 @@ +//! The state of the window, which is shared with the event-loop. + +use std::num::NonZeroU32; +use std::sync::{Arc, Mutex, Weak}; +use std::time::Duration; + +use ahash::HashSet; +use tracing::{info, warn}; + +use sctk::reexports::client::backend::ObjectId; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::protocol::wl_shm::WlShm; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; +use sctk::reexports::csd_frame::{ + DecorationsFrame, FrameAction, FrameClick, ResizeEdge, WindowState as XdgWindowState, +}; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1; +use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; +use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge as XdgResizeEdge; + +use sctk::compositor::{CompositorState, Region, SurfaceData, SurfaceDataExt}; +use sctk::seat::pointer::{PointerDataExt, ThemedPointer}; +use sctk::shell::xdg::window::{DecorationMode, Window, WindowConfigure}; +use sctk::shell::xdg::XdgSurface; +use sctk::shell::WaylandSurface; +use sctk::shm::slot::SlotPool; +use sctk::shm::Shm; +use sctk::subcompositor::SubcompositorState; +use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur; + +use crate::cursor::CustomCursor as RootCustomCursor; +use crate::dpi::{LogicalPosition, LogicalSize, PhysicalSize, Size}; +use crate::error::{ExternalError, NotSupportedError}; +use crate::platform_impl::wayland::logical_to_physical_rounded; +use crate::platform_impl::wayland::types::cursor::{CustomCursor, SelectedCursor}; +use crate::platform_impl::wayland::types::kwin_blur::KWinBlurManager; +use crate::platform_impl::{PlatformCustomCursor, WindowId}; +use crate::window::{CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme}; + +use crate::platform_impl::wayland::seat::{ + PointerConstraintsState, WinitPointerData, WinitPointerDataExt, ZwpTextInputV3Ext, +}; +use crate::platform_impl::wayland::state::{WindowCompositorUpdate, WinitState}; + +#[cfg(feature = "sctk-adwaita")] +pub type WinitFrame = sctk_adwaita::AdwaitaFrame; +#[cfg(not(feature = "sctk-adwaita"))] +pub type WinitFrame = sctk::shell::xdg::fallback_frame::FallbackFrame; + +// Minimum window inner size. +const MIN_WINDOW_SIZE: LogicalSize = LogicalSize::new(2, 1); + +/// The state of the window which is being updated from the [`WinitState`]. +pub struct WindowState { + /// The connection to Wayland server. + pub connection: Connection, + + /// The `Shm` to set cursor. + pub shm: WlShm, + + // A shared pool where to allocate custom cursors. + custom_cursor_pool: Arc>, + + /// The last received configure. + pub last_configure: Option, + + /// The pointers observed on the window. + pub pointers: Vec>>, + + selected_cursor: SelectedCursor, + + /// Whether the cursor is visible. + pub cursor_visible: bool, + + /// Pointer constraints to lock/confine pointer. + pub pointer_constraints: Option>, + + /// Queue handle. + pub queue_handle: QueueHandle, + + /// Theme variant. + theme: Option, + + /// The current window title. + title: String, + + /// Whether the frame is resizable. + resizable: bool, + + // NOTE: we can't use simple counter, since it's racy when seat getting destroyed and new + // is created, since add/removed stuff could be delivered a bit out of order. + /// Seats that has keyboard focus on that window. + seat_focus: HashSet, + + /// The scale factor of the window. + scale_factor: f64, + + /// Whether the window is transparent. + transparent: bool, + + /// The state of the compositor to create WlRegions. + compositor: Arc, + + /// The current cursor grabbing mode. + cursor_grab_mode: GrabState, + + /// Whether the IME input is allowed for that window. + ime_allowed: bool, + + /// The current IME purpose. + ime_purpose: ImePurpose, + + /// The text inputs observed on the window. + text_inputs: Vec, + + /// The inner size of the window, as in without client side decorations. + size: LogicalSize, + + /// Whether the CSD fail to create, so we don't try to create them on each iteration. + csd_fails: bool, + + /// Whether we should decorate the frame. + decorate: bool, + + /// Min size. + min_inner_size: LogicalSize, + max_inner_size: Option>, + + /// The size of the window when no states were applied to it. The primary use for it + /// is to fallback to original window size, before it was maximized, if the compositor + /// sends `None` for the new size in the configure. + stateless_size: LogicalSize, + + /// Initial window size provided by the user. Removed on the first + /// configure. + initial_size: Option, + + /// The state of the frame callback. + frame_callback_state: FrameCallbackState, + + viewport: Option, + fractional_scale: Option, + blur: Option, + blur_manager: Option, + + /// Whether the client side decorations have pending move operations. + /// + /// The value is the serial of the event triggered moved. + has_pending_move: Option, + + /// The underlying SCTK window. + pub window: Window, + + // NOTE: The spec says that destroying parent(`window` in our case), will unmap the + // subsurfaces. Thus to achieve atomic unmap of the client, drop the decorations + // frame after the `window` is dropped. To achieve that we rely on rust's struct + // field drop order guarantees. + /// The window frame, which is created from the configure request. + frame: Option, +} + +impl WindowState { + /// Create new window state. + pub fn new( + connection: Connection, + queue_handle: &QueueHandle, + winit_state: &WinitState, + initial_size: Size, + window: Window, + theme: Option, + ) -> Self { + let compositor = winit_state.compositor_state.clone(); + let pointer_constraints = winit_state.pointer_constraints.clone(); + let viewport = winit_state + .viewporter_state + .as_ref() + .map(|state| state.get_viewport(window.wl_surface(), queue_handle)); + let fractional_scale = winit_state + .fractional_scaling_manager + .as_ref() + .map(|fsm| fsm.fractional_scaling(window.wl_surface(), queue_handle)); + + Self { + blur: None, + blur_manager: winit_state.kwin_blur_manager.clone(), + compositor, + connection, + csd_fails: false, + cursor_grab_mode: GrabState::new(), + selected_cursor: Default::default(), + cursor_visible: true, + decorate: true, + fractional_scale, + frame: None, + frame_callback_state: FrameCallbackState::None, + seat_focus: Default::default(), + has_pending_move: None, + ime_allowed: false, + ime_purpose: ImePurpose::Normal, + last_configure: None, + max_inner_size: None, + min_inner_size: MIN_WINDOW_SIZE, + pointer_constraints, + pointers: Default::default(), + queue_handle: queue_handle.clone(), + resizable: true, + scale_factor: 1., + shm: winit_state.shm.wl_shm().clone(), + custom_cursor_pool: winit_state.custom_cursor_pool.clone(), + size: initial_size.to_logical(1.), + stateless_size: initial_size.to_logical(1.), + initial_size: Some(initial_size), + text_inputs: Vec::new(), + theme, + title: String::default(), + transparent: false, + viewport, + window, + } + } + + /// Apply closure on the given pointer. + fn apply_on_pointer, &WinitPointerData)>( + &self, + callback: F, + ) { + self.pointers + .iter() + .filter_map(Weak::upgrade) + .for_each(|pointer| { + let data = pointer.pointer().winit_data(); + callback(pointer.as_ref(), data); + }) + } + + /// Get the current state of the frame callback. + pub fn frame_callback_state(&self) -> FrameCallbackState { + self.frame_callback_state + } + + /// The frame callback was received, but not yet sent to the user. + pub fn frame_callback_received(&mut self) { + self.frame_callback_state = FrameCallbackState::Received; + } + + /// Reset the frame callbacks state. + pub fn frame_callback_reset(&mut self) { + self.frame_callback_state = FrameCallbackState::None; + } + + /// Request a frame callback if we don't have one for this window in flight. + pub fn request_frame_callback(&mut self) { + let surface = self.window.wl_surface(); + match self.frame_callback_state { + FrameCallbackState::None | FrameCallbackState::Received => { + self.frame_callback_state = FrameCallbackState::Requested; + surface.frame(&self.queue_handle, surface.clone()); + } + FrameCallbackState::Requested => (), + } + } + + pub fn configure( + &mut self, + configure: WindowConfigure, + shm: &Shm, + subcompositor: &Option>, + ) -> bool { + // NOTE: when using fractional scaling or wl_compositor@v6 the scaling + // should be delivered before the first configure, thus apply it to + // properly scale the physical sizes provided by the users. + if let Some(initial_size) = self.initial_size.take() { + self.size = initial_size.to_logical(self.scale_factor()); + self.stateless_size = self.size; + } + + if let Some(subcompositor) = subcompositor.as_ref().filter(|_| { + configure.decoration_mode == DecorationMode::Client + && self.frame.is_none() + && !self.csd_fails + }) { + match WinitFrame::new( + &self.window, + shm, + #[cfg(feature = "sctk-adwaita")] + self.compositor.clone(), + subcompositor.clone(), + self.queue_handle.clone(), + #[cfg(feature = "sctk-adwaita")] + into_sctk_adwaita_config(self.theme), + ) { + Ok(mut frame) => { + frame.set_title(&self.title); + frame.set_scaling_factor(self.scale_factor); + // Hide the frame if we were asked to not decorate. + frame.set_hidden(!self.decorate); + self.frame = Some(frame); + } + Err(err) => { + warn!("Failed to create client side decorations frame: {err}"); + self.csd_fails = true; + } + } + } else if configure.decoration_mode == DecorationMode::Server { + // Drop the frame for server side decorations to save resources. + self.frame = None; + } + + let stateless = Self::is_stateless(&configure); + + let (mut new_size, constrain) = if let Some(frame) = self.frame.as_mut() { + // Configure the window states. + frame.update_state(configure.state); + + match configure.new_size { + (Some(width), Some(height)) => { + let (width, height) = frame.subtract_borders(width, height); + let width = width.map(|w| w.get()).unwrap_or(1); + let height = height.map(|h| h.get()).unwrap_or(1); + ((width, height).into(), false) + } + (..) if stateless => (self.stateless_size, true), + _ => (self.size, true), + } + } else { + match configure.new_size { + (Some(width), Some(height)) => { + ((width.get(), height.get()).into(), false) + } + _ if stateless => (self.stateless_size, true), + _ => (self.size, true), + } + }; + + // Apply configure bounds only when compositor let the user decide what size to pick. + if constrain { + let bounds = self.inner_size_bounds(&configure); + new_size.width = bounds + .0 + .map(|bound_w| new_size.width.min(bound_w.get())) + .unwrap_or(new_size.width); + new_size.height = bounds + .1 + .map(|bound_h| new_size.height.min(bound_h.get())) + .unwrap_or(new_size.height); + } + + let new_state = configure.state; + let old_state = self + .last_configure + .as_ref() + .map(|configure| configure.state); + + let state_change_requires_resize = old_state + .map(|old_state| { + !old_state + .symmetric_difference(new_state) + .difference(XdgWindowState::ACTIVATED | XdgWindowState::SUSPENDED) + .is_empty() + }) + // NOTE: `None` is present for the initial configure, thus we must always resize. + .unwrap_or(true); + + // NOTE: Set the configure before doing a resize, since we query it during it. + self.last_configure = Some(configure); + + if state_change_requires_resize || new_size != self.inner_size() { + self.resize(new_size); + true + } else { + false + } + } + + /// Compute the bounds for the inner size of the surface. + fn inner_size_bounds( + &self, + configure: &WindowConfigure, + ) -> (Option, Option) { + let configure_bounds = match configure.suggested_bounds { + Some((width, height)) => (NonZeroU32::new(width), NonZeroU32::new(height)), + None => (None, None), + }; + + if let Some(frame) = self.frame.as_ref() { + let (width, height) = frame.subtract_borders( + configure_bounds.0.unwrap_or(NonZeroU32::new(1).unwrap()), + configure_bounds.1.unwrap_or(NonZeroU32::new(1).unwrap()), + ); + ( + configure_bounds.0.and(width), + configure_bounds.1.and(height), + ) + } else { + configure_bounds + } + } + + #[inline] + fn is_stateless(configure: &WindowConfigure) -> bool { + !(configure.is_maximized() || configure.is_fullscreen() || configure.is_tiled()) + } + + /// Start interacting drag resize. + pub fn drag_resize_window( + &self, + direction: ResizeDirection, + ) -> Result<(), ExternalError> { + let xdg_toplevel = self.window.xdg_toplevel(); + + // TODO(kchibisov) handle touch serials. + self.apply_on_pointer(|_, data| { + let serial = data.latest_button_serial(); + let seat = data.seat(); + xdg_toplevel.resize(seat, serial, direction.into()); + }); + + Ok(()) + } + + /// Start the window drag. + pub fn drag_window(&self) -> Result<(), ExternalError> { + let xdg_toplevel = self.window.xdg_toplevel(); + // TODO(kchibisov) handle touch serials. + self.apply_on_pointer(|_, data| { + let serial = data.latest_button_serial(); + let seat = data.seat(); + xdg_toplevel._move(seat, serial); + }); + + Ok(()) + } + + /// Tells whether the window should be closed. + #[allow(clippy::too_many_arguments)] + pub fn frame_click( + &mut self, + click: FrameClick, + pressed: bool, + seat: &WlSeat, + serial: u32, + timestamp: Duration, + window_id: WindowId, + updates: &mut Vec, + ) -> Option { + match self.frame.as_mut()?.on_click(timestamp, click, pressed)? { + FrameAction::Minimize => self.window.set_minimized(), + FrameAction::Maximize => self.window.set_maximized(), + FrameAction::UnMaximize => self.window.unset_maximized(), + FrameAction::Close => WinitState::queue_close(updates, window_id), + FrameAction::Move => self.has_pending_move = Some(serial), + FrameAction::Resize(edge) => { + let edge = match edge { + ResizeEdge::None => XdgResizeEdge::None, + ResizeEdge::Top => XdgResizeEdge::Top, + ResizeEdge::Bottom => XdgResizeEdge::Bottom, + ResizeEdge::Left => XdgResizeEdge::Left, + ResizeEdge::TopLeft => XdgResizeEdge::TopLeft, + ResizeEdge::BottomLeft => XdgResizeEdge::BottomLeft, + ResizeEdge::Right => XdgResizeEdge::Right, + ResizeEdge::TopRight => XdgResizeEdge::TopRight, + ResizeEdge::BottomRight => XdgResizeEdge::BottomRight, + _ => return None, + }; + self.window.resize(seat, serial, edge); + } + FrameAction::ShowMenu(x, y) => { + self.window.show_window_menu(seat, serial, (x, y)) + } + _ => (), + }; + + Some(false) + } + + pub fn frame_point_left(&mut self) { + if let Some(frame) = self.frame.as_mut() { + frame.click_point_left(); + } + } + + // Move the point over decorations. + pub fn frame_point_moved( + &mut self, + seat: &WlSeat, + surface: &WlSurface, + timestamp: Duration, + x: f64, + y: f64, + ) -> Option { + // Take the serial if we had any, so it doesn't stick around. + let serial = self.has_pending_move.take(); + + if let Some(frame) = self.frame.as_mut() { + let cursor = frame.click_point_moved(timestamp, &surface.id(), x, y); + // If we have a cursor change, that means that cursor is over the decorations, + // so try to apply move. + if let Some(serial) = cursor.is_some().then_some(serial).flatten() { + self.window.move_(seat, serial); + None + } else { + cursor + } + } else { + None + } + } + + /// Get the stored resizable state. + #[inline] + pub fn resizable(&self) -> bool { + self.resizable + } + + /// Set the resizable state on the window. + /// + /// Returns `true` when the state was applied. + #[inline] + pub fn set_resizable(&mut self, resizable: bool) -> bool { + if self.resizable == resizable { + return false; + } + + self.resizable = resizable; + if resizable { + // Restore min/max sizes of the window. + self.reload_min_max_hints(); + } else { + self.set_min_inner_size(Some(self.size)); + self.set_max_inner_size(Some(self.size)); + } + + // Reload the state on the frame as well. + if let Some(frame) = self.frame.as_mut() { + frame.set_resizable(resizable); + } + + true + } + + /// Whether the window is focused by any seat. + #[inline] + pub fn has_focus(&self) -> bool { + !self.seat_focus.is_empty() + } + + /// Whether the IME is allowed. + #[inline] + pub fn ime_allowed(&self) -> bool { + self.ime_allowed + } + + /// Get the size of the window. + #[inline] + pub fn inner_size(&self) -> LogicalSize { + self.size + } + + /// Whether the window received initial configure event from the compositor. + #[inline] + pub fn is_configured(&self) -> bool { + self.last_configure.is_some() + } + + #[inline] + pub fn is_decorated(&mut self) -> bool { + let csd = self + .last_configure + .as_ref() + .map(|configure| configure.decoration_mode == DecorationMode::Client) + .unwrap_or(false); + if let Some(frame) = csd.then_some(self.frame.as_ref()).flatten() { + !frame.is_hidden() + } else { + // Server side decorations. + true + } + } + + /// Get the outer size of the window. + #[inline] + pub fn outer_size(&self) -> LogicalSize { + self.frame + .as_ref() + .map(|frame| frame.add_borders(self.size.width, self.size.height).into()) + .unwrap_or(self.size) + } + + /// Register pointer on the top-level. + pub fn pointer_entered(&mut self, added: Weak>) { + self.pointers.push(added); + self.reload_cursor_style(); + + let mode = self.cursor_grab_mode.user_grab_mode; + let _ = self.set_cursor_grab_inner(mode); + } + + /// Pointer has left the top-level. + pub fn pointer_left(&mut self, removed: Weak>) { + let mut new_pointers = Vec::new(); + for pointer in self.pointers.drain(..) { + if let Some(pointer) = pointer.upgrade() { + if pointer.pointer() != removed.upgrade().unwrap().pointer() { + new_pointers.push(Arc::downgrade(&pointer)); + } + } + } + + self.pointers = new_pointers; + } + + /// Refresh the decorations frame if it's present returning whether the client should redraw. + pub fn refresh_frame(&mut self) -> bool { + if let Some(frame) = self.frame.as_mut() { + if !frame.is_hidden() && frame.is_dirty() { + return frame.draw(); + } + } + + false + } + + /// Reload the cursor style on the given window. + pub fn reload_cursor_style(&mut self) { + if self.cursor_visible { + match &self.selected_cursor { + SelectedCursor::Named(icon) => self.set_cursor(*icon), + SelectedCursor::Custom(cursor) => self.apply_custom_cursor(cursor), + } + } else { + self.set_cursor_visible(self.cursor_visible); + } + } + + /// Reissue the transparency hint to the compositor. + pub fn reload_transparency_hint(&self) { + let surface = self.window.wl_surface(); + + if self.transparent { + surface.set_opaque_region(None); + } else if let Ok(region) = Region::new(&*self.compositor) { + region.add(0, 0, i32::MAX, i32::MAX); + surface.set_opaque_region(Some(region.wl_region())); + } else { + warn!("Failed to mark window opaque."); + } + } + + /// Try to resize the window when the user can do so. + pub fn request_inner_size(&mut self, inner_size: Size) -> PhysicalSize { + if self + .last_configure + .as_ref() + .map(Self::is_stateless) + .unwrap_or(true) + { + self.resize(inner_size.to_logical(self.scale_factor())) + } + + logical_to_physical_rounded(self.inner_size(), self.scale_factor()) + } + + /// Resize the window to the new inner size. + fn resize(&mut self, inner_size: LogicalSize) { + self.size = inner_size; + + // Update the stateless size. + if Some(true) == self.last_configure.as_ref().map(Self::is_stateless) { + self.stateless_size = inner_size; + } + + // Update the inner frame. + let ((x, y), outer_size) = if let Some(frame) = self.frame.as_mut() { + // Resize only visible frame. + if !frame.is_hidden() { + frame.resize( + NonZeroU32::new(self.size.width).unwrap(), + NonZeroU32::new(self.size.height).unwrap(), + ); + } + + ( + frame.location(), + frame.add_borders(self.size.width, self.size.height).into(), + ) + } else { + ((0, 0), self.size) + }; + + // Reload the hint. + self.reload_transparency_hint(); + + // Set the window geometry. + self.window.xdg_surface().set_window_geometry( + x, + y, + outer_size.width as i32, + outer_size.height as i32, + ); + + // Update the target viewport, this is used if and only if fractional scaling is in use. + if let Some(viewport) = self.viewport.as_ref() { + // Set inner size without the borders. + viewport.set_destination(self.size.width as _, self.size.height as _); + } + } + + /// Get the scale factor of the window. + #[inline] + pub fn scale_factor(&self) -> f64 { + self.scale_factor + } + + /// Set the cursor icon. + pub fn set_cursor(&mut self, cursor_icon: CursorIcon) { + self.selected_cursor = SelectedCursor::Named(cursor_icon); + + if !self.cursor_visible { + return; + } + + self.apply_on_pointer(|pointer, _| { + if pointer.set_cursor(&self.connection, cursor_icon).is_err() { + warn!("Failed to set cursor to {:?}", cursor_icon); + } + }) + } + + /// Set the custom cursor icon. + pub(crate) fn set_custom_cursor(&mut self, cursor: RootCustomCursor) { + let cursor = match cursor { + RootCustomCursor { + inner: PlatformCustomCursor::Wayland(cursor), + } => cursor.0, + #[cfg(x11_platform)] + RootCustomCursor { + inner: PlatformCustomCursor::X(_), + } => { + tracing::error!("passed a X11 cursor to Wayland backend"); + return; + } + }; + + let cursor = { + let mut pool = self.custom_cursor_pool.lock().unwrap(); + CustomCursor::new(&mut pool, &cursor) + }; + + if self.cursor_visible { + self.apply_custom_cursor(&cursor); + } + + self.selected_cursor = SelectedCursor::Custom(cursor); + } + + fn apply_custom_cursor(&self, cursor: &CustomCursor) { + self.apply_on_pointer(|pointer, _| { + let surface = pointer.surface(); + + let scale = surface + .data::() + .unwrap() + .surface_data() + .scale_factor(); + + surface.set_buffer_scale(scale); + surface.attach(Some(cursor.buffer.wl_buffer()), 0, 0); + if surface.version() >= 4 { + surface.damage_buffer(0, 0, cursor.w, cursor.h); + } else { + surface.damage(0, 0, cursor.w / scale, cursor.h / scale); + } + surface.commit(); + + let serial = pointer + .pointer() + .data::() + .and_then(|data| data.pointer_data().latest_enter_serial()) + .unwrap(); + + pointer.pointer().set_cursor( + serial, + Some(surface), + cursor.hotspot_x / scale, + cursor.hotspot_y / scale, + ); + }); + } + + /// Set maximum inner window size. + pub fn set_min_inner_size(&mut self, size: Option>) { + // Ensure that the window has the right minimum size. + let mut size = size.unwrap_or(MIN_WINDOW_SIZE); + size.width = size.width.max(MIN_WINDOW_SIZE.width); + size.height = size.height.max(MIN_WINDOW_SIZE.height); + + // Add the borders. + let size = self + .frame + .as_ref() + .map(|frame| frame.add_borders(size.width, size.height).into()) + .unwrap_or(size); + + self.min_inner_size = size; + self.window.set_min_size(Some(size.into())); + } + + /// Set maximum inner window size. + pub fn set_max_inner_size(&mut self, size: Option>) { + let size = size.map(|size| { + self.frame + .as_ref() + .map(|frame| frame.add_borders(size.width, size.height).into()) + .unwrap_or(size) + }); + + self.max_inner_size = size; + self.window.set_max_size(size.map(Into::into)); + } + + /// Set the CSD theme. + pub fn set_theme(&mut self, theme: Option) { + self.theme = theme; + #[cfg(feature = "sctk-adwaita")] + if let Some(frame) = self.frame.as_mut() { + frame.set_config(into_sctk_adwaita_config(theme)) + } + } + + /// The current theme for CSD decorations. + #[inline] + pub fn theme(&self) -> Option { + self.theme + } + + /// Set the cursor grabbing state on the top-level. + pub fn set_cursor_grab(&mut self, mode: CursorGrabMode) -> Result<(), ExternalError> { + if self.cursor_grab_mode.user_grab_mode == mode { + return Ok(()); + } + + self.set_cursor_grab_inner(mode)?; + // Update user grab on success. + self.cursor_grab_mode.user_grab_mode = mode; + Ok(()) + } + + /// Reload the hints for minimum and maximum sizes. + pub fn reload_min_max_hints(&mut self) { + self.set_min_inner_size(Some(self.min_inner_size)); + self.set_max_inner_size(self.max_inner_size); + } + + /// Set the grabbing state on the surface. + fn set_cursor_grab_inner( + &mut self, + mode: CursorGrabMode, + ) -> Result<(), ExternalError> { + let pointer_constraints = match self.pointer_constraints.as_ref() { + Some(pointer_constraints) => pointer_constraints, + None if mode == CursorGrabMode::None => return Ok(()), + None => return Err(ExternalError::NotSupported(NotSupportedError::new())), + }; + + // Replace the current mode. + let old_mode = + std::mem::replace(&mut self.cursor_grab_mode.current_grab_mode, mode); + + match old_mode { + CursorGrabMode::None => (), + CursorGrabMode::Confined => self.apply_on_pointer(|_, data| { + data.unconfine_pointer(); + }), + CursorGrabMode::Locked => { + self.apply_on_pointer(|_, data| data.unlock_pointer()); + } + } + + let surface = self.window.wl_surface(); + match mode { + CursorGrabMode::Locked => self.apply_on_pointer(|pointer, data| { + let pointer = pointer.pointer(); + data.lock_pointer( + pointer_constraints, + surface, + pointer, + &self.queue_handle, + ) + }), + CursorGrabMode::Confined => self.apply_on_pointer(|pointer, data| { + let pointer = pointer.pointer(); + data.confine_pointer( + pointer_constraints, + surface, + pointer, + &self.queue_handle, + ) + }), + CursorGrabMode::None => { + // Current lock/confine was already removed. + } + } + + Ok(()) + } + + pub fn show_window_menu(&self, position: LogicalPosition) { + // TODO(kchibisov) handle touch serials. + self.apply_on_pointer(|_, data| { + let serial = data.latest_button_serial(); + let seat = data.seat(); + self.window.show_window_menu(seat, serial, position.into()); + }); + } + + /// Set the position of the cursor. + pub fn set_cursor_position( + &self, + position: LogicalPosition, + ) -> Result<(), ExternalError> { + if self.pointer_constraints.is_none() { + return Err(ExternalError::NotSupported(NotSupportedError::new())); + } + + // Position can be set only for locked cursor. + if self.cursor_grab_mode.current_grab_mode != CursorGrabMode::Locked { + return Err(ExternalError::Os(os_error!( + crate::platform_impl::OsError::Misc( + "cursor position can be set only for locked cursor." + ) + ))); + } + + self.apply_on_pointer(|_, data| { + data.set_locked_cursor_position(position.x, position.y); + }); + + Ok(()) + } + + /// Set the visibility state of the cursor. + pub fn set_cursor_visible(&mut self, cursor_visible: bool) { + self.cursor_visible = cursor_visible; + + if self.cursor_visible { + match &self.selected_cursor { + SelectedCursor::Named(icon) => self.set_cursor(*icon), + SelectedCursor::Custom(cursor) => self.apply_custom_cursor(cursor), + } + } else { + for pointer in self.pointers.iter().filter_map(|pointer| pointer.upgrade()) { + let latest_enter_serial = + pointer.pointer().winit_data().latest_enter_serial(); + + pointer + .pointer() + .set_cursor(latest_enter_serial, None, 0, 0); + } + } + } + + /// Whether show or hide client side decorations. + #[inline] + pub fn set_decorate(&mut self, decorate: bool) { + if decorate == self.decorate { + return; + } + + self.decorate = decorate; + + match self + .last_configure + .as_ref() + .map(|configure| configure.decoration_mode) + { + Some(DecorationMode::Server) if !self.decorate => { + // To disable decorations we should request client and hide the frame. + self.window + .request_decoration_mode(Some(DecorationMode::Client)) + } + _ if self.decorate => self + .window + .request_decoration_mode(Some(DecorationMode::Server)), + _ => (), + } + + if let Some(frame) = self.frame.as_mut() { + frame.set_hidden(!decorate); + // Force the resize. + self.resize(self.size); + } + } + + /// Add seat focus for the window. + #[inline] + pub fn add_seat_focus(&mut self, seat: ObjectId) { + self.seat_focus.insert(seat); + } + + /// Remove seat focus from the window. + #[inline] + pub fn remove_seat_focus(&mut self, seat: &ObjectId) { + self.seat_focus.remove(seat); + } + + /// Returns `true` if the requested state was applied. + pub fn set_ime_allowed(&mut self, allowed: bool) -> bool { + self.ime_allowed = allowed; + + let mut applied = false; + for text_input in &self.text_inputs { + applied = true; + if allowed { + text_input.enable(); + text_input.set_content_type_by_purpose(self.ime_purpose); + } else { + text_input.disable(); + } + text_input.commit(); + } + + applied + } + + /// Set the IME position. + pub fn set_ime_cursor_area( + &self, + position: LogicalPosition, + size: LogicalSize, + ) { + // FIXME: This won't fly unless user will have a way to request IME window per seat, since + // the ime windows will be overlapping, but winit doesn't expose API to specify for + // which seat we're setting IME position. + let (x, y) = (position.x as i32, position.y as i32); + let (width, height) = (size.width as i32, size.height as i32); + for text_input in self.text_inputs.iter() { + text_input.set_cursor_rectangle(x, y, width, height); + text_input.commit(); + } + } + + /// Set the IME purpose. + pub fn set_ime_purpose(&mut self, purpose: ImePurpose) { + self.ime_purpose = purpose; + + for text_input in &self.text_inputs { + text_input.set_content_type_by_purpose(purpose); + text_input.commit(); + } + } + + /// Get the IME purpose. + pub fn ime_purpose(&self) -> ImePurpose { + self.ime_purpose + } + + /// Set the scale factor for the given window. + #[inline] + pub fn set_scale_factor(&mut self, scale_factor: f64) { + self.scale_factor = scale_factor; + + // NOTE: When fractional scaling is not used update the buffer scale. + if self.fractional_scale.is_none() { + let _ = self.window.set_buffer_scale(self.scale_factor as _); + } + + if let Some(frame) = self.frame.as_mut() { + frame.set_scaling_factor(scale_factor); + } + } + + /// Make window background blurred + #[inline] + pub fn set_blur(&mut self, blurred: bool) { + if blurred && self.blur.is_none() { + if let Some(blur_manager) = self.blur_manager.as_ref() { + let blur = + blur_manager.blur(self.window.wl_surface(), &self.queue_handle); + blur.commit(); + self.blur = Some(blur); + } else { + info!("Blur manager unavailable, unable to change blur") + } + } else if !blurred && self.blur.is_some() { + self.blur_manager + .as_ref() + .unwrap() + .unset(self.window.wl_surface()); + self.blur.take().unwrap().release(); + } + } + + /// Set the window title to a new value. + /// + /// This will automatically truncate the title to something meaningful. + pub fn set_title(&mut self, mut title: String) { + // Truncate the title to at most 1024 bytes, so that it does not blow up the protocol + // messages + if title.len() > 1024 { + let mut new_len = 1024; + while !title.is_char_boundary(new_len) { + new_len -= 1; + } + title.truncate(new_len); + } + + // Update the CSD title. + if let Some(frame) = self.frame.as_mut() { + frame.set_title(&title); + } + + self.window.set_title(&title); + self.title = title; + } + + /// Mark the window as transparent. + #[inline] + pub fn set_transparent(&mut self, transparent: bool) { + self.transparent = transparent; + self.reload_transparency_hint(); + } + + /// Register text input on the top-level. + #[inline] + pub fn text_input_entered(&mut self, text_input: &ZwpTextInputV3) { + if !self.text_inputs.iter().any(|t| t == text_input) { + self.text_inputs.push(text_input.clone()); + } + } + + /// The text input left the top-level. + #[inline] + pub fn text_input_left(&mut self, text_input: &ZwpTextInputV3) { + if let Some(position) = self.text_inputs.iter().position(|t| t == text_input) { + self.text_inputs.remove(position); + } + } + + /// Get the cached title. + #[inline] + pub fn title(&self) -> &str { + &self.title + } +} + +impl Drop for WindowState { + fn drop(&mut self) { + if let Some(blur) = self.blur.take() { + blur.release(); + } + + if let Some(fs) = self.fractional_scale.take() { + fs.destroy(); + } + + if let Some(viewport) = self.viewport.take() { + viewport.destroy(); + } + + // NOTE: the wl_surface used by the window is being cleaned up when + // dropping SCTK `Window`. + } +} + +/// The state of the cursor grabs. +#[derive(Clone, Copy)] +struct GrabState { + /// The grab mode requested by the user. + user_grab_mode: CursorGrabMode, + + /// The current grab mode. + current_grab_mode: CursorGrabMode, +} + +impl GrabState { + fn new() -> Self { + Self { + user_grab_mode: CursorGrabMode::None, + current_grab_mode: CursorGrabMode::None, + } + } +} + +/// The state of the frame callback. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrameCallbackState { + /// No frame callback was requested. + #[default] + None, + /// The frame callback was requested, but not yet arrived, the redraw events are throttled. + Requested, + /// The callback was marked as done, and user could receive redraw requested + Received, +} + +impl From for XdgResizeEdge { + fn from(value: ResizeDirection) -> Self { + match value { + ResizeDirection::North => XdgResizeEdge::Top, + ResizeDirection::West => XdgResizeEdge::Left, + ResizeDirection::NorthWest => XdgResizeEdge::TopLeft, + ResizeDirection::NorthEast => XdgResizeEdge::TopRight, + ResizeDirection::East => XdgResizeEdge::Right, + ResizeDirection::SouthWest => XdgResizeEdge::BottomLeft, + ResizeDirection::SouthEast => XdgResizeEdge::BottomRight, + ResizeDirection::South => XdgResizeEdge::Bottom, + } + } +} + +// NOTE: Rust doesn't allow `From>`. +#[cfg(feature = "sctk-adwaita")] +fn into_sctk_adwaita_config(theme: Option) -> sctk_adwaita::FrameConfig { + match theme { + Some(Theme::Light) => sctk_adwaita::FrameConfig::light(), + Some(Theme::Dark) => sctk_adwaita::FrameConfig::dark(), + None => sctk_adwaita::FrameConfig::auto(), + } +} diff --git a/rio-window/src/platform_impl/linux/x11/activation.rs b/rio-window/src/platform_impl/linux/x11/activation.rs new file mode 100644 index 00000000..b77262e2 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/activation.rs @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! X11 activation handling. +//! +//! X11 has a "startup notification" specification similar to Wayland's, see this URL: +//! + +use super::atoms::*; +use super::{VoidCookie, X11Error, XConnection}; + +use std::ffi::CString; +use std::fmt::Write; + +use x11rb::protocol::xproto::{self, ConnectionExt as _}; + +impl XConnection { + /// "Request" a new activation token from the server. + pub(crate) fn request_activation_token( + &self, + window_title: &str, + ) -> Result { + // The specification recommends the format "hostname+pid+"_TIME"+current time" + let uname = rustix::system::uname(); + let pid = rustix::process::getpid(); + let time = self.timestamp(); + + let activation_token = format!( + "{}{}_TIME{}", + uname.nodename().to_str().unwrap_or("winit"), + pid.as_raw_nonzero(), + time + ); + + // Set up the new startup notification. + let notification = { + let mut buffer = Vec::new(); + buffer.extend_from_slice(b"new: ID="); + quote_string(&activation_token, &mut buffer); + buffer.extend_from_slice(b" NAME="); + quote_string(window_title, &mut buffer); + buffer.extend_from_slice(b" SCREEN="); + push_display(&mut buffer, &self.default_screen_index()); + + CString::new(buffer) + .map_err(|err| X11Error::InvalidActivationToken(err.into_vec()))? + .into_bytes_with_nul() + }; + self.send_message(¬ification)?; + + Ok(activation_token) + } + + /// Finish launching a window with the given startup ID. + pub(crate) fn remove_activation_token( + &self, + window: xproto::Window, + startup_id: &str, + ) -> Result<(), X11Error> { + let atoms = self.atoms(); + + // Set the _NET_STARTUP_ID property on the window. + self.xcb_connection() + .change_property( + xproto::PropMode::REPLACE, + window, + atoms[_NET_STARTUP_ID], + xproto::AtomEnum::STRING, + 8, + startup_id.len().try_into().unwrap(), + startup_id.as_bytes(), + )? + .check()?; + + // Send the message indicating that the startup is over. + let message = { + const MESSAGE_ROOT: &str = "remove: ID="; + + let mut buffer = Vec::with_capacity( + MESSAGE_ROOT + .len() + .checked_add(startup_id.len()) + .and_then(|x| x.checked_add(1)) + .unwrap(), + ); + buffer.extend_from_slice(MESSAGE_ROOT.as_bytes()); + quote_string(startup_id, &mut buffer); + CString::new(buffer) + .map_err(|err| X11Error::InvalidActivationToken(err.into_vec()))? + .into_bytes_with_nul() + }; + + self.send_message(&message) + } + + /// Send a startup notification message to the window manager. + fn send_message(&self, message: &[u8]) -> Result<(), X11Error> { + let atoms = self.atoms(); + + // Create a new window to send the message over. + let screen = self.default_root(); + let window = xproto::WindowWrapper::create_window( + self.xcb_connection(), + screen.root_depth, + screen.root, + -100, + -100, + 1, + 1, + 0, + xproto::WindowClass::INPUT_OUTPUT, + screen.root_visual, + &xproto::CreateWindowAux::new() + .override_redirect(1) + .event_mask( + xproto::EventMask::STRUCTURE_NOTIFY + | xproto::EventMask::PROPERTY_CHANGE, + ), + )?; + + // Serialize the messages in 20-byte chunks. + let mut message_type = atoms[_NET_STARTUP_INFO_BEGIN]; + message + .chunks(20) + .map(|chunk| { + let mut buffer = [0u8; 20]; + buffer[..chunk.len()].copy_from_slice(chunk); + let event = xproto::ClientMessageEvent::new( + 8, + window.window(), + message_type, + buffer, + ); + + // Set the message type to the continuation atom for the next chunk. + message_type = atoms[_NET_STARTUP_INFO]; + + event + }) + .try_for_each(|event| { + // Send each event in order. + self.xcb_connection() + .send_event( + false, + screen.root, + xproto::EventMask::PROPERTY_CHANGE, + event, + ) + .map(VoidCookie::ignore_error) + })?; + + Ok(()) + } +} + +/// Quote a literal string as per the startup notification specification. +fn quote_string(s: &str, target: &mut Vec) { + let total_len = s.len().checked_add(3).expect("quote string overflow"); + target.reserve(total_len); + + // Add the opening quote. + target.push(b'"'); + + // Iterate over the string split by literal quotes. + s.as_bytes().split(|&b| b == b'"').for_each(|part| { + // Add the part. + target.extend_from_slice(part); + + // Escape the quote. + target.push(b'\\'); + target.push(b'"'); + }); + + // Un-escape the last quote. + target.remove(target.len() - 2); +} + +/// Push a `Display` implementation to the buffer. +fn push_display(buffer: &mut Vec, display: &impl std::fmt::Display) { + struct Writer<'a> { + buffer: &'a mut Vec, + } + + impl<'a> std::fmt::Write for Writer<'a> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.buffer.extend_from_slice(s.as_bytes()); + Ok(()) + } + } + + write!(Writer { buffer }, "{}", display).unwrap(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn properly_escapes_x11_messages() { + let assert_eq = |input: &str, output: &[u8]| { + let mut buf = vec![]; + quote_string(input, &mut buf); + assert_eq!(buf, output); + }; + + assert_eq("", b"\"\""); + assert_eq("foo", b"\"foo\""); + assert_eq("foo\"bar", b"\"foo\\\"bar\""); + } +} diff --git a/rio-window/src/platform_impl/linux/x11/atoms.rs b/rio-window/src/platform_impl/linux/x11/atoms.rs new file mode 100644 index 00000000..c6d96c8f --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/atoms.rs @@ -0,0 +1,117 @@ +//! Collects every atom used by the platform implementation. + +use core::ops::Index; + +macro_rules! atom_manager { + ($($name:ident $(:$lit:literal)?),*) => { + x11rb::atom_manager! { + /// The atoms used by `winit` + pub Atoms: AtomsCookie { + $($name $(:$lit)?,)* + } + } + + /// Indices into the `Atoms` struct. + #[derive(Copy, Clone, Debug)] + #[allow(non_camel_case_types)] + pub enum AtomName { + $($name,)* + } + + impl AtomName { + pub(crate) fn atom_from( + self, + atoms: &Atoms + ) -> &x11rb::protocol::xproto::Atom { + match self { + $(AtomName::$name => &atoms.$name,)* + } + } + } + }; +} + +atom_manager! { + // General Use Atoms + CARD32, + UTF8_STRING, + WM_CHANGE_STATE, + WM_CLIENT_MACHINE, + WM_DELETE_WINDOW, + WM_PROTOCOLS, + WM_STATE, + XIM_SERVERS, + + // Assorted ICCCM Atoms + _NET_WM_ICON, + _NET_WM_MOVERESIZE, + _NET_WM_NAME, + _NET_WM_PID, + _NET_WM_PING, + _NET_WM_STATE, + _NET_WM_STATE_ABOVE, + _NET_WM_STATE_BELOW, + _NET_WM_STATE_FULLSCREEN, + _NET_WM_STATE_HIDDEN, + _NET_WM_STATE_MAXIMIZED_HORZ, + _NET_WM_STATE_MAXIMIZED_VERT, + _NET_WM_WINDOW_TYPE, + + // Activation atoms. + _NET_STARTUP_INFO_BEGIN, + _NET_STARTUP_INFO, + _NET_STARTUP_ID, + + // WM window types. + _NET_WM_WINDOW_TYPE_DESKTOP, + _NET_WM_WINDOW_TYPE_DOCK, + _NET_WM_WINDOW_TYPE_TOOLBAR, + _NET_WM_WINDOW_TYPE_MENU, + _NET_WM_WINDOW_TYPE_UTILITY, + _NET_WM_WINDOW_TYPE_SPLASH, + _NET_WM_WINDOW_TYPE_DIALOG, + _NET_WM_WINDOW_TYPE_DROPDOWN_MENU, + _NET_WM_WINDOW_TYPE_POPUP_MENU, + _NET_WM_WINDOW_TYPE_TOOLTIP, + _NET_WM_WINDOW_TYPE_NOTIFICATION, + _NET_WM_WINDOW_TYPE_COMBO, + _NET_WM_WINDOW_TYPE_DND, + _NET_WM_WINDOW_TYPE_NORMAL, + + // Drag-N-Drop Atoms + XdndAware, + XdndEnter, + XdndLeave, + XdndDrop, + XdndPosition, + XdndStatus, + XdndActionPrivate, + XdndSelection, + XdndFinished, + XdndTypeList, + TextUriList: b"text/uri-list", + None: b"None", + + // Miscellaneous Atoms + _GTK_THEME_VARIANT, + _MOTIF_WM_HINTS, + _NET_ACTIVE_WINDOW, + _NET_CLIENT_LIST, + _NET_FRAME_EXTENTS, + _NET_SUPPORTED, + _NET_SUPPORTING_WM_CHECK, + _XEMBED, + _XSETTINGS_SETTINGS +} + +impl Index for Atoms { + type Output = x11rb::protocol::xproto::Atom; + + fn index(&self, index: AtomName) -> &Self::Output { + index.atom_from(self) + } +} + +pub(crate) use AtomName::*; +// Make sure `None` is still defined. +pub(crate) use core::option::Option::None; diff --git a/rio-window/src/platform_impl/linux/x11/dnd.rs b/rio-window/src/platform_impl/linux/x11/dnd.rs new file mode 100644 index 00000000..ef9ced1f --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/dnd.rs @@ -0,0 +1,188 @@ +use std::io; +use std::os::raw::*; +use std::path::{Path, PathBuf}; +use std::str::Utf8Error; +use std::sync::Arc; + +use percent_encoding::percent_decode; +use x11rb::protocol::xproto::{self, ConnectionExt}; + +use super::atoms::AtomName::None as DndNone; +use super::atoms::*; +use super::{util, CookieResultExt, X11Error, XConnection}; + +#[derive(Debug, Clone, Copy)] +pub enum DndState { + Accepted, + Rejected, +} + +#[derive(Debug)] +pub enum DndDataParseError { + EmptyData, + InvalidUtf8(#[allow(dead_code)] Utf8Error), + HostnameSpecified(#[allow(dead_code)] String), + UnexpectedProtocol(#[allow(dead_code)] String), + UnresolvablePath(#[allow(dead_code)] io::Error), +} + +impl From for DndDataParseError { + fn from(e: Utf8Error) -> Self { + DndDataParseError::InvalidUtf8(e) + } +} + +impl From for DndDataParseError { + fn from(e: io::Error) -> Self { + DndDataParseError::UnresolvablePath(e) + } +} + +pub struct Dnd { + xconn: Arc, + // Populated by XdndEnter event handler + pub version: Option, + pub type_list: Option>, + // Populated by XdndPosition event handler + pub source_window: Option, + // Populated by SelectionNotify event handler (triggered by XdndPosition event handler) + pub result: Option, DndDataParseError>>, +} + +impl Dnd { + pub fn new(xconn: Arc) -> Result { + Ok(Dnd { + xconn, + version: None, + type_list: None, + source_window: None, + result: None, + }) + } + + pub fn reset(&mut self) { + self.version = None; + self.type_list = None; + self.source_window = None; + self.result = None; + } + + pub unsafe fn send_status( + &self, + this_window: xproto::Window, + target_window: xproto::Window, + state: DndState, + ) -> Result<(), X11Error> { + let atoms = self.xconn.atoms(); + let (accepted, action) = match state { + DndState::Accepted => (1, atoms[XdndActionPrivate]), + DndState::Rejected => (0, atoms[DndNone]), + }; + self.xconn + .send_client_msg( + target_window, + target_window, + atoms[XdndStatus] as _, + None, + [this_window, accepted, 0, 0, action as _], + )? + .ignore_error(); + + Ok(()) + } + + pub unsafe fn send_finished( + &self, + this_window: xproto::Window, + target_window: xproto::Window, + state: DndState, + ) -> Result<(), X11Error> { + let atoms = self.xconn.atoms(); + let (accepted, action) = match state { + DndState::Accepted => (1, atoms[XdndActionPrivate]), + DndState::Rejected => (0, atoms[DndNone]), + }; + self.xconn + .send_client_msg( + target_window, + target_window, + atoms[XdndFinished] as _, + None, + [this_window, accepted, action as _, 0, 0], + )? + .ignore_error(); + + Ok(()) + } + + pub unsafe fn get_type_list( + &self, + source_window: xproto::Window, + ) -> Result, util::GetPropertyError> { + let atoms = self.xconn.atoms(); + self.xconn.get_property( + source_window, + atoms[XdndTypeList], + xproto::Atom::from(xproto::AtomEnum::ATOM), + ) + } + + pub unsafe fn convert_selection( + &self, + window: xproto::Window, + time: xproto::Timestamp, + ) { + let atoms = self.xconn.atoms(); + self.xconn + .xcb_connection() + .convert_selection( + window, + atoms[XdndSelection], + atoms[TextUriList], + atoms[XdndSelection], + time, + ) + .expect_then_ignore_error("Failed to send XdndSelection event") + } + + pub unsafe fn read_data( + &self, + window: xproto::Window, + ) -> Result, util::GetPropertyError> { + let atoms = self.xconn.atoms(); + self.xconn + .get_property(window, atoms[XdndSelection], atoms[TextUriList]) + } + + pub fn parse_data( + &self, + data: &mut [c_uchar], + ) -> Result, DndDataParseError> { + if !data.is_empty() { + let mut path_list = Vec::new(); + let decoded = percent_decode(data).decode_utf8()?.into_owned(); + for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) { + // The format is specified as protocol://host/path + // However, it's typically simply protocol:///path + let path_str = if uri.starts_with("file://") { + let path_str = uri.replace("file://", ""); + if !path_str.starts_with('/') { + // A hostname is specified + // Supporting this case is beyond the scope of my mental health + return Err(DndDataParseError::HostnameSpecified(path_str)); + } + path_str + } else { + // Only the file protocol is supported + return Err(DndDataParseError::UnexpectedProtocol(uri.to_owned())); + }; + + let path = Path::new(&path_str).canonicalize()?; + path_list.push(path); + } + Ok(path_list) + } else { + Err(DndDataParseError::EmptyData) + } + } +} diff --git a/rio-window/src/platform_impl/linux/x11/event_processor.rs b/rio-window/src/platform_impl/linux/x11/event_processor.rs new file mode 100644 index 00000000..6e807c43 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/event_processor.rs @@ -0,0 +1,2059 @@ +use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, VecDeque}; +use std::os::raw::{c_char, c_int, c_long, c_ulong}; +use std::slice; +use std::sync::{Arc, Mutex}; + +use x11_dl::xinput2::{ + self, XIDeviceEvent, XIEnterEvent, XIFocusInEvent, XIFocusOutEvent, XIHierarchyEvent, + XILeaveEvent, XIModifierState, XIRawEvent, +}; +use x11_dl::xlib::{ + self, Display as XDisplay, Window as XWindow, XAnyEvent, XClientMessageEvent, + XConfigureEvent, XDestroyWindowEvent, XEvent, XExposeEvent, XKeyEvent, XMapEvent, + XPropertyEvent, XReparentEvent, XSelectionEvent, XVisibilityEvent, XkbAnyEvent, + XkbStateRec, +}; +use x11rb::protocol::xinput; +use x11rb::protocol::xkb::ID as XkbId; +use x11rb::protocol::xproto::{self, ConnectionExt as _, ModMask}; +use x11rb::x11_utils::{ExtensionInformation, Serialize}; +use xkbcommon_dl::xkb_mod_mask_t; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::event::{ + DeviceEvent, ElementState, Event, Ime, InnerSizeWriter, MouseButton, + MouseScrollDelta, RawKeyEvent, Touch, TouchPhase, WindowEvent, +}; +use crate::event_loop::ActiveEventLoop as RootAEL; +use crate::keyboard::ModifiersState; +use crate::platform_impl::common::xkb::{self, XkbState}; +use crate::platform_impl::platform::common::xkb::Context; +use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventReceiver, ImeRequest}; +use crate::platform_impl::platform::x11::ActiveEventLoop; +use crate::platform_impl::platform::ActiveEventLoop as PlatformActiveEventLoop; +use crate::platform_impl::x11::atoms::*; +use crate::platform_impl::x11::util::cookie::GenericEventCookie; +use crate::platform_impl::x11::{ + mkdid, mkwid, util, CookieResultExt, Device, DeviceId, DeviceInfo, Dnd, DndState, + ImeReceiver, ScrollOrientation, UnownedWindow, WindowId, +}; + +/// The maximum amount of X modifiers to replay. +pub const MAX_MOD_REPLAY_LEN: usize = 32; + +/// The X11 documentation states: "Keycodes lie in the inclusive range `[8, 255]`". +const KEYCODE_OFFSET: u8 = 8; + +pub struct EventProcessor { + pub dnd: Dnd, + pub ime_receiver: ImeReceiver, + pub ime_event_receiver: ImeEventReceiver, + pub randr_event_offset: u8, + pub devices: RefCell>, + pub xi2ext: ExtensionInformation, + pub xkbext: ExtensionInformation, + pub target: RootAEL, + pub xkb_context: Context, + // Number of touch events currently in progress + pub num_touch: u32, + // This is the last pressed key that is repeatable (if it hasn't been + // released). + // + // Used to detect key repeats. + pub held_key_press: Option, + pub first_touch: Option, + // Currently focused window belonging to this process + pub active_window: Option, + /// Latest modifiers we've sent for the user to trigger change in event. + pub modifiers: Cell, + pub xfiltered_modifiers: VecDeque, + pub xmodmap: util::ModifierKeymap, + pub is_composing: bool, +} + +impl EventProcessor { + pub fn process_event(&mut self, xev: &mut XEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + self.process_xevent(xev, &mut callback); + + let window_target = Self::window_target_mut(&mut self.target); + + // Handle IME requests. + while let Ok(request) = self.ime_receiver.try_recv() { + let ime = match window_target.ime.as_mut() { + Some(ime) => ime, + None => continue, + }; + let ime = ime.get_mut(); + match request { + ImeRequest::Position(window_id, x, y) => { + ime.send_xim_spot(window_id, x, y); + } + ImeRequest::Allow(window_id, allowed) => { + ime.set_ime_allowed(window_id, allowed); + } + } + } + + // Drain IME events. + while let Ok((window, event)) = self.ime_event_receiver.try_recv() { + let window_id = mkwid(window as xproto::Window); + let event = match event { + ImeEvent::Enabled => WindowEvent::Ime(Ime::Enabled), + ImeEvent::Start => { + self.is_composing = true; + WindowEvent::Ime(Ime::Preedit("".to_owned(), None)) + } + ImeEvent::Update(text, position) if self.is_composing => { + WindowEvent::Ime(Ime::Preedit(text, Some((position, position)))) + } + ImeEvent::End => { + self.is_composing = false; + // Issue empty preedit on `Done`. + WindowEvent::Ime(Ime::Preedit(String::new(), None)) + } + ImeEvent::Disabled => { + self.is_composing = false; + WindowEvent::Ime(Ime::Disabled) + } + _ => continue, + }; + + callback(&self.target, Event::WindowEvent { window_id, event }); + } + } + + /// XFilterEvent tells us when an event has been discarded by the input method. + /// Specifically, this involves all of the KeyPress events in compose/pre-edit sequences, + /// along with an extra copy of the KeyRelease events. This also prevents backspace and + /// arrow keys from being detected twice. + fn filter_event(&mut self, xev: &mut XEvent) -> bool { + let wt = Self::window_target(&self.target); + unsafe { + (wt.xconn.xlib.XFilterEvent)(xev, { + let xev: &XAnyEvent = xev.as_ref(); + xev.window + }) == xlib::True + } + } + + fn process_xevent(&mut self, xev: &mut XEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let event_type = xev.get_type(); + + if self.filter_event(xev) { + if event_type == xlib::KeyPress || event_type == xlib::KeyRelease { + let xev: &XKeyEvent = xev.as_ref(); + if self.xmodmap.is_modifier(xev.keycode as u8) { + // Don't grow the buffer past the `MAX_MOD_REPLAY_LEN`. This could happen + // when the modifiers are consumed entirely or serials are altered. + // + // Both cases shouldn't happen in well behaving clients. + if self.xfiltered_modifiers.len() == MAX_MOD_REPLAY_LEN { + self.xfiltered_modifiers.pop_back(); + } + self.xfiltered_modifiers.push_front(xev.serial); + } + } + return; + } + + match event_type { + xlib::ClientMessage => self.client_message(xev.as_ref(), &mut callback), + xlib::SelectionNotify => self.selection_notify(xev.as_ref(), &mut callback), + xlib::ConfigureNotify => self.configure_notify(xev.as_ref(), &mut callback), + xlib::ReparentNotify => self.reparent_notify(xev.as_ref()), + xlib::MapNotify => self.map_notify(xev.as_ref(), &mut callback), + xlib::DestroyNotify => self.destroy_notify(xev.as_ref(), &mut callback), + xlib::PropertyNotify => self.property_notify(xev.as_ref(), &mut callback), + xlib::VisibilityNotify => self.visibility_notify(xev.as_ref(), &mut callback), + xlib::Expose => self.expose(xev.as_ref(), &mut callback), + // Note that in compose/pre-edit sequences, we'll always receive KeyRelease events. + ty @ xlib::KeyPress | ty @ xlib::KeyRelease => { + let state = if ty == xlib::KeyPress { + ElementState::Pressed + } else { + ElementState::Released + }; + + self.xinput_key_input(xev.as_mut(), state, &mut callback); + } + xlib::GenericEvent => { + let wt = Self::window_target(&self.target); + let xev: GenericEventCookie = + match GenericEventCookie::from_event(wt.xconn.clone(), *xev) { + Some(xev) if xev.extension() == self.xi2ext.major_opcode => xev, + _ => return, + }; + + let evtype = xev.evtype(); + + match evtype { + ty @ xinput2::XI_ButtonPress | ty @ xinput2::XI_ButtonRelease => { + let state = if ty == xinput2::XI_ButtonPress { + ElementState::Pressed + } else { + ElementState::Released + }; + + let xev: &XIDeviceEvent = unsafe { xev.as_event() }; + self.update_mods_from_xinput2_event( + &xev.mods, + &xev.group, + false, + &mut callback, + ); + self.xinput2_button_input(xev, state, &mut callback); + } + xinput2::XI_Motion => { + let xev: &XIDeviceEvent = unsafe { xev.as_event() }; + self.update_mods_from_xinput2_event( + &xev.mods, + &xev.group, + false, + &mut callback, + ); + self.xinput2_mouse_motion(xev, &mut callback); + } + xinput2::XI_Enter => { + let xev: &XIEnterEvent = unsafe { xev.as_event() }; + self.xinput2_mouse_enter(xev, &mut callback); + } + xinput2::XI_Leave => { + let xev: &XILeaveEvent = unsafe { xev.as_event() }; + self.update_mods_from_xinput2_event( + &xev.mods, + &xev.group, + false, + &mut callback, + ); + self.xinput2_mouse_left(xev, &mut callback); + } + xinput2::XI_FocusIn => { + let xev: &XIFocusInEvent = unsafe { xev.as_event() }; + self.xinput2_focused(xev, &mut callback); + } + xinput2::XI_FocusOut => { + let xev: &XIFocusOutEvent = unsafe { xev.as_event() }; + self.xinput2_unfocused(xev, &mut callback); + } + xinput2::XI_TouchBegin + | xinput2::XI_TouchUpdate + | xinput2::XI_TouchEnd => { + let phase = match evtype { + xinput2::XI_TouchBegin => TouchPhase::Started, + xinput2::XI_TouchUpdate => TouchPhase::Moved, + xinput2::XI_TouchEnd => TouchPhase::Ended, + _ => unreachable!(), + }; + + let xev: &XIDeviceEvent = unsafe { xev.as_event() }; + self.xinput2_touch(xev, phase, &mut callback); + } + xinput2::XI_RawButtonPress | xinput2::XI_RawButtonRelease => { + let state = match evtype { + xinput2::XI_RawButtonPress => ElementState::Pressed, + xinput2::XI_RawButtonRelease => ElementState::Released, + _ => unreachable!(), + }; + + let xev: &XIRawEvent = unsafe { xev.as_event() }; + self.xinput2_raw_button_input(xev, state, &mut callback); + } + xinput2::XI_RawMotion => { + let xev: &XIRawEvent = unsafe { xev.as_event() }; + self.xinput2_raw_mouse_motion(xev, &mut callback); + } + xinput2::XI_RawKeyPress | xinput2::XI_RawKeyRelease => { + let state = match evtype { + xinput2::XI_RawKeyPress => ElementState::Pressed, + xinput2::XI_RawKeyRelease => ElementState::Released, + _ => unreachable!(), + }; + + let xev: &xinput2::XIRawEvent = unsafe { xev.as_event() }; + self.xinput2_raw_key_input(xev, state, &mut callback); + } + + xinput2::XI_HierarchyChanged => { + let xev: &XIHierarchyEvent = unsafe { xev.as_event() }; + self.xinput2_hierarchy_changed(xev, &mut callback); + } + _ => {} + } + } + _ => { + if event_type == self.xkbext.first_event as _ { + let xev: &XkbAnyEvent = + unsafe { &*(xev as *const _ as *const XkbAnyEvent) }; + self.xkb_event(xev, &mut callback); + } + if event_type == self.randr_event_offset as c_int { + self.process_dpi_change(&mut callback); + } + } + } + } + + pub fn poll(&self) -> bool { + let window_target = Self::window_target(&self.target); + let result = + unsafe { (window_target.xconn.xlib.XPending)(window_target.xconn.display) }; + + result != 0 + } + + pub unsafe fn poll_one_event(&mut self, event_ptr: *mut XEvent) -> bool { + let window_target = Self::window_target(&self.target); + // This function is used to poll and remove a single event + // from the Xlib event queue in a non-blocking, atomic way. + // XCheckIfEvent is non-blocking and removes events from queue. + // XNextEvent can't be used because it blocks while holding the + // global Xlib mutex. + // XPeekEvent does not remove events from the queue. + unsafe extern "C" fn predicate( + _display: *mut XDisplay, + _event: *mut XEvent, + _arg: *mut c_char, + ) -> c_int { + // This predicate always returns "true" (1) to accept all events + 1 + } + + let result = unsafe { + (window_target.xconn.xlib.XCheckIfEvent)( + window_target.xconn.display, + event_ptr, + Some(predicate), + std::ptr::null_mut(), + ) + }; + + result != 0 + } + + pub fn init_device(&self, device: xinput::DeviceId) { + let window_target = Self::window_target(&self.target); + let mut devices = self.devices.borrow_mut(); + if let Some(info) = DeviceInfo::get(&window_target.xconn, device as _) { + for info in info.iter() { + devices.insert(DeviceId(info.deviceid as _), Device::new(info)); + } + } + } + + pub fn with_window( + &self, + window_id: xproto::Window, + callback: F, + ) -> Option + where + F: Fn(&Arc) -> Ret, + { + let mut deleted = false; + let window_id = WindowId(window_id as _); + let window_target = Self::window_target(&self.target); + let result = window_target + .windows + .borrow() + .get(&window_id) + .and_then(|window| { + let arc = window.upgrade(); + deleted = arc.is_none(); + arc + }) + .map(|window| callback(&window)); + + if deleted { + // Garbage collection + window_target.windows.borrow_mut().remove(&window_id); + } + + result + } + + // NOTE: we avoid `self` to not borrow the entire `self` as not mut. + /// Get the platform window target. + pub fn window_target(window_target: &RootAEL) -> &ActiveEventLoop { + match &window_target.p { + PlatformActiveEventLoop::X(target) => target, + #[cfg(wayland_platform)] + _ => unreachable!(), + } + } + + /// Get the platform window target. + pub fn window_target_mut(window_target: &mut RootAEL) -> &mut ActiveEventLoop { + match &mut window_target.p { + PlatformActiveEventLoop::X(target) => target, + #[cfg(wayland_platform)] + _ => unreachable!(), + } + } + + fn client_message( + &mut self, + xev: &XClientMessageEvent, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let atoms = wt.xconn.atoms(); + + let window = xev.window as xproto::Window; + let window_id = mkwid(window); + + if xev.data.get_long(0) as xproto::Atom == wt.wm_delete_window { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CloseRequested, + }; + callback(&self.target, event); + return; + } + + if xev.data.get_long(0) as xproto::Atom == wt.net_wm_ping { + let client_msg = xproto::ClientMessageEvent { + response_type: xproto::CLIENT_MESSAGE_EVENT, + format: xev.format as _, + sequence: xev.serial as _, + window: wt.root, + type_: xev.message_type as _, + data: xproto::ClientMessageData::from({ + let [a, b, c, d, e]: [c_long; 5] = + xev.data.as_longs().try_into().unwrap(); + [a as u32, b as u32, c as u32, d as u32, e as u32] + }), + }; + + wt.xconn + .xcb_connection() + .send_event( + false, + wt.root, + xproto::EventMask::SUBSTRUCTURE_NOTIFY + | xproto::EventMask::SUBSTRUCTURE_REDIRECT, + client_msg.serialize(), + ) + .expect_then_ignore_error("Failed to send `ClientMessage` event."); + return; + } + + if xev.message_type == atoms[XdndEnter] as c_ulong { + let source_window = xev.data.get_long(0) as xproto::Window; + let flags = xev.data.get_long(1); + let version = flags >> 24; + self.dnd.version = Some(version); + let has_more_types = flags - (flags & (c_long::max_value() - 1)) == 1; + if !has_more_types { + let type_list = vec![ + xev.data.get_long(2) as xproto::Atom, + xev.data.get_long(3) as xproto::Atom, + xev.data.get_long(4) as xproto::Atom, + ]; + self.dnd.type_list = Some(type_list); + } else if let Ok(more_types) = + unsafe { self.dnd.get_type_list(source_window) } + { + self.dnd.type_list = Some(more_types); + } + return; + } + + if xev.message_type == atoms[XdndPosition] as c_ulong { + // This event occurs every time the mouse moves while a file's being dragged + // over our window. We emit HoveredFile in response; while the macOS backend + // does that upon a drag entering, XDND doesn't have access to the actual drop + // data until this event. For parity with other platforms, we only emit + // `HoveredFile` the first time, though if winit's API is later extended to + // supply position updates with `HoveredFile` or another event, implementing + // that here would be trivial. + + let source_window = xev.data.get_long(0) as xproto::Window; + + // Equivalent to `(x << shift) | y` + // where `shift = mem::size_of::() * 8` + // Note that coordinates are in "desktop space", not "window space" + // (in X11 parlance, they're root window coordinates) + // let packed_coordinates = xev.data.get_long(2); + // let shift = mem::size_of::() * 8; + // let x = packed_coordinates >> shift; + // let y = packed_coordinates & !(x << shift); + + // By our own state flow, `version` should never be `None` at this point. + let version = self.dnd.version.unwrap_or(5); + + // Action is specified in versions 2 and up, though we don't need it anyway. + // let action = xev.data.get_long(4); + + let accepted = if let Some(ref type_list) = self.dnd.type_list { + type_list.contains(&atoms[TextUriList]) + } else { + false + }; + + if !accepted { + unsafe { + self.dnd + .send_status(window, source_window, DndState::Rejected) + .expect("Failed to send `XdndStatus` message."); + } + self.dnd.reset(); + return; + } + + self.dnd.source_window = Some(source_window); + if self.dnd.result.is_none() { + let time = if version >= 1 { + xev.data.get_long(3) as xproto::Timestamp + } else { + // In version 0, time isn't specified + x11rb::CURRENT_TIME + }; + + // Log this timestamp. + wt.xconn.set_timestamp(time); + + // This results in the `SelectionNotify` event below + unsafe { + self.dnd.convert_selection(window, time); + } + } + + unsafe { + self.dnd + .send_status(window, source_window, DndState::Accepted) + .expect("Failed to send `XdndStatus` message."); + } + return; + } + + if xev.message_type == atoms[XdndDrop] as c_ulong { + let (source_window, state) = + if let Some(source_window) = self.dnd.source_window { + if let Some(Ok(ref path_list)) = self.dnd.result { + for path in path_list { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::DroppedFile(path.clone()), + }; + callback(&self.target, event); + } + } + (source_window, DndState::Accepted) + } else { + // `source_window` won't be part of our DND state if we already rejected the drop in + // our `XdndPosition` handler. + let source_window = xev.data.get_long(0) as xproto::Window; + (source_window, DndState::Rejected) + }; + + unsafe { + self.dnd + .send_finished(window, source_window, state) + .expect("Failed to send `XdndFinished` message."); + } + + self.dnd.reset(); + return; + } + + if xev.message_type == atoms[XdndLeave] as c_ulong { + self.dnd.reset(); + let event = Event::WindowEvent { + window_id, + event: WindowEvent::HoveredFileCancelled, + }; + callback(&self.target, event); + } + } + + fn selection_notify(&mut self, xev: &XSelectionEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let atoms = wt.xconn.atoms(); + + let window = xev.requestor as xproto::Window; + let window_id = mkwid(window); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if xev.property != atoms[XdndSelection] as c_ulong { + return; + } + + // This is where we receive data from drag and drop + self.dnd.result = None; + if let Ok(mut data) = unsafe { self.dnd.read_data(window) } { + let parse_result = self.dnd.parse_data(&mut data); + if let Ok(ref path_list) = parse_result { + for path in path_list { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::HoveredFile(path.clone()), + }; + callback(&self.target, event); + } + } + self.dnd.result = Some(parse_result); + } + } + + fn configure_notify(&self, xev: &XConfigureEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + let xwindow = xev.window as xproto::Window; + let window_id = mkwid(xwindow); + + let window = match self.with_window(xwindow, Arc::clone) { + Some(window) => window, + None => return, + }; + + // So apparently... + // `XSendEvent` (synthetic `ConfigureNotify`) -> position relative to root + // `XConfigureNotify` (real `ConfigureNotify`) -> position relative to parent + // https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.5 + // We don't want to send `Moved` when this is false, since then every `Resized` + // (whether the window moved or not) is accompanied by an extraneous `Moved` event + // that has a position relative to the parent window. + let is_synthetic = xev.send_event == xlib::True; + + // These are both in physical space. + let new_inner_size = (xev.width as u32, xev.height as u32); + let new_inner_position = (xev.x, xev.y); + + let (mut resized, moved) = { + let mut shared_state_lock = window.shared_state_lock(); + + let resized = util::maybe_change(&mut shared_state_lock.size, new_inner_size); + let moved = if is_synthetic { + util::maybe_change( + &mut shared_state_lock.inner_position, + new_inner_position, + ) + } else { + // Detect when frame extents change. + // Since this isn't synthetic, as per the notes above, this position is relative to + // the parent window. + let rel_parent = new_inner_position; + if util::maybe_change( + &mut shared_state_lock.inner_position_rel_parent, + rel_parent, + ) { + // This ensures we process the next `Moved`. + shared_state_lock.inner_position = None; + // Extra insurance against stale frame extents. + shared_state_lock.frame_extents = None; + } + false + }; + (resized, moved) + }; + + let position = window.shared_state_lock().position; + + let new_outer_position = if let (Some(position), false) = (position, moved) { + position + } else { + let mut shared_state_lock = window.shared_state_lock(); + + // We need to convert client area position to window position. + let frame_extents = shared_state_lock + .frame_extents + .as_ref() + .cloned() + .unwrap_or_else(|| { + let frame_extents = + wt.xconn.get_frame_extents_heuristic(xwindow, wt.root); + shared_state_lock.frame_extents = Some(frame_extents.clone()); + frame_extents + }); + let outer = frame_extents + .inner_pos_to_outer(new_inner_position.0, new_inner_position.1); + shared_state_lock.position = Some(outer); + + // Unlock shared state to prevent deadlock in callback below + drop(shared_state_lock); + + if moved { + callback( + &self.target, + Event::WindowEvent { + window_id, + event: WindowEvent::Moved(outer.into()), + }, + ); + } + outer + }; + + if is_synthetic { + let mut shared_state_lock = window.shared_state_lock(); + // If we don't use the existing adjusted value when available, then the user can screw + // up the resizing by dragging across monitors *without* dropping the + // window. + let (width, height) = shared_state_lock + .dpi_adjusted + .unwrap_or((xev.width as u32, xev.height as u32)); + + let last_scale_factor = shared_state_lock.last_monitor.scale_factor; + let new_scale_factor = { + let window_rect = util::AaRect::new(new_outer_position, new_inner_size); + let monitor = wt + .xconn + .get_monitor_for_window(Some(window_rect)) + .expect("Failed to find monitor for window"); + + if monitor.is_dummy() { + // Avoid updating monitor using a dummy monitor handle + last_scale_factor + } else { + shared_state_lock.last_monitor = monitor.clone(); + monitor.scale_factor + } + }; + if last_scale_factor != new_scale_factor { + let (new_width, new_height) = window.adjust_for_dpi( + last_scale_factor, + new_scale_factor, + width, + height, + &shared_state_lock, + ); + + let old_inner_size = PhysicalSize::new(width, height); + let new_inner_size = PhysicalSize::new(new_width, new_height); + + // Unlock shared state to prevent deadlock in callback below + drop(shared_state_lock); + + let inner_size = Arc::new(Mutex::new(new_inner_size)); + callback( + &self.target, + Event::WindowEvent { + window_id, + event: WindowEvent::ScaleFactorChanged { + scale_factor: new_scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade( + &inner_size, + )), + }, + }, + ); + + let new_inner_size = *inner_size.lock().unwrap(); + drop(inner_size); + + if new_inner_size != old_inner_size { + window.request_inner_size_physical( + new_inner_size.width, + new_inner_size.height, + ); + window.shared_state_lock().dpi_adjusted = Some(new_inner_size.into()); + // if the DPI factor changed, force a resize event to ensure the logical + // size is computed with the right DPI factor + resized = true; + } + } + } + + // NOTE: Ensure that the lock is dropped before handling the resized and + // sending the event back to user. + let hittest = { + let mut shared_state_lock = window.shared_state_lock(); + let hittest = shared_state_lock.cursor_hittest; + + // This is a hack to ensure that the DPI adjusted resize is actually + // applied on all WMs. KWin doesn't need this, but Xfwm does. The hack + // should not be run on other WMs, since tiling WMs constrain the window + // size, making the resize fail. This would cause an endless stream of + // XResizeWindow requests, making Xorg, the winit client, and the WM + // consume 100% of CPU. + if let Some(adjusted_size) = shared_state_lock.dpi_adjusted { + if new_inner_size == adjusted_size || !util::wm_name_is_one_of(&["Xfwm4"]) + { + // When this finally happens, the event will not be synthetic. + shared_state_lock.dpi_adjusted = None; + } else { + // Unlock shared state to prevent deadlock in callback below + drop(shared_state_lock); + window.request_inner_size_physical(adjusted_size.0, adjusted_size.1); + } + } + + hittest + }; + + // Reload hittest. + if hittest.unwrap_or(false) { + let _ = window.set_cursor_hittest(true); + } + + if resized { + callback( + &self.target, + Event::WindowEvent { + window_id, + event: WindowEvent::Resized(new_inner_size.into()), + }, + ); + } + } + + /// This is generally a reliable way to detect when the window manager's been + /// replaced, though this event is only fired by reparenting window managers + /// (which is almost all of them). Failing to correctly update WM info doesn't + /// really have much impact, since on the WMs affected (xmonad, dwm, etc.) the only + /// effect is that we waste some time trying to query unsupported properties. + fn reparent_notify(&self, xev: &XReparentEvent) { + let wt = Self::window_target(&self.target); + + wt.xconn.update_cached_wm_info(wt.root); + + self.with_window(xev.window as xproto::Window, |window| { + window.invalidate_cached_frame_extents(); + }); + } + + fn map_notify(&self, xev: &XMapEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let window = xev.window as xproto::Window; + let window_id = mkwid(window); + + // NOTE: Re-issue the focus state when mapping the window. + // + // The purpose of it is to deliver initial focused state of the newly created + // window, given that we can't rely on `CreateNotify`, due to it being not + // sent. + let focus = self + .with_window(window, |window| window.has_focus()) + .unwrap_or_default(); + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Focused(focus), + }; + + callback(&self.target, event); + } + + fn destroy_notify(&self, xev: &XDestroyWindowEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + let window = xev.window as xproto::Window; + let window_id = mkwid(window); + + // In the event that the window's been destroyed without being dropped first, we + // cleanup again here. + wt.windows.borrow_mut().remove(&WindowId(window as _)); + + // Since all XIM stuff needs to happen from the same thread, we destroy the input + // context here instead of when dropping the window. + if let Some(ime) = wt.ime.as_ref() { + ime.borrow_mut() + .remove_context(window as XWindow) + .expect("Failed to destroy input context"); + } + + callback( + &self.target, + Event::WindowEvent { + window_id, + event: WindowEvent::Destroyed, + }, + ); + } + + fn property_notify(&mut self, xev: &XPropertyEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let atoms = wt.x_connection().atoms(); + let atom = xev.atom as xproto::Atom; + + if atom == xproto::Atom::from(xproto::AtomEnum::RESOURCE_MANAGER) + || atom == atoms[_XSETTINGS_SETTINGS] + { + self.process_dpi_change(&mut callback); + } + } + + fn visibility_notify(&self, xev: &XVisibilityEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let xwindow = xev.window as xproto::Window; + + let event = Event::WindowEvent { + window_id: mkwid(xwindow), + event: WindowEvent::Occluded(xev.state == xlib::VisibilityFullyObscured), + }; + callback(&self.target, event); + + self.with_window(xwindow, |window| { + window.visibility_notify(); + }); + } + + fn expose(&self, xev: &XExposeEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + // Multiple Expose events may be received for subareas of a window. + // We issue `RedrawRequested` only for the last event of such a series. + if xev.count == 0 { + let window = xev.window as xproto::Window; + let window_id = mkwid(window); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::RedrawRequested, + }; + + callback(&self.target, event); + } + } + + fn xinput_key_input( + &mut self, + xev: &mut XKeyEvent, + state: ElementState, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let window = match self.active_window { + Some(window) => window, + None => return, + }; + + let window_id = mkwid(window); + let device_id = mkdid(util::VIRTUAL_CORE_KEYBOARD); + + let keycode = xev.keycode as _; + + // Update state to track key repeats and determine whether this key was a repeat. + // + // Note, when a key is held before focusing on this window the first + // (non-synthetic) event will not be flagged as a repeat (also note that the + // synthetic press event that is generated before this when the window gains focus + // will also not be flagged as a repeat). + // + // Only keys that can repeat should change the held_key_press state since a + // continuously held repeatable key may continue repeating after the press of a + // non-repeatable key. + let key_repeats = self + .xkb_context + .keymap_mut() + .map(|k| k.key_repeats(keycode)) + .unwrap_or(false); + let repeat = if key_repeats { + let is_latest_held = self.held_key_press == Some(keycode); + + if state == ElementState::Pressed { + self.held_key_press = Some(keycode); + is_latest_held + } else { + // Check that the released key is the latest repeatable key that has been + // pressed, since repeats will continue for the latest key press if a + // different previously pressed key is released. + if is_latest_held { + self.held_key_press = None; + } + false + } + } else { + false + }; + + // NOTE: When the modifier was captured by the XFilterEvents the modifiers for the modifier + // itself are out of sync due to XkbState being delivered before XKeyEvent, since it's + // being replayed by the XIM, thus we should replay ourselves. + let replay = if let Some(position) = self + .xfiltered_modifiers + .iter() + .rev() + .position(|&s| s == xev.serial) + { + // We don't have to replay modifiers pressed before the current event if some events + // were not forwarded to us, since their state is irrelevant. + self.xfiltered_modifiers + .resize(self.xfiltered_modifiers.len() - 1 - position, 0); + true + } else { + false + }; + + // Always update the modifiers when we're not replaying. + if !replay { + self.update_mods_from_core_event(window_id, xev.state as u16, &mut callback); + } + + if keycode != 0 && !self.is_composing { + // Don't alter the modifiers state from replaying. + if replay { + self.send_synthic_modifier_from_core( + window_id, + xev.state as u16, + &mut callback, + ); + } + + if let Some(mut key_processor) = self.xkb_context.key_context() { + let event = key_processor.process_key_event(keycode, state, repeat); + let event = Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { + device_id, + event, + is_synthetic: false, + }, + }; + callback(&self.target, event); + } + + // Restore the client's modifiers state after replay. + if replay { + self.send_modifiers(window_id, self.modifiers.get(), true, &mut callback); + } + + return; + } + + let wt = Self::window_target(&self.target); + + if let Some(ic) = wt + .ime + .as_ref() + .and_then(|ime| ime.borrow().get_context(window as XWindow)) + { + let written = wt.xconn.lookup_utf8(ic, xev); + if !written.is_empty() { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }; + callback(&self.target, event); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Ime(Ime::Commit(written)), + }; + + self.is_composing = false; + callback(&self.target, event); + } + } + } + + fn send_synthic_modifier_from_core( + &mut self, + window_id: crate::window::WindowId, + state: u16, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let keymap = match self.xkb_context.keymap_mut() { + Some(keymap) => keymap, + None => return, + }; + + let wt = Self::window_target(&self.target); + let xcb = wt.xconn.xcb_connection().get_raw_xcb_connection(); + + // Use synthetic state since we're replaying the modifier. The user modifier state + // will be restored later. + let mut xkb_state = match XkbState::new_x11(xcb, keymap) { + Some(xkb_state) => xkb_state, + None => return, + }; + + let mask = self.xkb_mod_mask_from_core(state); + xkb_state.update_modifiers(mask, 0, 0, 0, 0, Self::core_keyboard_group(state)); + let mods: ModifiersState = xkb_state.modifiers().into(); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::ModifiersChanged(mods.into()), + }; + + callback(&self.target, event); + } + + fn xinput2_button_input( + &self, + event: &XIDeviceEvent, + state: ElementState, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let window_id = mkwid(event.event as xproto::Window); + let device_id = mkdid(event.deviceid as xinput::DeviceId); + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + // Deliver multi-touch events instead of emulated mouse events. + if (event.flags & xinput2::XIPointerEmulated) != 0 { + return; + } + + let event = match event.detail as u32 { + xlib::Button1 => WindowEvent::MouseInput { + device_id, + state, + button: MouseButton::Left, + }, + xlib::Button2 => WindowEvent::MouseInput { + device_id, + state, + button: MouseButton::Middle, + }, + + xlib::Button3 => WindowEvent::MouseInput { + device_id, + state, + button: MouseButton::Right, + }, + + // Suppress emulated scroll wheel clicks, since we handle the real motion events for + // those. In practice, even clicky scroll wheels appear to be reported by + // evdev (and XInput2 in turn) as axis motion, so we don't otherwise + // special-case these button presses. + 4..=7 => WindowEvent::MouseWheel { + device_id, + delta: match event.detail { + 4 => MouseScrollDelta::LineDelta(0.0, 1.0), + 5 => MouseScrollDelta::LineDelta(0.0, -1.0), + 6 => MouseScrollDelta::LineDelta(1.0, 0.0), + 7 => MouseScrollDelta::LineDelta(-1.0, 0.0), + _ => unreachable!(), + }, + phase: TouchPhase::Moved, + }, + 8 => WindowEvent::MouseInput { + device_id, + state, + button: MouseButton::Back, + }, + + 9 => WindowEvent::MouseInput { + device_id, + state, + button: MouseButton::Forward, + }, + x => WindowEvent::MouseInput { + device_id, + state, + button: MouseButton::Other(x as u16), + }, + }; + + let event = Event::WindowEvent { window_id, event }; + callback(&self.target, event); + } + + fn xinput2_mouse_motion(&self, event: &XIDeviceEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + let device_id = mkdid(event.deviceid as xinput::DeviceId); + let window = event.event as xproto::Window; + let window_id = mkwid(window); + let new_cursor_pos = (event.event_x, event.event_y); + + let cursor_moved = self.with_window(window, |window| { + let mut shared_state_lock = window.shared_state_lock(); + util::maybe_change(&mut shared_state_lock.cursor_pos, new_cursor_pos) + }); + + if cursor_moved == Some(true) { + let position = PhysicalPosition::new(event.event_x, event.event_y); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { + device_id, + position, + }, + }; + callback(&self.target, event); + } else if cursor_moved.is_none() { + return; + } + + // More gymnastics, for self.devices + let mask = unsafe { + slice::from_raw_parts(event.valuators.mask, event.valuators.mask_len as usize) + }; + let mut devices = self.devices.borrow_mut(); + let physical_device = + match devices.get_mut(&DeviceId(event.sourceid as xinput::DeviceId)) { + Some(device) => device, + None => return, + }; + + let mut events = Vec::new(); + let mut value = event.valuators.values; + for i in 0..event.valuators.mask_len * 8 { + if !xinput2::XIMaskIsSet(mask, i) { + continue; + } + + let x = unsafe { *value }; + + let event = if let Some(&mut (_, ref mut info)) = physical_device + .scroll_axes + .iter_mut() + .find(|&&mut (axis, _)| axis == i as _) + { + let delta = (x - info.position) / info.increment; + info.position = x; + // X11 vertical scroll coordinates are opposite to winit's + let delta = match info.orientation { + ScrollOrientation::Horizontal => { + MouseScrollDelta::LineDelta(-delta as f32, 0.0) + } + ScrollOrientation::Vertical => { + MouseScrollDelta::LineDelta(0.0, -delta as f32) + } + }; + + WindowEvent::MouseWheel { + device_id, + delta, + phase: TouchPhase::Moved, + } + } else { + WindowEvent::AxisMotion { + device_id, + axis: i as u32, + value: unsafe { *value }, + } + }; + + events.push(Event::WindowEvent { window_id, event }); + + value = unsafe { value.offset(1) }; + } + + for event in events { + callback(&self.target, event); + } + } + + fn xinput2_mouse_enter(&self, event: &XIEnterEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + let window = event.event as xproto::Window; + let window_id = mkwid(window); + let device_id = mkdid(event.deviceid as xinput::DeviceId); + + if let Some(all_info) = DeviceInfo::get(&wt.xconn, super::ALL_DEVICES.into()) { + let mut devices = self.devices.borrow_mut(); + for device_info in all_info.iter() { + // The second expression is need for resetting to work correctly on i3, and + // presumably some other WMs. On those, `XI_Enter` doesn't include the physical + // device ID, so both `sourceid` and `deviceid` are the virtual device. + if device_info.deviceid == event.sourceid + || device_info.attachment == event.sourceid + { + let device_id = DeviceId(device_info.deviceid as _); + if let Some(device) = devices.get_mut(&device_id) { + device.reset_scroll_position(device_info); + } + } + } + } + + if self.window_exists(window) { + let position = PhysicalPosition::new(event.event_x, event.event_y); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorEntered { device_id }, + }; + callback(&self.target, event); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { + device_id, + position, + }, + }; + callback(&self.target, event); + } + } + + fn xinput2_mouse_left(&self, event: &XILeaveEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let window = event.event as xproto::Window; + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + // Leave, FocusIn, and FocusOut can be received by a window that's already + // been destroyed, which the user presumably doesn't want to deal with. + if self.window_exists(window) { + let event = Event::WindowEvent { + window_id: mkwid(window), + event: WindowEvent::CursorLeft { + device_id: mkdid(event.deviceid as xinput::DeviceId), + }, + }; + callback(&self.target, event); + } + } + + fn xinput2_focused(&mut self, xev: &XIFocusInEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let window = xev.event as xproto::Window; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if let Some(ime) = wt.ime.as_ref() { + ime.borrow_mut() + .focus(xev.event) + .expect("Failed to focus input context"); + } + + if self.active_window == Some(window) { + return; + } + + self.active_window = Some(window); + + wt.update_listen_device_events(true); + + let window_id = mkwid(window); + let position = PhysicalPosition::new(xev.event_x, xev.event_y); + + if let Some(window) = self.with_window(window, Arc::clone) { + window.shared_state_lock().has_focus = true; + } + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Focused(true), + }; + callback(&self.target, event); + + // Issue key press events for all pressed keys + Self::handle_pressed_keys( + &self.target, + window_id, + ElementState::Pressed, + &mut self.xkb_context, + &mut callback, + ); + + self.update_mods_from_query(window_id, &mut callback); + + // The deviceid for this event is for a keyboard instead of a pointer, + // so we have to do a little extra work. + let pointer_id = self + .devices + .borrow() + .get(&DeviceId(xev.deviceid as xinput::DeviceId)) + .map(|device| device.attachment) + .unwrap_or(2); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { + device_id: mkdid(pointer_id as _), + position, + }, + }; + callback(&self.target, event); + } + + fn xinput2_unfocused(&mut self, xev: &XIFocusOutEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let window = xev.event as xproto::Window; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if !self.window_exists(window) { + return; + } + + if let Some(ime) = wt.ime.as_ref() { + ime.borrow_mut() + .unfocus(xev.event) + .expect("Failed to unfocus input context"); + } + + if self.active_window.take() == Some(window) { + let window_id = mkwid(window); + + wt.update_listen_device_events(false); + + // Clear the modifiers when unfocusing the window. + if let Some(xkb_state) = self.xkb_context.state_mut() { + xkb_state.update_modifiers(0, 0, 0, 0, 0, 0); + let mods = xkb_state.modifiers(); + self.send_modifiers(window_id, mods.into(), true, &mut callback); + } + + // Issue key release events for all pressed keys + Self::handle_pressed_keys( + &self.target, + window_id, + ElementState::Released, + &mut self.xkb_context, + &mut callback, + ); + + // Clear this so detecting key repeats is consistently handled when the + // window regains focus. + self.held_key_press = None; + + if let Some(window) = self.with_window(window, Arc::clone) { + window.shared_state_lock().has_focus = false; + } + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Focused(false), + }; + callback(&self.target, event) + } + } + + fn xinput2_touch( + &mut self, + xev: &XIDeviceEvent, + phase: TouchPhase, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let window = xev.event as xproto::Window; + if self.window_exists(window) { + let window_id = mkwid(window); + let id = xev.detail as u64; + let location = PhysicalPosition::new(xev.event_x, xev.event_y); + + // Mouse cursor position changes when touch events are received. + // Only the first concurrently active touch ID moves the mouse cursor. + if is_first_touch(&mut self.first_touch, &mut self.num_touch, id, phase) { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { + device_id: mkdid(util::VIRTUAL_CORE_POINTER), + position: location.cast(), + }, + }; + callback(&self.target, event); + } + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Touch(Touch { + device_id: mkdid(xev.deviceid as xinput::DeviceId), + phase, + location, + force: None, // TODO + id, + }), + }; + callback(&self.target, event) + } + } + + fn xinput2_raw_button_input( + &self, + xev: &XIRawEvent, + state: ElementState, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if xev.flags & xinput2::XIPointerEmulated == 0 { + let event = Event::DeviceEvent { + device_id: mkdid(xev.deviceid as xinput::DeviceId), + event: DeviceEvent::Button { + state, + button: xev.detail as u32, + }, + }; + callback(&self.target, event); + } + } + + fn xinput2_raw_mouse_motion(&self, xev: &XIRawEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let did = mkdid(xev.deviceid as xinput::DeviceId); + + let mask = unsafe { + slice::from_raw_parts(xev.valuators.mask, xev.valuators.mask_len as usize) + }; + let mut value = xev.raw_values; + let mut mouse_delta = util::Delta::default(); + let mut scroll_delta = util::Delta::default(); + for i in 0..xev.valuators.mask_len * 8 { + if !xinput2::XIMaskIsSet(mask, i) { + continue; + } + let x = unsafe { value.read_unaligned() }; + + // We assume that every XInput2 device with analog axes is a pointing device emitting + // relative coordinates. + match i { + 0 => mouse_delta.set_x(x), + 1 => mouse_delta.set_y(x), + 2 => scroll_delta.set_x(x as f32), + 3 => scroll_delta.set_y(x as f32), + _ => {} + } + + let event = Event::DeviceEvent { + device_id: did, + event: DeviceEvent::Motion { + axis: i as u32, + value: x, + }, + }; + callback(&self.target, event); + + value = unsafe { value.offset(1) }; + } + + if let Some(mouse_delta) = mouse_delta.consume() { + let event = Event::DeviceEvent { + device_id: did, + event: DeviceEvent::MouseMotion { delta: mouse_delta }, + }; + callback(&self.target, event); + } + + if let Some(scroll_delta) = scroll_delta.consume() { + let event = Event::DeviceEvent { + device_id: did, + event: DeviceEvent::MouseWheel { + delta: MouseScrollDelta::LineDelta(scroll_delta.0, scroll_delta.1), + }, + }; + callback(&self.target, event); + } + } + + fn xinput2_raw_key_input( + &mut self, + xev: &XIRawEvent, + state: ElementState, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let device_id = mkdid(xev.sourceid as xinput::DeviceId); + let keycode = xev.detail as u32; + if keycode < KEYCODE_OFFSET as u32 { + return; + } + let physical_key = xkb::raw_keycode_to_physicalkey(keycode); + + callback( + &self.target, + Event::DeviceEvent { + device_id, + event: DeviceEvent::Key(RawKeyEvent { + physical_key, + state, + }), + }, + ); + } + + fn xinput2_hierarchy_changed( + &mut self, + xev: &XIHierarchyEvent, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + let infos = unsafe { slice::from_raw_parts(xev.info, xev.num_info as usize) }; + for info in infos { + if 0 != info.flags & (xinput2::XISlaveAdded | xinput2::XIMasterAdded) { + self.init_device(info.deviceid as xinput::DeviceId); + callback( + &self.target, + Event::DeviceEvent { + device_id: mkdid(info.deviceid as xinput::DeviceId), + event: DeviceEvent::Added, + }, + ); + } else if 0 + != info.flags & (xinput2::XISlaveRemoved | xinput2::XIMasterRemoved) + { + callback( + &self.target, + Event::DeviceEvent { + device_id: mkdid(info.deviceid as xinput::DeviceId), + event: DeviceEvent::Removed, + }, + ); + let mut devices = self.devices.borrow_mut(); + devices.remove(&DeviceId(info.deviceid as xinput::DeviceId)); + } + } + } + + fn xkb_event(&mut self, xev: &XkbAnyEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + match xev.xkb_type { + xlib::XkbNewKeyboardNotify => { + let xev = unsafe { + &*(xev as *const _ as *const xlib::XkbNewKeyboardNotifyEvent) + }; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let keycodes_changed_flag = 0x1; + let geometry_changed_flag = 0x1 << 1; + + let keycodes_changed = util::has_flag(xev.changed, keycodes_changed_flag); + let geometry_changed = util::has_flag(xev.changed, geometry_changed_flag); + + if xev.device == self.xkb_context.core_keyboard_id + && (keycodes_changed || geometry_changed) + { + let xcb = wt.xconn.xcb_connection().get_raw_xcb_connection(); + self.xkb_context.set_keymap_from_x11(xcb); + self.xmodmap.reload_from_x_connection(&wt.xconn); + + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + if let Some(state) = self.xkb_context.state_mut() { + let mods = state.modifiers().into(); + self.send_modifiers(window_id, mods, true, &mut callback); + } + } + } + xlib::XkbMapNotify => { + let xcb = wt.xconn.xcb_connection().get_raw_xcb_connection(); + self.xkb_context.set_keymap_from_x11(xcb); + self.xmodmap.reload_from_x_connection(&wt.xconn); + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + if let Some(state) = self.xkb_context.state_mut() { + let mods = state.modifiers().into(); + self.send_modifiers(window_id, mods, true, &mut callback); + } + } + xlib::XkbStateNotify => { + let xev = + unsafe { &*(xev as *const _ as *const xlib::XkbStateNotifyEvent) }; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if let Some(state) = self.xkb_context.state_mut() { + state.update_modifiers( + xev.base_mods, + xev.latched_mods, + xev.locked_mods, + xev.base_group as u32, + xev.latched_group as u32, + xev.locked_group as u32, + ); + + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + let mods = state.modifiers().into(); + self.send_modifiers(window_id, mods, true, &mut callback); + } + } + _ => {} + } + } + + pub fn update_mods_from_xinput2_event( + &mut self, + mods: &XIModifierState, + group: &XIModifierState, + force: bool, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + if let Some(state) = self.xkb_context.state_mut() { + state.update_modifiers( + mods.base as u32, + mods.latched as u32, + mods.locked as u32, + group.base as u32, + group.latched as u32, + group.locked as u32, + ); + + // NOTE: we use active window since generally sub windows don't have keyboard input, + // and winit assumes that unfocused window doesn't have modifiers. + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + let mods = state.modifiers(); + self.send_modifiers(window_id, mods.into(), force, &mut callback); + } + } + + fn update_mods_from_query( + &mut self, + window_id: crate::window::WindowId, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + let xkb_state = match self.xkb_context.state_mut() { + Some(xkb_state) => xkb_state, + None => return, + }; + + unsafe { + let mut state: XkbStateRec = std::mem::zeroed(); + if (wt.xconn.xlib.XkbGetState)( + wt.xconn.display, + XkbId::USE_CORE_KBD.into(), + &mut state, + ) == xlib::True + { + xkb_state.update_modifiers( + state.base_mods as u32, + state.latched_mods as u32, + state.locked_mods as u32, + state.base_group as u32, + state.latched_group as u32, + state.locked_group as u32, + ); + } + } + + let mods = xkb_state.modifiers(); + self.send_modifiers(window_id, mods.into(), true, &mut callback) + } + + pub fn update_mods_from_core_event( + &mut self, + window_id: crate::window::WindowId, + state: u16, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let xkb_mask = self.xkb_mod_mask_from_core(state); + let xkb_state = match self.xkb_context.state_mut() { + Some(xkb_state) => xkb_state, + None => return, + }; + + // NOTE: this is inspired by Qt impl. + let mut depressed = xkb_state.depressed_modifiers() & xkb_mask; + let latched = xkb_state.latched_modifiers() & xkb_mask; + let locked = xkb_state.locked_modifiers() & xkb_mask; + // Set modifiers in depressed if they don't appear in any of the final masks. + depressed |= !(depressed | latched | locked) & xkb_mask; + + xkb_state.update_modifiers( + depressed, + latched, + locked, + 0, + 0, + Self::core_keyboard_group(state), + ); + + let mods = xkb_state.modifiers(); + self.send_modifiers(window_id, mods.into(), false, &mut callback); + } + + // Bits 13 and 14 report the state keyboard group. + pub fn core_keyboard_group(state: u16) -> u32 { + ((state >> 13) & 3) as u32 + } + + pub fn xkb_mod_mask_from_core(&mut self, state: u16) -> xkb_mod_mask_t { + let mods_indices = match self.xkb_context.keymap_mut() { + Some(keymap) => keymap.mods_indices(), + None => return 0, + }; + + // Build the XKB modifiers from the regular state. + let mut depressed = 0u32; + if let Some(shift) = mods_indices + .shift + .filter(|_| ModMask::SHIFT.intersects(state)) + { + depressed |= 1 << shift; + } + if let Some(caps) = mods_indices + .caps + .filter(|_| ModMask::LOCK.intersects(state)) + { + depressed |= 1 << caps; + } + if let Some(ctrl) = mods_indices + .ctrl + .filter(|_| ModMask::CONTROL.intersects(state)) + { + depressed |= 1 << ctrl; + } + if let Some(alt) = mods_indices.alt.filter(|_| ModMask::M1.intersects(state)) { + depressed |= 1 << alt; + } + if let Some(num) = mods_indices.num.filter(|_| ModMask::M2.intersects(state)) { + depressed |= 1 << num; + } + if let Some(mod3) = mods_indices.mod3.filter(|_| ModMask::M3.intersects(state)) { + depressed |= 1 << mod3; + } + if let Some(logo) = mods_indices.logo.filter(|_| ModMask::M4.intersects(state)) { + depressed |= 1 << logo; + } + if let Some(mod5) = mods_indices.mod5.filter(|_| ModMask::M5.intersects(state)) { + depressed |= 1 << mod5; + } + + depressed + } + + /// Send modifiers for the active window. + /// + /// The event won't be sent when the `modifiers` match the previously `sent` modifiers value, + /// unless `force` is passed. The `force` should be passed when the active window changes. + fn send_modifiers)>( + &self, + window_id: crate::window::WindowId, + modifiers: ModifiersState, + force: bool, + callback: &mut F, + ) { + // NOTE: Always update the modifiers to account for case when they've changed + // and forced was `true`. + if self.modifiers.replace(modifiers) != modifiers || force { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::ModifiersChanged(self.modifiers.get().into()), + }; + callback(&self.target, event); + } + } + + fn handle_pressed_keys( + target: &RootAEL, + window_id: crate::window::WindowId, + state: ElementState, + xkb_context: &mut Context, + callback: &mut F, + ) where + F: FnMut(&RootAEL, Event), + { + let device_id = mkdid(util::VIRTUAL_CORE_KEYBOARD); + + // Update modifiers state and emit key events based on which keys are currently pressed. + let window_target = Self::window_target(target); + let xcb = window_target + .xconn + .xcb_connection() + .get_raw_xcb_connection(); + + let keymap = match xkb_context.keymap_mut() { + Some(keymap) => keymap, + None => return, + }; + + // Send the keys using the synthetic state to not alter the main state. + let mut xkb_state = match XkbState::new_x11(xcb, keymap) { + Some(xkb_state) => xkb_state, + None => return, + }; + let mut key_processor = match xkb_context.key_context_with_state(&mut xkb_state) { + Some(key_processor) => key_processor, + None => return, + }; + + for keycode in window_target + .xconn + .query_keymap() + .into_iter() + .filter(|k| *k >= KEYCODE_OFFSET) + { + let event = key_processor.process_key_event(keycode as u32, state, false); + let event = Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { + device_id, + event, + is_synthetic: true, + }, + }; + callback(target, event); + } + } + + fn process_dpi_change(&self, callback: &mut F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + wt.xconn + .reload_database() + .expect("failed to reload Xft database"); + + // In the future, it would be quite easy to emit monitor hotplug events. + let prev_list = { + let prev_list = wt.xconn.invalidate_cached_monitor_list(); + match prev_list { + Some(prev_list) => prev_list, + None => return, + } + }; + + let new_list = wt + .xconn + .available_monitors() + .expect("Failed to get monitor list"); + for new_monitor in new_list { + // Previous list may be empty, in case of disconnecting and + // reconnecting the only one monitor. We still need to emit events in + // this case. + let maybe_prev_scale_factor = prev_list + .iter() + .find(|prev_monitor| prev_monitor.name == new_monitor.name) + .map(|prev_monitor| prev_monitor.scale_factor); + if Some(new_monitor.scale_factor) != maybe_prev_scale_factor { + for window in wt.windows.borrow().iter().filter_map(|(_, w)| w.upgrade()) + { + window.refresh_dpi_for_monitor( + &new_monitor, + maybe_prev_scale_factor, + |event| { + callback(&self.target, event); + }, + ) + } + } + } + } + + fn window_exists(&self, window_id: xproto::Window) -> bool { + self.with_window(window_id, |_| ()).is_some() + } +} + +fn is_first_touch( + first: &mut Option, + num: &mut u32, + id: u64, + phase: TouchPhase, +) -> bool { + match phase { + TouchPhase::Started => { + if *num == 0 { + *first = Some(id); + } + *num += 1; + } + TouchPhase::Cancelled | TouchPhase::Ended => { + if *first == Some(id) { + *first = None; + } + *num = num.saturating_sub(1); + } + _ => (), + } + + *first == Some(id) +} diff --git a/rio-window/src/platform_impl/linux/x11/ffi.rs b/rio-window/src/platform_impl/linux/x11/ffi.rs new file mode 100644 index 00000000..6895f192 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/ffi.rs @@ -0,0 +1,4 @@ +pub use x11_dl::error::OpenError; +pub use x11_dl::xinput2::*; +pub use x11_dl::xlib::*; +pub use x11_dl::xlib_xcb::*; diff --git a/rio-window/src/platform_impl/linux/x11/ime/callbacks.rs b/rio-window/src/platform_impl/linux/x11/ime/callbacks.rs new file mode 100644 index 00000000..30b41e5f --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/ime/callbacks.rs @@ -0,0 +1,216 @@ +use std::collections::HashMap; +use std::os::raw::c_char; +use std::ptr; +use std::sync::Arc; + +use super::{ffi, XConnection, XError}; + +use super::context::{ImeContext, ImeContextCreationError}; +use super::inner::{close_im, ImeInner}; +use super::input_method::PotentialInputMethods; + +pub(crate) unsafe fn xim_set_callback( + xconn: &Arc, + xim: ffi::XIM, + field: *const c_char, + callback: *mut ffi::XIMCallback, +) -> Result<(), XError> { + // It's advisable to wrap variadic FFI functions in our own functions, as we want to minimize + // access that isn't type-checked. + unsafe { (xconn.xlib.XSetIMValues)(xim, field, callback, ptr::null_mut::<()>()) }; + xconn.check_errors() +} + +// Set a callback for when an input method matching the current locale modifiers becomes +// available. Note that this has nothing to do with what input methods are open or able to be +// opened, and simply uses the modifiers that are set when the callback is set. +// * This is called per locale modifier, not per input method opened with that locale modifier. +// * Trying to set this for multiple locale modifiers causes problems, i.e. one of the rebuilt input +// contexts would always silently fail to use the input method. +pub(crate) unsafe fn set_instantiate_callback( + xconn: &Arc, + client_data: ffi::XPointer, +) -> Result<(), XError> { + unsafe { + (xconn.xlib.XRegisterIMInstantiateCallback)( + xconn.display, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + Some(xim_instantiate_callback), + client_data, + ) + }; + xconn.check_errors() +} + +pub(crate) unsafe fn unset_instantiate_callback( + xconn: &Arc, + client_data: ffi::XPointer, +) -> Result<(), XError> { + unsafe { + (xconn.xlib.XUnregisterIMInstantiateCallback)( + xconn.display, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + Some(xim_instantiate_callback), + client_data, + ) + }; + xconn.check_errors() +} + +pub(crate) unsafe fn set_destroy_callback( + xconn: &Arc, + im: ffi::XIM, + inner: &ImeInner, +) -> Result<(), XError> { + unsafe { + xim_set_callback( + xconn, + im, + ffi::XNDestroyCallback_0.as_ptr() as *const _, + &inner.destroy_callback as *const _ as *mut _, + ) + } +} + +#[derive(Debug)] +#[allow(clippy::enum_variant_names)] +enum ReplaceImError { + // Boxed to prevent large error type + MethodOpenFailed(#[allow(dead_code)] Box), + ContextCreationFailed(#[allow(dead_code)] ImeContextCreationError), + SetDestroyCallbackFailed(#[allow(dead_code)] XError), +} + +// Attempt to replace current IM (which may or may not be presently valid) with a new one. This +// includes replacing all existing input contexts and free'ing resources as necessary. This only +// modifies existing state if all operations succeed. +unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> { + let xconn = unsafe { &(*inner).xconn }; + + let (new_im, is_fallback) = { + let new_im = unsafe { (*inner).potential_input_methods.open_im(xconn, None) }; + let is_fallback = new_im.is_fallback(); + ( + new_im.ok().ok_or_else(|| { + ReplaceImError::MethodOpenFailed(Box::new(unsafe { + (*inner).potential_input_methods.clone() + })) + })?, + is_fallback, + ) + }; + + // It's important to always set a destroy callback, since there's otherwise potential for us + // to try to use or free a resource that's already been destroyed on the server. + { + let result = unsafe { set_destroy_callback(xconn, new_im.im, &*inner) }; + if result.is_err() { + let _ = unsafe { close_im(xconn, new_im.im) }; + } + result + } + .map_err(ReplaceImError::SetDestroyCallbackFailed)?; + + let mut new_contexts = HashMap::new(); + for (window, old_context) in unsafe { (*inner).contexts.iter() } { + let spot = old_context.as_ref().map(|old_context| old_context.ic_spot); + + // Check if the IME was allowed on that context. + let is_allowed = old_context + .as_ref() + .map(|old_context| old_context.is_allowed()) + .unwrap_or_default(); + + // We can't use the style from the old context here, since it may change on reload, so + // pick style from the new XIM based on the old state. + let style = if is_allowed { + new_im.preedit_style + } else { + new_im.none_style + }; + + let new_context = { + let result = unsafe { + ImeContext::new( + xconn, + new_im.im, + style, + *window, + spot, + (*inner).event_sender.clone(), + ) + }; + if result.is_err() { + let _ = unsafe { close_im(xconn, new_im.im) }; + } + result.map_err(ReplaceImError::ContextCreationFailed)? + }; + new_contexts.insert(*window, Some(new_context)); + } + + // If we've made it this far, everything succeeded. + unsafe { + let _ = (*inner).destroy_all_contexts_if_necessary(); + let _ = (*inner).close_im_if_necessary(); + (*inner).im = Some(new_im); + (*inner).contexts = new_contexts; + (*inner).is_destroyed = false; + (*inner).is_fallback = is_fallback; + } + Ok(()) +} + +pub unsafe extern "C" fn xim_instantiate_callback( + _display: *mut ffi::Display, + client_data: ffi::XPointer, + // This field is unsupplied. + _call_data: ffi::XPointer, +) { + let inner: *mut ImeInner = client_data as _; + if !inner.is_null() { + let xconn = unsafe { &(*inner).xconn }; + match unsafe { replace_im(inner) } { + Ok(()) => unsafe { + let _ = unset_instantiate_callback(xconn, client_data); + (*inner).is_fallback = false; + }, + Err(err) => unsafe { + if (*inner).is_destroyed { + // We have no usable input methods! + panic!("Failed to reopen input method: {err:?}"); + } + }, + } + } +} + +// This callback is triggered when the input method is closed on the server end. When this +// happens, XCloseIM/XDestroyIC doesn't need to be called, as the resources have already been +// free'd (attempting to do so causes our connection to freeze). +pub unsafe extern "C" fn xim_destroy_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + // This field is unsupplied. + _call_data: ffi::XPointer, +) { + let inner: *mut ImeInner = client_data as _; + if !inner.is_null() { + unsafe { (*inner).is_destroyed = true }; + let xconn = unsafe { &(*inner).xconn }; + if unsafe { !(*inner).is_fallback } { + let _ = unsafe { set_instantiate_callback(xconn, client_data) }; + // Attempt to open fallback input method. + match unsafe { replace_im(inner) } { + Ok(()) => unsafe { (*inner).is_fallback = true }, + Err(err) => { + // We have no usable input methods! + panic!("Failed to open fallback input method: {err:?}"); + } + } + } + } +} diff --git a/rio-window/src/platform_impl/linux/x11/ime/context.rs b/rio-window/src/platform_impl/linux/x11/ime/context.rs new file mode 100644 index 00000000..299d4fd0 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/ime/context.rs @@ -0,0 +1,394 @@ +use std::ffi::CStr; +use std::os::raw::c_short; +use std::sync::Arc; +use std::{mem, ptr}; + +use x11_dl::xlib::{ + XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct, +}; + +use crate::platform_impl::platform::x11::ime::input_method::{Style, XIMStyle}; +use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender}; + +use super::{ffi, util, XConnection, XError}; + +/// IME creation error. +#[derive(Debug)] +pub enum ImeContextCreationError { + /// Got the error from Xlib. + XError(XError), + + /// Got null pointer from Xlib but without exact reason. + Null, +} + +/// The callback used by XIM preedit functions. +type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer); + +/// Wrapper for creating XIM callbacks. +#[inline] +fn create_xim_callback( + client_data: ffi::XPointer, + callback: XIMProcNonnull, +) -> ffi::XIMCallback { + XIMCallback { + client_data, + callback: Some(callback), + } +} + +/// The server started preedit. +extern "C" fn preedit_start_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + _call_data: ffi::XPointer, +) -> i32 { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + + client_data.text.clear(); + client_data.cursor_pos = 0; + client_data + .event_sender + .send((client_data.window, ImeEvent::Start)) + .expect("failed to send preedit start event"); + -1 +} + +/// Done callback is used when the preedit should be hidden. +extern "C" fn preedit_done_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + _call_data: ffi::XPointer, +) { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + + // Drop text buffer and reset cursor position on done. + client_data.text = Vec::new(); + client_data.cursor_pos = 0; + + client_data + .event_sender + .send((client_data.window, ImeEvent::End)) + .expect("failed to send preedit end event"); +} + +fn calc_byte_position(text: &[char], pos: usize) -> usize { + text.iter() + .take(pos) + .fold(0, |byte_pos, text| byte_pos + text.len_utf8()) +} + +/// Preedit text information to be drawn inline by the client. +extern "C" fn preedit_draw_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + call_data: ffi::XPointer, +) { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + let call_data = unsafe { &mut *(call_data as *mut XIMPreeditDrawCallbackStruct) }; + client_data.cursor_pos = call_data.caret as usize; + + let chg_range = call_data.chg_first as usize + ..(call_data.chg_first + call_data.chg_length) as usize; + if chg_range.start > client_data.text.len() || chg_range.end > client_data.text.len() + { + tracing::warn!( + "invalid chg range: buffer length={}, but chg_first={} chg_lengthg={}", + client_data.text.len(), + call_data.chg_first, + call_data.chg_length + ); + return; + } + + // NULL indicate text deletion + let mut new_chars = if call_data.text.is_null() { + Vec::new() + } else { + let xim_text = unsafe { &mut *(call_data.text) }; + if xim_text.encoding_is_wchar > 0 { + return; + } + + let new_text = unsafe { xim_text.string.multi_byte }; + + if new_text.is_null() { + return; + } + + let new_text = unsafe { CStr::from_ptr(new_text) }; + + String::from(new_text.to_str().expect("Invalid UTF-8 String from IME")) + .chars() + .collect() + }; + let mut old_text_tail = client_data.text.split_off(chg_range.end); + client_data.text.truncate(chg_range.start); + client_data.text.append(&mut new_chars); + client_data.text.append(&mut old_text_tail); + let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos); + + client_data + .event_sender + .send(( + client_data.window, + ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos), + )) + .expect("failed to send preedit update event"); +} + +/// Handling of cursor movements in preedit text. +extern "C" fn preedit_caret_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + call_data: ffi::XPointer, +) { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + let call_data = unsafe { &mut *(call_data as *mut XIMPreeditCaretCallbackStruct) }; + + if call_data.direction == ffi::XIMCaretDirection::XIMAbsolutePosition { + client_data.cursor_pos = call_data.position as usize; + let cursor_byte_pos = + calc_byte_position(&client_data.text, client_data.cursor_pos); + + client_data + .event_sender + .send(( + client_data.window, + ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos), + )) + .expect("failed to send preedit update event"); + } +} + +/// Struct to simplify callback creation and latter passing into Xlib XIM. +struct PreeditCallbacks { + start_callback: ffi::XIMCallback, + done_callback: ffi::XIMCallback, + draw_callback: ffi::XIMCallback, + caret_callback: ffi::XIMCallback, +} + +impl PreeditCallbacks { + pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks { + let start_callback = create_xim_callback(client_data, unsafe { + mem::transmute(preedit_start_callback as usize) + }); + let done_callback = create_xim_callback(client_data, preedit_done_callback); + let caret_callback = create_xim_callback(client_data, preedit_caret_callback); + let draw_callback = create_xim_callback(client_data, preedit_draw_callback); + + PreeditCallbacks { + start_callback, + done_callback, + caret_callback, + draw_callback, + } + } +} + +struct ImeContextClientData { + window: ffi::Window, + event_sender: ImeEventSender, + text: Vec, + cursor_pos: usize, +} + +// XXX: this struct doesn't destroy its XIC resource when dropped. +// This is intentional, as it doesn't have enough information to know whether or not the context +// still exists on the server. Since `ImeInner` has that awareness, destruction must be handled +// through `ImeInner`. +pub struct ImeContext { + pub(crate) ic: ffi::XIC, + pub(crate) ic_spot: ffi::XPoint, + pub(crate) style: Style, + // Since the data is passed shared between X11 XIM callbacks, but couldn't be directly free + // from there we keep the pointer to automatically deallocate it. + _client_data: Box, +} + +impl ImeContext { + pub(crate) unsafe fn new( + xconn: &Arc, + im: ffi::XIM, + style: Style, + window: ffi::Window, + ic_spot: Option, + event_sender: ImeEventSender, + ) -> Result { + let client_data = Box::into_raw(Box::new(ImeContextClientData { + window, + event_sender, + text: Vec::new(), + cursor_pos: 0, + })); + + let ic = match style as _ { + Style::Preedit(style) => unsafe { + ImeContext::create_preedit_ic( + xconn, + im, + style, + window, + client_data as ffi::XPointer, + ) + }, + Style::Nothing(style) => unsafe { + ImeContext::create_nothing_ic(xconn, im, style, window) + }, + Style::None(style) => unsafe { + ImeContext::create_none_ic(xconn, im, style, window) + }, + } + .ok_or(ImeContextCreationError::Null)?; + + xconn + .check_errors() + .map_err(ImeContextCreationError::XError)?; + + let mut context = ImeContext { + ic, + ic_spot: ffi::XPoint { x: 0, y: 0 }, + style, + _client_data: unsafe { Box::from_raw(client_data) }, + }; + + // Set the spot location, if it's present. + if let Some(ic_spot) = ic_spot { + context.set_spot(xconn, ic_spot.x, ic_spot.y) + } + + Ok(context) + } + + unsafe fn create_none_ic( + xconn: &Arc, + im: ffi::XIM, + style: XIMStyle, + window: ffi::Window, + ) -> Option { + let ic = unsafe { + (xconn.xlib.XCreateIC)( + im, + ffi::XNInputStyle_0.as_ptr() as *const _, + style, + ffi::XNClientWindow_0.as_ptr() as *const _, + window, + ptr::null_mut::<()>(), + ) + }; + + (!ic.is_null()).then_some(ic) + } + + unsafe fn create_preedit_ic( + xconn: &Arc, + im: ffi::XIM, + style: XIMStyle, + window: ffi::Window, + client_data: ffi::XPointer, + ) -> Option { + let preedit_callbacks = PreeditCallbacks::new(client_data); + let preedit_attr = util::memory::XSmartPointer::new(xconn, unsafe { + (xconn.xlib.XVaCreateNestedList)( + 0, + ffi::XNPreeditStartCallback_0.as_ptr() as *const _, + &(preedit_callbacks.start_callback) as *const _, + ffi::XNPreeditDoneCallback_0.as_ptr() as *const _, + &(preedit_callbacks.done_callback) as *const _, + ffi::XNPreeditCaretCallback_0.as_ptr() as *const _, + &(preedit_callbacks.caret_callback) as *const _, + ffi::XNPreeditDrawCallback_0.as_ptr() as *const _, + &(preedit_callbacks.draw_callback) as *const _, + ptr::null_mut::<()>(), + ) + }) + .expect("XVaCreateNestedList returned NULL"); + + let ic = unsafe { + (xconn.xlib.XCreateIC)( + im, + ffi::XNInputStyle_0.as_ptr() as *const _, + style, + ffi::XNClientWindow_0.as_ptr() as *const _, + window, + ffi::XNPreeditAttributes_0.as_ptr() as *const _, + preedit_attr.ptr, + ptr::null_mut::<()>(), + ) + }; + + (!ic.is_null()).then_some(ic) + } + + unsafe fn create_nothing_ic( + xconn: &Arc, + im: ffi::XIM, + style: XIMStyle, + window: ffi::Window, + ) -> Option { + let ic = unsafe { + (xconn.xlib.XCreateIC)( + im, + ffi::XNInputStyle_0.as_ptr() as *const _, + style, + ffi::XNClientWindow_0.as_ptr() as *const _, + window, + ptr::null_mut::<()>(), + ) + }; + + (!ic.is_null()).then_some(ic) + } + + pub(crate) fn focus(&self, xconn: &Arc) -> Result<(), XError> { + unsafe { + (xconn.xlib.XSetICFocus)(self.ic); + } + xconn.check_errors() + } + + pub(crate) fn unfocus(&self, xconn: &Arc) -> Result<(), XError> { + unsafe { + (xconn.xlib.XUnsetICFocus)(self.ic); + } + xconn.check_errors() + } + + pub fn is_allowed(&self) -> bool { + !matches!(self.style, Style::None(_)) + } + + // Set the spot for preedit text. Setting spot isn't working with libX11 when preedit callbacks + // are being used. Certain IMEs do show selection window, but it's placed in bottom left of the + // window and couldn't be changed. + // + // For me see: https://bugs.freedesktop.org/show_bug.cgi?id=1580. + pub(crate) fn set_spot(&mut self, xconn: &Arc, x: c_short, y: c_short) { + if !self.is_allowed() || self.ic_spot.x == x && self.ic_spot.y == y { + return; + } + + self.ic_spot = ffi::XPoint { x, y }; + + unsafe { + let preedit_attr = util::memory::XSmartPointer::new( + xconn, + (xconn.xlib.XVaCreateNestedList)( + 0, + ffi::XNSpotLocation_0.as_ptr(), + &self.ic_spot, + ptr::null_mut::<()>(), + ), + ) + .expect("XVaCreateNestedList returned NULL"); + + (xconn.xlib.XSetICValues)( + self.ic, + ffi::XNPreeditAttributes_0.as_ptr() as *const _, + preedit_attr.ptr, + ptr::null_mut::<()>(), + ); + } + } +} diff --git a/rio-window/src/platform_impl/linux/x11/ime/inner.rs b/rio-window/src/platform_impl/linux/x11/ime/inner.rs new file mode 100644 index 00000000..a66af5ea --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/ime/inner.rs @@ -0,0 +1,81 @@ +use std::collections::HashMap; +use std::mem; +use std::sync::Arc; + +use super::{ffi, XConnection, XError}; + +use super::context::ImeContext; +use super::input_method::{InputMethod, PotentialInputMethods}; +use crate::platform_impl::platform::x11::ime::ImeEventSender; + +pub(crate) unsafe fn close_im( + xconn: &Arc, + im: ffi::XIM, +) -> Result<(), XError> { + unsafe { (xconn.xlib.XCloseIM)(im) }; + xconn.check_errors() +} + +pub(crate) unsafe fn destroy_ic( + xconn: &Arc, + ic: ffi::XIC, +) -> Result<(), XError> { + unsafe { (xconn.xlib.XDestroyIC)(ic) }; + xconn.check_errors() +} + +pub(crate) struct ImeInner { + pub xconn: Arc, + pub im: Option, + pub potential_input_methods: PotentialInputMethods, + pub contexts: HashMap>, + // WARNING: this is initially zeroed! + pub destroy_callback: ffi::XIMCallback, + pub event_sender: ImeEventSender, + // Indicates whether or not the input method was destroyed on the server end + // (i.e. if ibus/fcitx/etc. was terminated/restarted) + pub is_destroyed: bool, + pub is_fallback: bool, +} + +impl ImeInner { + pub(crate) fn new( + xconn: Arc, + potential_input_methods: PotentialInputMethods, + event_sender: ImeEventSender, + ) -> Self { + ImeInner { + xconn, + im: None, + potential_input_methods, + contexts: HashMap::new(), + destroy_callback: unsafe { mem::zeroed() }, + event_sender, + is_destroyed: false, + is_fallback: false, + } + } + + pub unsafe fn close_im_if_necessary(&self) -> Result { + if !self.is_destroyed && self.im.is_some() { + unsafe { close_im(&self.xconn, self.im.as_ref().unwrap().im) }.map(|_| true) + } else { + Ok(false) + } + } + + pub unsafe fn destroy_ic_if_necessary(&self, ic: ffi::XIC) -> Result { + if !self.is_destroyed { + unsafe { destroy_ic(&self.xconn, ic) }.map(|_| true) + } else { + Ok(false) + } + } + + pub unsafe fn destroy_all_contexts_if_necessary(&self) -> Result { + for context in self.contexts.values().flatten() { + unsafe { self.destroy_ic_if_necessary(context.ic)? }; + } + Ok(!self.is_destroyed) + } +} diff --git a/rio-window/src/platform_impl/linux/x11/ime/input_method.rs b/rio-window/src/platform_impl/linux/x11/ime/input_method.rs new file mode 100644 index 00000000..93044ac1 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/ime/input_method.rs @@ -0,0 +1,373 @@ +use std::ffi::{CStr, CString, IntoStringError}; +use std::os::raw::{c_char, c_ulong, c_ushort}; +use std::sync::{Arc, Mutex}; +use std::{env, fmt, ptr}; + +use super::super::atoms::*; +use super::{ffi, util, XConnection, XError}; +use x11rb::protocol::xproto; + +static GLOBAL_LOCK: Mutex<()> = Mutex::new(()); + +unsafe fn open_im(xconn: &Arc, locale_modifiers: &CStr) -> Option { + let _lock = GLOBAL_LOCK.lock(); + + // XSetLocaleModifiers returns... + // * The current locale modifiers if it's given a NULL pointer. + // * The new locale modifiers if we succeeded in setting them. + // * NULL if the locale modifiers string is malformed or if the current locale is not supported + // by Xlib. + unsafe { (xconn.xlib.XSetLocaleModifiers)(locale_modifiers.as_ptr()) }; + + let im = unsafe { + (xconn.xlib.XOpenIM)( + xconn.display, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + + if im.is_null() { + None + } else { + Some(im) + } +} + +#[derive(Debug)] +pub struct InputMethod { + pub im: ffi::XIM, + pub preedit_style: Style, + pub none_style: Style, + _name: String, +} + +impl InputMethod { + fn new(xconn: &Arc, im: ffi::XIM, name: String) -> Option { + let mut styles: *mut XIMStyles = std::ptr::null_mut(); + + // Query the styles supported by the XIM. + unsafe { + if !(xconn.xlib.XGetIMValues)( + im, + ffi::XNQueryInputStyle_0.as_ptr() as *const _, + (&mut styles) as *mut _, + std::ptr::null_mut::<()>(), + ) + .is_null() + { + return None; + } + } + + let mut preedit_style = None; + let mut none_style = None; + + unsafe { + std::slice::from_raw_parts( + (*styles).supported_styles, + (*styles).count_styles as _, + ) + .iter() + .for_each(|style| match *style { + XIM_PREEDIT_STYLE => { + preedit_style = Some(Style::Preedit(*style)); + } + XIM_NOTHING_STYLE if preedit_style.is_none() => { + preedit_style = Some(Style::Nothing(*style)) + } + XIM_NONE_STYLE => none_style = Some(Style::None(*style)), + _ => (), + }); + + (xconn.xlib.XFree)(styles.cast()); + }; + + if preedit_style.is_none() && none_style.is_none() { + return None; + } + + let preedit_style = preedit_style.unwrap_or_else(|| none_style.unwrap()); + let none_style = none_style.unwrap_or(preedit_style); + + Some(InputMethod { + im, + _name: name, + preedit_style, + none_style, + }) + } +} + +const XIM_PREEDIT_STYLE: XIMStyle = + (ffi::XIMPreeditCallbacks | ffi::XIMStatusNothing) as XIMStyle; +const XIM_NOTHING_STYLE: XIMStyle = + (ffi::XIMPreeditNothing | ffi::XIMStatusNothing) as XIMStyle; +const XIM_NONE_STYLE: XIMStyle = (ffi::XIMPreeditNone | ffi::XIMStatusNone) as XIMStyle; + +/// Style of the IME context. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Style { + /// Preedit callbacks. + Preedit(XIMStyle), + + /// Nothing. + Nothing(XIMStyle), + + /// No IME. + None(XIMStyle), +} + +impl Default for Style { + fn default() -> Self { + Style::None(XIM_NONE_STYLE) + } +} + +#[repr(C)] +#[derive(Debug)] +struct XIMStyles { + count_styles: c_ushort, + supported_styles: *const XIMStyle, +} + +pub(crate) type XIMStyle = c_ulong; + +#[derive(Debug)] +pub enum InputMethodResult { + /// Input method used locale modifier from `XMODIFIERS` environment variable. + XModifiers(InputMethod), + /// Input method used internal fallback locale modifier. + Fallback(InputMethod), + /// Input method could not be opened using any locale modifier tried. + Failure, +} + +impl InputMethodResult { + pub fn is_fallback(&self) -> bool { + matches!(self, InputMethodResult::Fallback(_)) + } + + pub fn ok(self) -> Option { + use self::InputMethodResult::*; + match self { + XModifiers(im) | Fallback(im) => Some(im), + Failure => None, + } + } +} + +#[derive(Debug, Clone)] +enum GetXimServersError { + XError(#[allow(dead_code)] XError), + GetPropertyError(#[allow(dead_code)] util::GetPropertyError), + InvalidUtf8(#[allow(dead_code)] IntoStringError), +} + +impl From for GetXimServersError { + fn from(error: util::GetPropertyError) -> Self { + GetXimServersError::GetPropertyError(error) + } +} + +// The root window has a property named XIM_SERVERS, which contains a list of atoms representing +// the available XIM servers. For instance, if you're using ibus, it would contain an atom named +// "@server=ibus". It's possible for this property to contain multiple atoms, though presumably +// rare. Note that we replace "@server=" with "@im=" in order to match the format of locale +// modifiers, since we don't want a user who's looking at logs to ask "am I supposed to set +// XMODIFIERS to `@server=ibus`?!?" +unsafe fn get_xim_servers( + xconn: &Arc, +) -> Result, GetXimServersError> { + let atoms = xconn.atoms(); + let servers_atom = atoms[XIM_SERVERS]; + + let root = unsafe { (xconn.xlib.XDefaultRootWindow)(xconn.display) }; + + let mut atoms: Vec = xconn + .get_property::( + root as xproto::Window, + servers_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + ) + .map_err(GetXimServersError::GetPropertyError)? + .into_iter() + .map(ffi::Atom::from) + .collect::>(); + + let mut names: Vec<*const c_char> = Vec::with_capacity(atoms.len()); + unsafe { + (xconn.xlib.XGetAtomNames)( + xconn.display, + atoms.as_mut_ptr(), + atoms.len() as _, + names.as_mut_ptr() as _, + ) + }; + unsafe { names.set_len(atoms.len()) }; + + let mut formatted_names = Vec::with_capacity(names.len()); + for name in names { + let string = unsafe { CStr::from_ptr(name) } + .to_owned() + .into_string() + .map_err(GetXimServersError::InvalidUtf8)?; + unsafe { (xconn.xlib.XFree)(name as _) }; + formatted_names.push(string.replace("@server=", "@im=")); + } + xconn.check_errors().map_err(GetXimServersError::XError)?; + Ok(formatted_names) +} + +#[derive(Clone)] +struct InputMethodName { + c_string: CString, + string: String, +} + +impl InputMethodName { + pub fn from_string(string: String) -> Self { + let c_string = CString::new(string.clone()) + .expect("String used to construct CString contained null byte"); + InputMethodName { c_string, string } + } + + pub fn from_str(string: &str) -> Self { + let c_string = CString::new(string) + .expect("String used to construct CString contained null byte"); + InputMethodName { + c_string, + string: string.to_owned(), + } + } +} + +impl fmt::Debug for InputMethodName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.string.fmt(f) + } +} + +#[derive(Debug, Clone)] +struct PotentialInputMethod { + name: InputMethodName, + successful: Option, +} + +impl PotentialInputMethod { + pub fn from_string(string: String) -> Self { + PotentialInputMethod { + name: InputMethodName::from_string(string), + successful: None, + } + } + + pub fn from_str(string: &str) -> Self { + PotentialInputMethod { + name: InputMethodName::from_str(string), + successful: None, + } + } + + pub fn reset(&mut self) { + self.successful = None; + } + + pub fn open_im(&mut self, xconn: &Arc) -> Option { + let im = unsafe { open_im(xconn, &self.name.c_string) }; + self.successful = Some(im.is_some()); + im.and_then(|im| InputMethod::new(xconn, im, self.name.string.clone())) + } +} + +// By logging this struct, you get a sequential listing of every locale modifier tried, where it +// came from, and if it succeeded. +#[derive(Debug, Clone)] +pub(crate) struct PotentialInputMethods { + // On correctly configured systems, the XMODIFIERS environment variable tells us everything we + // need to know. + xmodifiers: Option, + // We have some standard options at our disposal that should ostensibly always work. For users + // who only need compose sequences, this ensures that the program launches without a hitch + // For users who need more sophisticated IME features, this is more or less a silent failure. + // Logging features should be added in the future to allow both audiences to be effectively + // served. + fallbacks: [PotentialInputMethod; 2], + // For diagnostic purposes, we include the list of XIM servers that the server reports as + // being available. + _xim_servers: Result, GetXimServersError>, +} + +impl PotentialInputMethods { + pub fn new(xconn: &Arc) -> Self { + let xmodifiers = env::var("XMODIFIERS") + .ok() + .map(PotentialInputMethod::from_string); + PotentialInputMethods { + // Since passing "" to XSetLocaleModifiers results in it defaulting to the value of + // XMODIFIERS, it's worth noting what happens if XMODIFIERS is also "". If simply + // running the program with `XMODIFIERS="" cargo run`, then assuming XMODIFIERS is + // defined in the profile (or parent environment) then that parent XMODIFIERS is used. + // If that XMODIFIERS value is also "" (i.e. if you ran `export XMODIFIERS=""`), then + // XSetLocaleModifiers uses the default local input method. Note that defining + // XMODIFIERS as "" is different from XMODIFIERS not being defined at all, since in + // that case, we get `None` and end up skipping ahead to the next method. + xmodifiers, + fallbacks: [ + // This is a standard input method that supports compose sequences, which should + // always be available. `@im=none` appears to mean the same thing. + PotentialInputMethod::from_str("@im=local"), + // This explicitly specifies to use the implementation-dependent default, though + // that seems to be equivalent to just using the local input method. + PotentialInputMethod::from_str("@im="), + ], + // The XIM_SERVERS property can have surprising values. For instance, when I exited + // ibus to run fcitx, it retained the value denoting ibus. Even more surprising is + // that the fcitx input method could only be successfully opened using "@im=ibus". + // Presumably due to this quirk, it's actually possible to alternate between ibus and + // fcitx in a running application. + _xim_servers: unsafe { get_xim_servers(xconn) }, + } + } + + // This resets the `successful` field of every potential input method, ensuring we have + // accurate information when this struct is re-used by the destruction/instantiation callbacks. + fn reset(&mut self) { + if let Some(ref mut input_method) = self.xmodifiers { + input_method.reset(); + } + + for input_method in &mut self.fallbacks { + input_method.reset(); + } + } + + pub fn open_im( + &mut self, + xconn: &Arc, + callback: Option<&dyn Fn()>, + ) -> InputMethodResult { + use self::InputMethodResult::*; + + self.reset(); + + if let Some(ref mut input_method) = self.xmodifiers { + let im = input_method.open_im(xconn); + if let Some(im) = im { + return XModifiers(im); + } else if let Some(ref callback) = callback { + callback(); + } + } + + for input_method in &mut self.fallbacks { + let im = input_method.open_im(xconn); + if let Some(im) = im { + return Fallback(im); + } + } + + Failure + } +} diff --git a/rio-window/src/platform_impl/linux/x11/ime/mod.rs b/rio-window/src/platform_impl/linux/x11/ime/mod.rs new file mode 100644 index 00000000..457e0154 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/ime/mod.rs @@ -0,0 +1,247 @@ +// Important: all XIM calls need to happen from the same thread! + +mod callbacks; +mod context; +mod inner; +mod input_method; + +use std::sync::mpsc::{Receiver, Sender}; +use std::sync::Arc; + +use tracing::debug; + +use super::{ffi, util, XConnection, XError}; + +use self::callbacks::*; +use self::context::ImeContext; +pub use self::context::ImeContextCreationError; +use self::inner::{close_im, ImeInner}; +use self::input_method::{PotentialInputMethods, Style}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ImeEvent { + Enabled, + Start, + Update(String, usize), + End, + Disabled, +} + +pub type ImeReceiver = Receiver; +pub type ImeSender = Sender; +pub type ImeEventReceiver = Receiver<(ffi::Window, ImeEvent)>; +pub type ImeEventSender = Sender<(ffi::Window, ImeEvent)>; + +/// Request to control XIM handler from the window. +pub enum ImeRequest { + /// Set IME spot position for given `window_id`. + Position(ffi::Window, i16, i16), + + /// Allow IME input for the given `window_id`. + Allow(ffi::Window, bool), +} + +#[derive(Debug)] +pub(crate) enum ImeCreationError { + // Boxed to prevent large error type + OpenFailure(Box), + SetDestroyCallbackFailed(#[allow(dead_code)] XError), +} + +pub(crate) struct Ime { + xconn: Arc, + // The actual meat of this struct is boxed away, since it needs to have a fixed location in + // memory so we can pass a pointer to it around. + inner: Box, +} + +impl Ime { + pub fn new( + xconn: Arc, + event_sender: ImeEventSender, + ) -> Result { + let potential_input_methods = PotentialInputMethods::new(&xconn); + + let (mut inner, client_data) = { + let mut inner = + Box::new(ImeInner::new(xconn, potential_input_methods, event_sender)); + let inner_ptr = Box::into_raw(inner); + let client_data = inner_ptr as _; + let destroy_callback = ffi::XIMCallback { + client_data, + callback: Some(xim_destroy_callback), + }; + inner = unsafe { Box::from_raw(inner_ptr) }; + inner.destroy_callback = destroy_callback; + (inner, client_data) + }; + + let xconn = Arc::clone(&inner.xconn); + + let input_method = inner.potential_input_methods.open_im( + &xconn, + Some(&|| { + let _ = unsafe { set_instantiate_callback(&xconn, client_data) }; + }), + ); + + let is_fallback = input_method.is_fallback(); + if let Some(input_method) = input_method.ok() { + inner.is_fallback = is_fallback; + unsafe { + let result = set_destroy_callback(&xconn, input_method.im, &inner) + .map_err(ImeCreationError::SetDestroyCallbackFailed); + if result.is_err() { + let _ = close_im(&xconn, input_method.im); + } + result?; + } + inner.im = Some(input_method); + Ok(Ime { xconn, inner }) + } else { + Err(ImeCreationError::OpenFailure(Box::new( + inner.potential_input_methods, + ))) + } + } + + pub fn is_destroyed(&self) -> bool { + self.inner.is_destroyed + } + + // This pattern is used for various methods here: + // Ok(_) indicates that nothing went wrong internally + // Ok(true) indicates that the action was actually performed + // Ok(false) indicates that the action is not presently applicable + pub fn create_context( + &mut self, + window: ffi::Window, + with_preedit: bool, + ) -> Result { + let context = if self.is_destroyed() { + // Create empty entry in map, so that when IME is rebuilt, this window has a context. + None + } else { + let im = self.inner.im.as_ref().unwrap(); + let style = if with_preedit { + im.preedit_style + } else { + im.none_style + }; + + let context = unsafe { + ImeContext::new( + &self.inner.xconn, + im.im, + style, + window, + None, + self.inner.event_sender.clone(), + )? + }; + + // Check the state on the context, since it could fail to enable or disable preedit. + let event = if matches!(style, Style::None(_)) { + if with_preedit { + debug!("failed to create IME context with preedit support.") + } + ImeEvent::Disabled + } else { + if !with_preedit { + debug!("failed to create IME context without preedit support.") + } + ImeEvent::Enabled + }; + + self.inner + .event_sender + .send((window, event)) + .expect("Failed to send enabled event"); + + Some(context) + }; + + self.inner.contexts.insert(window, context); + Ok(!self.is_destroyed()) + } + + pub fn get_context(&self, window: ffi::Window) -> Option { + if self.is_destroyed() { + return None; + } + if let Some(Some(context)) = self.inner.contexts.get(&window) { + Some(context.ic) + } else { + None + } + } + + pub fn remove_context(&mut self, window: ffi::Window) -> Result { + if let Some(Some(context)) = self.inner.contexts.remove(&window) { + unsafe { + self.inner.destroy_ic_if_necessary(context.ic)?; + } + Ok(true) + } else { + Ok(false) + } + } + + pub fn focus(&mut self, window: ffi::Window) -> Result { + if self.is_destroyed() { + return Ok(false); + } + if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) { + context.focus(&self.xconn).map(|_| true) + } else { + Ok(false) + } + } + + pub fn unfocus(&mut self, window: ffi::Window) -> Result { + if self.is_destroyed() { + return Ok(false); + } + if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) { + context.unfocus(&self.xconn).map(|_| true) + } else { + Ok(false) + } + } + + pub fn send_xim_spot(&mut self, window: ffi::Window, x: i16, y: i16) { + if self.is_destroyed() { + return; + } + if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) { + context.set_spot(&self.xconn, x as _, y as _); + } + } + + pub fn set_ime_allowed(&mut self, window: ffi::Window, allowed: bool) { + if self.is_destroyed() { + return; + } + + if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) { + if allowed == context.is_allowed() { + return; + } + } + + // Remove context for that window. + let _ = self.remove_context(window); + + // Create new context supporting IME input. + let _ = self.create_context(window, allowed); + } +} + +impl Drop for Ime { + fn drop(&mut self) { + unsafe { + let _ = self.inner.destroy_all_contexts_if_necessary(); + let _ = self.inner.close_im_if_necessary(); + } + } +} diff --git a/rio-window/src/platform_impl/linux/x11/mod.rs b/rio-window/src/platform_impl/linux/x11/mod.rs new file mode 100644 index 00000000..b1bffbe7 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/mod.rs @@ -0,0 +1,1148 @@ +#![cfg(x11_platform)] + +use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::CStr; +use std::marker::PhantomData; +use std::mem::MaybeUninit; +use std::ops::Deref; +use std::os::raw::*; +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; +use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; +use std::sync::{Arc, Weak}; +use std::time::{Duration, Instant}; +use std::{fmt, ptr, slice, str}; + +use calloop::generic::Generic; +use calloop::ping::Ping; +use calloop::{EventLoop as Loop, Readiness}; +use libc::{setlocale, LC_CTYPE}; +use tracing::warn; + +use x11rb::connection::RequestConnection; +use x11rb::errors::{ConnectError, ConnectionError, IdsExhausted, ReplyError}; +use x11rb::protocol::xinput::{self, ConnectionExt as _}; +use x11rb::protocol::xkb; +use x11rb::protocol::xproto::{self, ConnectionExt as _}; +use x11rb::x11_utils::X11Error as LogicalError; +use x11rb::xcb_ffi::ReplyOrIdError; + +use crate::error::{EventLoopError, OsError as RootOsError}; +use crate::event::{Event, StartCause, WindowEvent}; +use crate::event_loop::{ + ActiveEventLoop as RootAEL, ControlFlow, DeviceEvents, EventLoopClosed, +}; +use crate::platform::pump_events::PumpStatus; +use crate::platform_impl::common::xkb::Context; +use crate::platform_impl::platform::{min_timeout, WindowId}; +use crate::platform_impl::{ + ActiveEventLoop as PlatformActiveEventLoop, OsError, PlatformCustomCursor, +}; +use crate::window::{ + CustomCursor as RootCustomCursor, CustomCursorSource, WindowAttributes, +}; + +mod activation; +mod atoms; +mod dnd; +mod event_processor; +pub mod ffi; +mod ime; +mod monitor; +mod util; +mod window; +mod xdisplay; +mod xsettings; + +pub use util::CustomCursor; + +use atoms::*; +use dnd::{Dnd, DndState}; +use event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN}; +use ime::{Ime, ImeCreationError, ImeReceiver, ImeRequest, ImeSender}; +pub(crate) use monitor::{MonitorHandle, VideoModeHandle}; +use window::UnownedWindow; +pub(crate) use xdisplay::{XConnection, XError, XNotSupported}; + +// Xinput constants not defined in x11rb +const ALL_DEVICES: u16 = 0; +const ALL_MASTER_DEVICES: u16 = 1; +const ICONIC_STATE: u32 = 3; + +/// The underlying x11rb connection that we are using. +type X11rbConnection = x11rb::xcb_ffi::XCBConnection; + +type X11Source = Generic>; + +struct WakeSender { + sender: Sender, + waker: Ping, +} + +impl Clone for WakeSender { + fn clone(&self) -> Self { + Self { + sender: self.sender.clone(), + waker: self.waker.clone(), + } + } +} + +impl WakeSender { + pub fn send(&self, t: T) -> Result<(), EventLoopClosed> { + let res = self.sender.send(t).map_err(|e| EventLoopClosed(e.0)); + if res.is_ok() { + self.waker.ping(); + } + res + } +} + +struct PeekableReceiver { + recv: Receiver, + first: Option, +} + +impl PeekableReceiver { + pub fn from_recv(recv: Receiver) -> Self { + Self { recv, first: None } + } + + pub fn has_incoming(&mut self) -> bool { + if self.first.is_some() { + return true; + } + + match self.recv.try_recv() { + Ok(v) => { + self.first = Some(v); + true + } + Err(TryRecvError::Empty) => false, + Err(TryRecvError::Disconnected) => { + warn!("Channel was disconnected when checking incoming"); + false + } + } + } + + pub fn try_recv(&mut self) -> Result { + if let Some(first) = self.first.take() { + return Ok(first); + } + self.recv.try_recv() + } +} + +pub struct ActiveEventLoop { + xconn: Arc, + wm_delete_window: xproto::Atom, + net_wm_ping: xproto::Atom, + ime_sender: ImeSender, + control_flow: Cell, + exit: Cell>, + root: xproto::Window, + ime: Option>, + windows: RefCell>>, + redraw_sender: WakeSender, + activation_sender: WakeSender, + device_events: Cell, +} + +pub struct EventLoop { + loop_running: bool, + event_loop: Loop<'static, EventLoopState>, + waker: calloop::ping::Ping, + event_processor: EventProcessor, + redraw_receiver: PeekableReceiver, + user_receiver: PeekableReceiver, + activation_receiver: PeekableReceiver, + user_sender: Sender, + + /// The current state of the event loop. + state: EventLoopState, +} + +type ActivationToken = (WindowId, crate::event_loop::AsyncRequestSerial); + +struct EventLoopState { + /// The latest readiness state for the x11 file descriptor + x11_readiness: Readiness, +} + +pub struct EventLoopProxy { + user_sender: WakeSender, +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy { + user_sender: self.user_sender.clone(), + } + } +} + +impl EventLoop { + pub(crate) fn new(xconn: Arc) -> EventLoop { + let root = xconn.default_root().root; + let atoms = xconn.atoms(); + + let wm_delete_window = atoms[WM_DELETE_WINDOW]; + let net_wm_ping = atoms[_NET_WM_PING]; + + let dnd = Dnd::new(Arc::clone(&xconn)) + .expect("Failed to call XInternAtoms when initializing drag and drop"); + + let (ime_sender, ime_receiver) = mpsc::channel(); + let (ime_event_sender, ime_event_receiver) = mpsc::channel(); + // Input methods will open successfully without setting the locale, but it won't be + // possible to actually commit pre-edit sequences. + unsafe { + // Remember default locale to restore it if target locale is unsupported + // by Xlib + let default_locale = setlocale(LC_CTYPE, ptr::null()); + setlocale(LC_CTYPE, b"\0".as_ptr() as *const _); + + // Check if set locale is supported by Xlib. + // If not, calls to some Xlib functions like `XSetLocaleModifiers` + // will fail. + let locale_supported = (xconn.xlib.XSupportsLocale)() == 1; + if !locale_supported { + let unsupported_locale = setlocale(LC_CTYPE, ptr::null()); + warn!( + "Unsupported locale \"{}\". Restoring default locale \"{}\".", + CStr::from_ptr(unsupported_locale).to_string_lossy(), + CStr::from_ptr(default_locale).to_string_lossy() + ); + // Restore default locale + setlocale(LC_CTYPE, default_locale); + } + } + + let ime = Ime::new(Arc::clone(&xconn), ime_event_sender); + if let Err(ImeCreationError::OpenFailure(state)) = ime.as_ref() { + warn!("Failed to open input method: {state:#?}"); + } else if let Err(err) = ime.as_ref() { + warn!("Failed to set input method destruction callback: {err:?}"); + } + + let ime = ime.ok().map(RefCell::new); + + let randr_event_offset = xconn + .select_xrandr_input(root) + .expect("Failed to query XRandR extension"); + + let xi2ext = xconn + .xcb_connection() + .extension_information(xinput::X11_EXTENSION_NAME) + .expect("Failed to query XInput extension") + .expect("X server missing XInput extension"); + let xkbext = xconn + .xcb_connection() + .extension_information(xkb::X11_EXTENSION_NAME) + .expect("Failed to query XKB extension") + .expect("X server missing XKB extension"); + + // Check for XInput2 support. + xconn + .xcb_connection() + .xinput_xi_query_version(2, 3) + .expect("Failed to send XInput2 query version request") + .reply() + .expect("Error while checking for XInput2 query version reply"); + + xconn.update_cached_wm_info(root); + + // Create an event loop. + let event_loop = Loop::::try_new() + .expect("Failed to initialize the event loop"); + let handle = event_loop.handle(); + + // Create the X11 event dispatcher. + let source = X11Source::new( + // SAFETY: xcb owns the FD and outlives the source. + unsafe { BorrowedFd::borrow_raw(xconn.xcb_connection().as_raw_fd()) }, + calloop::Interest::READ, + calloop::Mode::Level, + ); + handle + .insert_source(source, |readiness, _, state| { + state.x11_readiness = readiness; + Ok(calloop::PostAction::Continue) + }) + .expect("Failed to register the X11 event dispatcher"); + + let (waker, waker_source) = + calloop::ping::make_ping().expect("Failed to create event loop waker"); + event_loop + .handle() + .insert_source(waker_source, move |_, _, _| { + // No extra handling is required, we just need to wake-up. + }) + .expect("Failed to register the event loop waker source"); + + // Create a channel for handling redraw requests. + let (redraw_sender, redraw_channel) = mpsc::channel(); + + // Create a channel for sending activation tokens. + let (activation_token_sender, activation_token_channel) = mpsc::channel(); + + // Create a channel for sending user events. + let (user_sender, user_channel) = mpsc::channel(); + + let xkb_context = + Context::from_x11_xkb(xconn.xcb_connection().get_raw_xcb_connection()) + .unwrap(); + + let mut xmodmap = util::ModifierKeymap::new(); + xmodmap.reload_from_x_connection(&xconn); + + let window_target = ActiveEventLoop { + ime, + root, + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(None), + windows: Default::default(), + ime_sender, + xconn, + wm_delete_window, + net_wm_ping, + redraw_sender: WakeSender { + sender: redraw_sender, // not used again so no clone + waker: waker.clone(), + }, + activation_sender: WakeSender { + sender: activation_token_sender, // not used again so no clone + waker: waker.clone(), + }, + device_events: Default::default(), + }; + + // Set initial device event filter. + window_target.update_listen_device_events(true); + + let root_window_target = RootAEL { + p: PlatformActiveEventLoop::X(window_target), + _marker: PhantomData, + }; + + let event_processor = EventProcessor { + target: root_window_target, + dnd, + devices: Default::default(), + randr_event_offset, + ime_receiver, + ime_event_receiver, + xi2ext, + xfiltered_modifiers: VecDeque::with_capacity(MAX_MOD_REPLAY_LEN), + xmodmap, + xkbext, + xkb_context, + num_touch: 0, + held_key_press: None, + first_touch: None, + active_window: None, + modifiers: Default::default(), + is_composing: false, + }; + + // Register for device hotplug events + // (The request buffer is flushed during `init_device`) + let xconn = &EventProcessor::window_target(&event_processor.target).xconn; + + xconn + .select_xinput_events( + root, + ALL_DEVICES, + x11rb::protocol::xinput::XIEventMask::HIERARCHY, + ) + .expect_then_ignore_error( + "Failed to register for XInput2 device hotplug events", + ); + + xconn + .select_xkb_events( + 0x100, // Use the "core keyboard device" + xkb::EventType::NEW_KEYBOARD_NOTIFY + | xkb::EventType::MAP_NOTIFY + | xkb::EventType::STATE_NOTIFY, + ) + .unwrap(); + + event_processor.init_device(ALL_DEVICES); + + EventLoop { + loop_running: false, + event_loop, + waker, + event_processor, + redraw_receiver: PeekableReceiver::from_recv(redraw_channel), + activation_receiver: PeekableReceiver::from_recv(activation_token_channel), + user_receiver: PeekableReceiver::from_recv(user_channel), + user_sender, + state: EventLoopState { + x11_readiness: Readiness::EMPTY, + }, + } + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + user_sender: WakeSender { + sender: self.user_sender.clone(), + waker: self.waker.clone(), + }, + } + } + + pub(crate) fn window_target(&self) -> &RootAEL { + &self.event_processor.target + } + + pub fn run_on_demand(&mut self, mut event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootAEL), + { + let exit = loop { + match self.pump_events(None, &mut event_handler) { + PumpStatus::Exit(0) => { + break Ok(()); + } + PumpStatus::Exit(code) => { + break Err(EventLoopError::ExitFailure(code)); + } + _ => { + continue; + } + } + }; + + // Applications aren't allowed to carry windows between separate + // `run_on_demand` calls but if they have only just dropped their + // windows we need to make sure those last requests are sent to the + // X Server. + let wt = EventProcessor::window_target(&self.event_processor.target); + wt.x_connection().sync_with_server().map_err(|x_err| { + EventLoopError::Os(os_error!(OsError::XError(Arc::new(X11Error::Xlib( + x_err + ))))) + })?; + + exit + } + + pub fn pump_events( + &mut self, + timeout: Option, + mut callback: F, + ) -> PumpStatus + where + F: FnMut(Event, &RootAEL), + { + if !self.loop_running { + self.loop_running = true; + + // run the initial loop iteration + self.single_iteration(&mut callback, StartCause::Init); + } + + // Consider the possibility that the `StartCause::Init` iteration could + // request to Exit. + if !self.exiting() { + self.poll_events_with_timeout(timeout, &mut callback); + } + if let Some(code) = self.exit_code() { + self.loop_running = false; + + callback(Event::LoopExiting, self.window_target()); + + PumpStatus::Exit(code) + } else { + PumpStatus::Continue + } + } + + fn has_pending(&mut self) -> bool { + self.event_processor.poll() + || self.user_receiver.has_incoming() + || self.redraw_receiver.has_incoming() + } + + pub fn poll_events_with_timeout( + &mut self, + mut timeout: Option, + mut callback: F, + ) where + F: FnMut(Event, &RootAEL), + { + let start = Instant::now(); + + let has_pending = self.has_pending(); + + timeout = if has_pending { + // If we already have work to do then we don't want to block on the next poll. + Some(Duration::ZERO) + } else { + let control_flow_timeout = match self.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + Some(wait_deadline.saturating_duration_since(start)) + } + }; + + min_timeout(control_flow_timeout, timeout) + }; + + self.state.x11_readiness = Readiness::EMPTY; + if let Err(error) = self + .event_loop + .dispatch(timeout, &mut self.state) + .map_err(std::io::Error::from) + { + tracing::error!("Failed to poll for events: {error:?}"); + let exit_code = error.raw_os_error().unwrap_or(1); + self.set_exit_code(exit_code); + return; + } + + // NB: `StartCause::Init` is handled as a special case and doesn't need + // to be considered here + let cause = match self.control_flow() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { + start, + requested_resume: None, + }, + ControlFlow::WaitUntil(deadline) => { + if Instant::now() < deadline { + StartCause::WaitCancelled { + start, + requested_resume: Some(deadline), + } + } else { + StartCause::ResumeTimeReached { + start, + requested_resume: deadline, + } + } + } + }; + + // False positive / spurious wake ups could lead to us spamming + // redundant iterations of the event loop with no new events to + // dispatch. + // + // If there's no readable event source then we just double check if we + // have any pending `_receiver` events and if not we return without + // running a loop iteration. + // If we don't have any pending `_receiver` + if !self.has_pending() + && !matches!( + &cause, + StartCause::ResumeTimeReached { .. } | StartCause::Poll + ) + { + return; + } + + self.single_iteration(&mut callback, cause); + } + + fn single_iteration(&mut self, callback: &mut F, cause: StartCause) + where + F: FnMut(Event, &RootAEL), + { + callback(Event::NewEvents(cause), &self.event_processor.target); + + // NB: For consistency all platforms must emit a 'resumed' event even though X11 + // applications don't themselves have a formal suspend/resume lifecycle. + if cause == StartCause::Init { + callback(Event::Resumed, &self.event_processor.target); + } + + // Process all pending events + self.drain_events(callback); + + // Empty activation tokens. + while let Ok((window_id, serial)) = self.activation_receiver.try_recv() { + let token = self + .event_processor + .with_window(window_id.0 as xproto::Window, |window| { + window.generate_activation_token() + }); + + match token { + Some(Ok(token)) => { + let event = Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::ActivationTokenDone { + serial, + token: crate::window::ActivationToken::_new(token), + }, + }; + callback(event, &self.event_processor.target) + } + Some(Err(e)) => { + tracing::error!("Failed to get activation token: {}", e); + } + None => {} + } + } + + // Empty the user event buffer + { + while let Ok(event) = self.user_receiver.try_recv() { + callback(Event::UserEvent(event), &self.event_processor.target); + } + } + + // Empty the redraw requests + { + let mut windows = HashSet::new(); + + while let Ok(window_id) = self.redraw_receiver.try_recv() { + windows.insert(window_id); + } + + for window_id in windows { + let window_id = crate::window::WindowId(window_id); + callback( + Event::WindowEvent { + window_id, + event: WindowEvent::RedrawRequested, + }, + &self.event_processor.target, + ); + } + } + + // This is always the last event we dispatch before poll again + { + callback(Event::AboutToWait, &self.event_processor.target); + } + } + + fn drain_events(&mut self, callback: &mut F) + where + F: FnMut(Event, &RootAEL), + { + let mut xev = MaybeUninit::uninit(); + + while unsafe { self.event_processor.poll_one_event(xev.as_mut_ptr()) } { + let mut xev = unsafe { xev.assume_init() }; + self.event_processor + .process_event(&mut xev, |window_target, event| { + if let Event::WindowEvent { + window_id: crate::window::WindowId(wid), + event: WindowEvent::RedrawRequested, + } = event + { + let window_target = EventProcessor::window_target(window_target); + window_target.redraw_sender.send(wid).unwrap(); + } else { + callback(event, window_target); + } + }); + } + } + + fn control_flow(&self) -> ControlFlow { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.control_flow() + } + + fn exiting(&self) -> bool { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.exiting() + } + + fn set_exit_code(&self, code: i32) { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.set_exit_code(code); + } + + fn exit_code(&self) -> Option { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.exit_code() + } +} + +impl AsFd for EventLoop { + fn as_fd(&self) -> BorrowedFd<'_> { + self.event_loop.as_fd() + } +} + +impl AsRawFd for EventLoop { + fn as_raw_fd(&self) -> RawFd { + self.event_loop.as_raw_fd() + } +} + +impl ActiveEventLoop { + /// Returns the `XConnection` of this events loop. + #[inline] + pub(crate) fn x_connection(&self) -> &Arc { + &self.xconn + } + + pub fn available_monitors(&self) -> impl Iterator { + self.xconn.available_monitors().into_iter().flatten() + } + + pub fn primary_monitor(&self) -> Option { + self.xconn.primary_monitor().ok() + } + + pub(crate) fn create_custom_cursor( + &self, + cursor: CustomCursorSource, + ) -> RootCustomCursor { + RootCustomCursor { + inner: PlatformCustomCursor::X( + CustomCursor::new(self, cursor.inner).unwrap(), + ), + } + } + + pub fn listen_device_events(&self, allowed: DeviceEvents) { + self.device_events.set(allowed); + } + + /// Update the device event based on window focus. + pub fn update_listen_device_events(&self, focus: bool) { + let device_events = self.device_events.get() == DeviceEvents::Always + || (focus && self.device_events.get() == DeviceEvents::WhenFocused); + + let mut mask = xinput::XIEventMask::from(0u32); + if device_events { + mask = xinput::XIEventMask::RAW_MOTION + | xinput::XIEventMask::RAW_BUTTON_PRESS + | xinput::XIEventMask::RAW_BUTTON_RELEASE + | xinput::XIEventMask::RAW_KEY_PRESS + | xinput::XIEventMask::RAW_KEY_RELEASE; + } + + self.xconn + .select_xinput_events(self.root, ALL_MASTER_DEVICES, mask) + .expect_then_ignore_error("Failed to update device event filter"); + } + + #[cfg(feature = "rwh_06")] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + let display_handle = rwh_06::XlibDisplayHandle::new( + // SAFETY: display will never be null + Some( + std::ptr::NonNull::new(self.xconn.display as *mut _) + .expect("X11 display should never be null"), + ), + self.xconn.default_screen_index() as c_int, + ); + Ok(display_handle.into()) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub(crate) fn exit(&self) { + self.exit.set(Some(0)) + } + + pub(crate) fn clear_exit(&self) { + self.exit.set(None) + } + + pub(crate) fn exiting(&self) -> bool { + self.exit.get().is_some() + } + + pub(crate) fn set_exit_code(&self, code: i32) { + self.exit.set(Some(code)) + } + + pub(crate) fn exit_code(&self) -> Option { + self.exit.get() + } +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.user_sender + .send(event) + .map_err(|e| EventLoopClosed(e.0)) + } +} + +struct DeviceInfo<'a> { + xconn: &'a XConnection, + info: *const ffi::XIDeviceInfo, + count: usize, +} + +impl<'a> DeviceInfo<'a> { + fn get(xconn: &'a XConnection, device: c_int) -> Option { + unsafe { + let mut count = 0; + let info = (xconn.xinput2.XIQueryDevice)(xconn.display, device, &mut count); + xconn.check_errors().ok()?; + + if info.is_null() || count == 0 { + None + } else { + Some(DeviceInfo { + xconn, + info, + count: count as usize, + }) + } + } + } +} + +impl<'a> Drop for DeviceInfo<'a> { + fn drop(&mut self) { + assert!(!self.info.is_null()); + unsafe { (self.xconn.xinput2.XIFreeDeviceInfo)(self.info as *mut _) }; + } +} + +impl<'a> Deref for DeviceInfo<'a> { + type Target = [ffi::XIDeviceInfo]; + + fn deref(&self) -> &Self::Target { + unsafe { slice::from_raw_parts(self.info, self.count) } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(xinput::DeviceId); + +impl DeviceId { + #[allow(unused)] + pub const unsafe fn dummy() -> Self { + DeviceId(0) + } +} + +pub(crate) struct Window(Arc); + +impl Deref for Window { + type Target = UnownedWindow; + + #[inline] + fn deref(&self) -> &UnownedWindow { + &self.0 + } +} + +impl Window { + pub(crate) fn new( + event_loop: &ActiveEventLoop, + attribs: WindowAttributes, + ) -> Result { + let window = Arc::new(UnownedWindow::new(event_loop, attribs)?); + event_loop + .windows + .borrow_mut() + .insert(window.id(), Arc::downgrade(&window)); + Ok(Window(window)) + } +} + +impl Drop for Window { + fn drop(&mut self) { + let window = self.deref(); + let xconn = &window.xconn; + + if let Ok(c) = xconn + .xcb_connection() + .destroy_window(window.id().0 as xproto::Window) + { + c.ignore_error(); + } + } +} + +/// Generic sum error type for X11 errors. +#[derive(Debug)] +pub enum X11Error { + /// An error from the Xlib library. + Xlib(XError), + + /// An error that occurred while trying to connect to the X server. + Connect(ConnectError), + + /// An error that occurred over the connection medium. + Connection(ConnectionError), + + /// An error that occurred logically on the X11 end. + X11(LogicalError), + + /// The XID range has been exhausted. + XidsExhausted(IdsExhausted), + + /// Got `null` from an Xlib function without a reason. + UnexpectedNull(&'static str), + + /// Got an invalid activation token. + InvalidActivationToken(Vec), + + /// An extension that we rely on is not available. + MissingExtension(&'static str), + + /// Could not find a matching X11 visual for this visualid + NoSuchVisual(xproto::Visualid), + + /// Unable to parse xsettings. + XsettingsParse(xsettings::ParserError), + + /// Failed to get property. + GetProperty(util::GetPropertyError), + + /// Could not find an ARGB32 pict format. + NoArgb32Format, +} + +impl fmt::Display for X11Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + X11Error::Xlib(e) => write!(f, "Xlib error: {}", e), + X11Error::Connect(e) => write!(f, "X11 connection error: {}", e), + X11Error::Connection(e) => write!(f, "X11 connection error: {}", e), + X11Error::XidsExhausted(e) => write!(f, "XID range exhausted: {}", e), + X11Error::GetProperty(e) => write!(f, "Failed to get X property {}", e), + X11Error::X11(e) => write!(f, "X11 error: {:?}", e), + X11Error::UnexpectedNull(s) => { + write!(f, "Xlib function returned null: {}", s) + } + X11Error::InvalidActivationToken(s) => write!( + f, + "Invalid activation token: {}", + std::str::from_utf8(s).unwrap_or("") + ), + X11Error::MissingExtension(s) => write!(f, "Missing X11 extension: {}", s), + X11Error::NoSuchVisual(visualid) => { + write!( + f, + "Could not find a matching X11 visual for ID `{:x}`", + visualid + ) + } + X11Error::XsettingsParse(err) => { + write!(f, "Failed to parse xsettings: {:?}", err) + } + X11Error::NoArgb32Format => f.write_str( + "winit only supports X11 displays with ARGB32 picture formats", + ), + } + } +} + +impl std::error::Error for X11Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + X11Error::Xlib(e) => Some(e), + X11Error::Connect(e) => Some(e), + X11Error::Connection(e) => Some(e), + X11Error::XidsExhausted(e) => Some(e), + _ => None, + } + } +} + +impl From for X11Error { + fn from(e: XError) -> Self { + X11Error::Xlib(e) + } +} + +impl From for X11Error { + fn from(e: ConnectError) -> Self { + X11Error::Connect(e) + } +} + +impl From for X11Error { + fn from(e: ConnectionError) -> Self { + X11Error::Connection(e) + } +} + +impl From for X11Error { + fn from(e: LogicalError) -> Self { + X11Error::X11(e) + } +} + +impl From for X11Error { + fn from(value: ReplyError) -> Self { + match value { + ReplyError::ConnectionError(e) => e.into(), + ReplyError::X11Error(e) => e.into(), + } + } +} + +impl From for X11Error { + fn from(value: ime::ImeContextCreationError) -> Self { + match value { + ime::ImeContextCreationError::XError(e) => e.into(), + ime::ImeContextCreationError::Null => Self::UnexpectedNull("XOpenIM"), + } + } +} + +impl From for X11Error { + fn from(value: ReplyOrIdError) -> Self { + match value { + ReplyOrIdError::ConnectionError(e) => e.into(), + ReplyOrIdError::X11Error(e) => e.into(), + ReplyOrIdError::IdsExhausted => Self::XidsExhausted(IdsExhausted), + } + } +} + +impl From for X11Error { + fn from(value: xsettings::ParserError) -> Self { + Self::XsettingsParse(value) + } +} + +impl From for X11Error { + fn from(value: util::GetPropertyError) -> Self { + Self::GetProperty(value) + } +} + +/// Type alias for a void cookie. +type VoidCookie<'a> = x11rb::cookie::VoidCookie<'a, X11rbConnection>; + +/// Extension trait for `Result`. +trait CookieResultExt { + /// Unwrap the send error and ignore the result. + fn expect_then_ignore_error(self, msg: &str); +} + +impl<'a, E: fmt::Debug> CookieResultExt for Result, E> { + fn expect_then_ignore_error(self, msg: &str) { + self.expect(msg).ignore_error() + } +} + +fn mkwid(w: xproto::Window) -> crate::window::WindowId { + crate::window::WindowId(crate::platform_impl::platform::WindowId(w as _)) +} +fn mkdid(w: xinput::DeviceId) -> crate::event::DeviceId { + crate::event::DeviceId(crate::platform_impl::DeviceId::X(DeviceId(w))) +} + +#[derive(Debug)] +pub struct Device { + _name: String, + scroll_axes: Vec<(i32, ScrollAxis)>, + // For master devices, this is the paired device (pointer <-> keyboard). + // For slave devices, this is the master. + attachment: c_int, +} + +#[derive(Debug, Copy, Clone)] +struct ScrollAxis { + increment: f64, + orientation: ScrollOrientation, + position: f64, +} + +#[derive(Debug, Copy, Clone)] +enum ScrollOrientation { + Vertical, + Horizontal, +} + +impl Device { + fn new(info: &ffi::XIDeviceInfo) -> Self { + let name = unsafe { CStr::from_ptr(info.name).to_string_lossy() }; + let mut scroll_axes = Vec::new(); + + if Device::physical_device(info) { + // Identify scroll axes + for &class_ptr in Device::classes(info) { + let ty = unsafe { (*class_ptr)._type }; + if ty == ffi::XIScrollClass { + let info = unsafe { &*(class_ptr as *const ffi::XIScrollClassInfo) }; + scroll_axes.push(( + info.number, + ScrollAxis { + increment: info.increment, + orientation: match info.scroll_type { + ffi::XIScrollTypeHorizontal => { + ScrollOrientation::Horizontal + } + ffi::XIScrollTypeVertical => ScrollOrientation::Vertical, + _ => unreachable!(), + }, + position: 0.0, + }, + )); + } + } + } + + let mut device = Device { + _name: name.into_owned(), + scroll_axes, + attachment: info.attachment, + }; + device.reset_scroll_position(info); + device + } + + fn reset_scroll_position(&mut self, info: &ffi::XIDeviceInfo) { + if Device::physical_device(info) { + for &class_ptr in Device::classes(info) { + let ty = unsafe { (*class_ptr)._type }; + if ty == ffi::XIValuatorClass { + let info = + unsafe { &*(class_ptr as *const ffi::XIValuatorClassInfo) }; + if let Some(&mut (_, ref mut axis)) = self + .scroll_axes + .iter_mut() + .find(|&&mut (axis, _)| axis == info.number) + { + axis.position = info.value; + } + } + } + } + } + + #[inline] + fn physical_device(info: &ffi::XIDeviceInfo) -> bool { + info._use == ffi::XISlaveKeyboard + || info._use == ffi::XISlavePointer + || info._use == ffi::XIFloatingSlave + } + + #[inline] + fn classes(info: &ffi::XIDeviceInfo) -> &[*const ffi::XIAnyClassInfo] { + unsafe { + slice::from_raw_parts( + info.classes as *const *const ffi::XIAnyClassInfo, + info.num_classes as usize, + ) + } + } +} + +/// Convert the raw X11 representation for a 32-bit floating point to a double. +#[inline] +fn xinput_fp1616_to_float(fp: xinput::Fp1616) -> f64 { + (fp as f64) / ((1 << 16) as f64) +} diff --git a/rio-window/src/platform_impl/linux/x11/monitor.rs b/rio-window/src/platform_impl/linux/x11/monitor.rs new file mode 100644 index 00000000..635b11d6 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/monitor.rs @@ -0,0 +1,379 @@ +use super::{util, X11Error, XConnection}; +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::platform_impl::VideoModeHandle as PlatformVideoModeHandle; +use x11rb::connection::RequestConnection; +use x11rb::protocol::randr::{self, ConnectionExt as _}; +use x11rb::protocol::xproto; + +// Used for testing. This should always be committed as false. +const DISABLE_MONITOR_LIST_CACHING: bool = false; + +impl XConnection { + pub fn invalidate_cached_monitor_list(&self) -> Option> { + // We update this lazily. + self.monitor_handles.lock().unwrap().take() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct VideoModeHandle { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate_millihertz: u32, + pub(crate) native_mode: randr::Mode, + pub(crate) monitor: Option, +} + +impl VideoModeHandle { + #[inline] + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + #[inline] + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + #[inline] + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone().unwrap() + } +} + +#[derive(Debug, Clone)] +pub struct MonitorHandle { + /// The actual id + pub(crate) id: randr::Crtc, + /// The name of the monitor + pub(crate) name: String, + /// The size of the monitor + dimensions: (u32, u32), + /// The position of the monitor in the X screen + position: (i32, i32), + /// If the monitor is the primary one + primary: bool, + /// The refresh rate used by monitor. + refresh_rate_millihertz: Option, + /// The DPI scale factor + pub(crate) scale_factor: f64, + /// Used to determine which windows are on this monitor + pub(crate) rect: util::AaRect, + /// Supported video modes on this monitor + video_modes: Vec, +} + +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for MonitorHandle {} + +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MonitorHandle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +#[inline] +pub fn mode_refresh_rate_millihertz(mode: &randr::ModeInfo) -> Option { + if mode.dot_clock > 0 && mode.htotal > 0 && mode.vtotal > 0 { + #[allow(clippy::unnecessary_cast)] + Some( + (mode.dot_clock as u64 * 1000 / (mode.htotal as u64 * mode.vtotal as u64)) + as u32, + ) + } else { + None + } +} + +impl MonitorHandle { + fn new( + xconn: &XConnection, + resources: &ScreenResources, + id: randr::Crtc, + crtc: &randr::GetCrtcInfoReply, + primary: bool, + ) -> Option { + let (name, scale_factor, video_modes) = xconn.get_output_info(resources, crtc)?; + let dimensions = (crtc.width as u32, crtc.height as u32); + let position = (crtc.x as i32, crtc.y as i32); + + // Get the refresh rate of the current video mode. + let current_mode = crtc.mode; + let screen_modes = resources.modes(); + let refresh_rate_millihertz = screen_modes + .iter() + .find(|mode| mode.id == current_mode) + .and_then(mode_refresh_rate_millihertz); + + let rect = util::AaRect::new(position, dimensions); + + Some(MonitorHandle { + id, + name, + refresh_rate_millihertz, + scale_factor, + dimensions, + position, + primary, + rect, + video_modes, + }) + } + + pub fn dummy() -> Self { + MonitorHandle { + id: 0, + name: "".into(), + scale_factor: 1.0, + dimensions: (1, 1), + position: (0, 0), + refresh_rate_millihertz: None, + primary: true, + rect: util::AaRect::new((0, 0), (1, 1)), + video_modes: Vec::new(), + } + } + + pub(crate) fn is_dummy(&self) -> bool { + // Zero is an invalid XID value; no real monitor will have it + self.id == 0 + } + + pub fn name(&self) -> Option { + Some(self.name.clone()) + } + + #[inline] + pub fn native_identifier(&self) -> u32 { + self.id as _ + } + + pub fn size(&self) -> PhysicalSize { + self.dimensions.into() + } + + pub fn position(&self) -> PhysicalPosition { + self.position.into() + } + + pub fn refresh_rate_millihertz(&self) -> Option { + self.refresh_rate_millihertz + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.scale_factor + } + + #[inline] + pub fn video_modes(&self) -> impl Iterator { + let monitor = self.clone(); + self.video_modes.clone().into_iter().map(move |mut x| { + x.monitor = Some(monitor.clone()); + PlatformVideoModeHandle::X(x) + }) + } +} + +impl XConnection { + pub fn get_monitor_for_window( + &self, + window_rect: Option, + ) -> Result { + let monitors = self.available_monitors()?; + + if monitors.is_empty() { + // Return a dummy monitor to avoid panicking + return Ok(MonitorHandle::dummy()); + } + + let default = monitors.first().unwrap(); + + let window_rect = match window_rect { + Some(rect) => rect, + None => return Ok(default.to_owned()), + }; + + let mut largest_overlap = 0; + let mut matched_monitor = default; + for monitor in &monitors { + let overlapping_area = window_rect.get_overlapping_area(&monitor.rect); + if overlapping_area > largest_overlap { + largest_overlap = overlapping_area; + matched_monitor = monitor; + } + } + + Ok(matched_monitor.to_owned()) + } + + fn query_monitor_list(&self) -> Result, X11Error> { + let root = self.default_root(); + let resources = ScreenResources::from_connection( + self.xcb_connection(), + root, + self.randr_version(), + )?; + + // Pipeline all of the get-crtc requests. + let mut crtc_cookies = Vec::with_capacity(resources.crtcs().len()); + for &crtc in resources.crtcs() { + crtc_cookies.push( + self.xcb_connection() + .randr_get_crtc_info(crtc, x11rb::CURRENT_TIME)?, + ); + } + + // Do this here so we do all of our requests in one shot. + let primary = self + .xcb_connection() + .randr_get_output_primary(root.root)? + .reply()? + .output; + + let mut crtc_infos = Vec::with_capacity(crtc_cookies.len()); + for cookie in crtc_cookies { + let reply = cookie.reply()?; + crtc_infos.push(reply); + } + + let mut has_primary = false; + let mut available_monitors = Vec::with_capacity(resources.crtcs().len()); + for (crtc_id, crtc) in resources.crtcs().iter().zip(crtc_infos.iter()) { + if crtc.width == 0 || crtc.height == 0 || crtc.outputs.is_empty() { + continue; + } + + let is_primary = crtc.outputs[0] == primary; + has_primary |= is_primary; + let monitor = + MonitorHandle::new(self, &resources, *crtc_id, crtc, is_primary); + available_monitors.extend(monitor); + } + + // If we don't have a primary monitor, just pick one ourselves! + if !has_primary { + if let Some(ref mut fallback) = available_monitors.first_mut() { + // Setting this here will come in handy if we ever add an `is_primary` method. + fallback.primary = true; + } + } + + Ok(available_monitors) + } + + pub fn available_monitors(&self) -> Result, X11Error> { + let mut monitors_lock = self.monitor_handles.lock().unwrap(); + match *monitors_lock { + Some(ref monitors) => Ok(monitors.clone()), + None => { + let monitors = self.query_monitor_list()?; + if !DISABLE_MONITOR_LIST_CACHING { + *monitors_lock = Some(monitors.clone()); + } + Ok(monitors) + } + } + } + + #[inline] + pub fn primary_monitor(&self) -> Result { + Ok(self + .available_monitors()? + .into_iter() + .find(|monitor| monitor.primary) + .unwrap_or_else(MonitorHandle::dummy)) + } + + pub fn select_xrandr_input(&self, root: xproto::Window) -> Result { + use randr::NotifyMask; + + // Get extension info. + let info = self + .xcb_connection() + .extension_information(randr::X11_EXTENSION_NAME)? + .ok_or_else(|| X11Error::MissingExtension(randr::X11_EXTENSION_NAME))?; + + // Select input data. + let event_mask = NotifyMask::CRTC_CHANGE + | NotifyMask::OUTPUT_PROPERTY + | NotifyMask::SCREEN_CHANGE; + self.xcb_connection().randr_select_input(root, event_mask)?; + + Ok(info.first_event) + } +} + +pub struct ScreenResources { + /// List of attached modes. + modes: Vec, + + /// List of attached CRTCs. + crtcs: Vec, +} + +impl ScreenResources { + pub(crate) fn modes(&self) -> &[randr::ModeInfo] { + &self.modes + } + + pub(crate) fn crtcs(&self) -> &[randr::Crtc] { + &self.crtcs + } + + pub(crate) fn from_connection( + conn: &impl x11rb::connection::Connection, + root: &x11rb::protocol::xproto::Screen, + (major_version, minor_version): (u32, u32), + ) -> Result { + if (major_version == 1 && minor_version >= 3) || major_version > 1 { + let reply = conn + .randr_get_screen_resources_current(root.root)? + .reply()?; + Ok(Self::from_get_screen_resources_current_reply(reply)) + } else { + let reply = conn.randr_get_screen_resources(root.root)?.reply()?; + Ok(Self::from_get_screen_resources_reply(reply)) + } + } + + pub(crate) fn from_get_screen_resources_reply( + reply: randr::GetScreenResourcesReply, + ) -> Self { + Self { + modes: reply.modes, + crtcs: reply.crtcs, + } + } + + pub(crate) fn from_get_screen_resources_current_reply( + reply: randr::GetScreenResourcesCurrentReply, + ) -> Self { + Self { + modes: reply.modes, + crtcs: reply.crtcs, + } + } +} diff --git a/rio-window/src/platform_impl/linux/x11/tests/xsettings.dat b/rio-window/src/platform_impl/linux/x11/tests/xsettings.dat new file mode 100644 index 00000000..e04eeb14 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/tests/xsettings.dat @@ -0,0 +1 @@ +0x6c,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x22,0x00,0x00,0x00,0x00,0x00,0x0b,0x00,0x58,0x66,0x74,0x2f,0x48,0x69,0x6e,0x74,0x69,0x6e,0x67,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x14,0x00,0x47,0x74,0x6b,0x2f,0x44,0x69,0x61,0x6c,0x6f,0x67,0x73,0x55,0x73,0x65,0x48,0x65,0x61,0x64,0x65,0x72,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x0c,0x00,0x47,0x74,0x6b,0x2f,0x46,0x6f,0x6e,0x74,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x0b,0x00,0x00,0x00,0x4e,0x6f,0x74,0x6f,0x20,0x53,0x61,0x6e,0x73,0x20,0x39,0x00,0x01,0x00,0x0d,0x00,0x58,0x66,0x74,0x2f,0x4c,0x63,0x64,0x66,0x69,0x6c,0x74,0x65,0x72,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0a,0x00,0x00,0x00,0x6c,0x63,0x64,0x64,0x65,0x66,0x61,0x75,0x6c,0x74,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x4b,0x65,0x79,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x0d,0x00,0x58,0x66,0x74,0x2f,0x48,0x69,0x6e,0x74,0x53,0x74,0x79,0x6c,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0a,0x00,0x00,0x00,0x68,0x69,0x6e,0x74,0x73,0x6c,0x69,0x67,0x68,0x74,0x00,0x00,0x01,0x00,0x11,0x00,0x4e,0x65,0x74,0x2f,0x49,0x63,0x6f,0x6e,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,0x00,0x00,0x00,0x65,0x6c,0x65,0x6d,0x65,0x6e,0x74,0x61,0x72,0x79,0x2d,0x78,0x66,0x63,0x65,0x2d,0x64,0x61,0x72,0x6b,0x00,0x00,0x0d,0x00,0x58,0x66,0x74,0x2f,0x41,0x6e,0x74,0x69,0x61,0x6c,0x69,0x61,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x08,0x00,0x58,0x66,0x74,0x2f,0x52,0x47,0x42,0x41,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x72,0x67,0x62,0x00,0x00,0x00,0x13,0x00,0x4e,0x65,0x74,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x42,0x6c,0x69,0x6e,0x6b,0x54,0x69,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0xb0,0x04,0x00,0x00,0x00,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x54,0x68,0x65,0x6d,0x65,0x53,0x69,0x7a,0x65,0x00,0x00,0x00,0x00,0x00,0x18,0x00,0x00,0x00,0x01,0x00,0x15,0x00,0x4e,0x65,0x74,0x2f,0x46,0x61,0x6c,0x6c,0x62,0x61,0x63,0x6b,0x49,0x63,0x6f,0x6e,0x54,0x68,0x65,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x67,0x6e,0x6f,0x6d,0x65,0x00,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x54,0x6f,0x6f,0x6c,0x62,0x61,0x72,0x53,0x74,0x79,0x6c,0x65,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x69,0x63,0x6f,0x6e,0x73,0x00,0x00,0x00,0x01,0x00,0x12,0x00,0x4e,0x65,0x74,0x2f,0x53,0x6f,0x75,0x6e,0x64,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x00,0x00,0x00,0x64,0x65,0x66,0x61,0x75,0x6c,0x74,0x00,0x00,0x00,0x15,0x00,0x4e,0x65,0x74,0x2f,0x45,0x6e,0x61,0x62,0x6c,0x65,0x45,0x76,0x65,0x6e,0x74,0x53,0x6f,0x75,0x6e,0x64,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0x00,0x4e,0x65,0x74,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x42,0x6c,0x69,0x6e,0x6b,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x43,0x6f,0x6c,0x6f,0x72,0x50,0x61,0x6c,0x65,0x74,0x74,0x65,0x00,0x00,0x00,0x00,0x94,0x00,0x00,0x00,0x62,0x6c,0x61,0x63,0x6b,0x3a,0x77,0x68,0x69,0x74,0x65,0x3a,0x67,0x72,0x61,0x79,0x35,0x30,0x3a,0x72,0x65,0x64,0x3a,0x70,0x75,0x72,0x70,0x6c,0x65,0x3a,0x62,0x6c,0x75,0x65,0x3a,0x6c,0x69,0x67,0x68,0x74,0x20,0x62,0x6c,0x75,0x65,0x3a,0x67,0x72,0x65,0x65,0x6e,0x3a,0x79,0x65,0x6c,0x6c,0x6f,0x77,0x3a,0x6f,0x72,0x61,0x6e,0x67,0x65,0x3a,0x6c,0x61,0x76,0x65,0x6e,0x64,0x65,0x72,0x3a,0x62,0x72,0x6f,0x77,0x6e,0x3a,0x67,0x6f,0x6c,0x64,0x65,0x6e,0x72,0x6f,0x64,0x34,0x3a,0x64,0x6f,0x64,0x67,0x65,0x72,0x20,0x62,0x6c,0x75,0x65,0x3a,0x70,0x69,0x6e,0x6b,0x3a,0x6c,0x69,0x67,0x68,0x74,0x20,0x67,0x72,0x65,0x65,0x6e,0x3a,0x67,0x72,0x61,0x79,0x31,0x30,0x3a,0x67,0x72,0x61,0x79,0x33,0x30,0x3a,0x67,0x72,0x61,0x79,0x37,0x35,0x3a,0x67,0x72,0x61,0x79,0x39,0x30,0x00,0x00,0x13,0x00,0x4e,0x65,0x74,0x2f,0x44,0x6f,0x75,0x62,0x6c,0x65,0x43,0x6c,0x69,0x63,0x6b,0x54,0x69,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x90,0x01,0x00,0x00,0x00,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x43,0x61,0x6e,0x43,0x68,0x61,0x6e,0x67,0x65,0x41,0x63,0x63,0x65,0x6c,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x4d,0x65,0x6e,0x75,0x42,0x61,0x72,0x41,0x63,0x63,0x65,0x6c,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x46,0x31,0x30,0x00,0x01,0x00,0x0d,0x00,0x4e,0x65,0x74,0x2f,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x47,0x72,0x65,0x79,0x62,0x69,0x72,0x64,0x01,0x00,0x17,0x00,0x47,0x74,0x6b,0x2f,0x54,0x69,0x74,0x6c,0x65,0x62,0x61,0x72,0x4d,0x69,0x64,0x64,0x6c,0x65,0x43,0x6c,0x69,0x63,0x6b,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x6c,0x6f,0x77,0x65,0x72,0x00,0x00,0x00,0x00,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x42,0x75,0x74,0x74,0x6f,0x6e,0x49,0x6d,0x61,0x67,0x65,0x73,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x17,0x00,0x4e,0x65,0x74,0x2f,0x44,0x6f,0x75,0x62,0x6c,0x65,0x43,0x6c,0x69,0x63,0x6b,0x44,0x69,0x73,0x74,0x61,0x6e,0x63,0x65,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x01,0x00,0x15,0x00,0x47,0x74,0x6b,0x2f,0x4d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x46,0x6f,0x6e,0x74,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0c,0x00,0x00,0x00,0x4d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x20,0x31,0x30,0x00,0x00,0x07,0x00,0x58,0x66,0x74,0x2f,0x44,0x50,0x49,0x00,0x02,0x00,0x00,0x00,0x00,0x80,0x01,0x00,0x01,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x09,0x00,0x00,0x00,0x44,0x4d,0x5a,0x2d,0x57,0x68,0x69,0x74,0x65,0x00,0x00,0x00,0x00,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x54,0x6f,0x6f,0x6c,0x62,0x61,0x72,0x49,0x63,0x6f,0x6e,0x53,0x69,0x7a,0x65,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x14,0x00,0x4e,0x65,0x74,0x2f,0x44,0x6e,0x64,0x44,0x72,0x61,0x67,0x54,0x68,0x72,0x65,0x73,0x68,0x6f,0x6c,0x64,0x00,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x01,0x00,0x14,0x00,0x47,0x74,0x6b,0x2f,0x44,0x65,0x63,0x6f,0x72,0x61,0x74,0x69,0x6f,0x6e,0x4c,0x61,0x79,0x6f,0x75,0x74,0x00,0x00,0x00,0x00,0x1c,0x00,0x00,0x00,0x6d,0x65,0x6e,0x75,0x3a,0x6d,0x69,0x6e,0x69,0x6d,0x69,0x7a,0x65,0x2c,0x6d,0x61,0x78,0x69,0x6d,0x69,0x7a,0x65,0x2c,0x63,0x6c,0x6f,0x73,0x65,0x00,0x00,0x1d,0x00,0x4e,0x65,0x74,0x2f,0x45,0x6e,0x61,0x62,0x6c,0x65,0x49,0x6e,0x70,0x75,0x74,0x46,0x65,0x65,0x64,0x62,0x61,0x63,0x6b,0x53,0x6f,0x75,0x6e,0x64,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x17,0x00,0x47,0x64,0x6b,0x2f,0x57,0x69,0x6e,0x64,0x6f,0x77,0x53,0x63,0x61,0x6c,0x69,0x6e,0x67,0x46,0x61,0x63,0x74,0x6f,0x72,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x0d,0x00,0x47,0x74,0x6b,0x2f,0x49,0x63,0x6f,0x6e,0x53,0x69,0x7a,0x65,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x00,0x00,0x00,0x67,0x74,0x6b,0x2d,0x62,0x75,0x74,0x74,0x6f,0x6e,0x3d,0x31,0x36,0x2c,0x31,0x36,0x00,0x00,0x0e,0x00,0x47,0x74,0x6b,0x2f,0x4d,0x65,0x6e,0x75,0x49,0x6d,0x61,0x67,0x65,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00 diff --git a/rio-window/src/platform_impl/linux/x11/util/client_msg.rs b/rio-window/src/platform_impl/linux/x11/util/client_msg.rs new file mode 100644 index 00000000..cf551776 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/client_msg.rs @@ -0,0 +1,31 @@ +use super::*; +use x11rb::x11_utils::Serialize; + +impl XConnection { + pub fn send_client_msg( + &self, + window: xproto::Window, // The window this is "about"; not necessarily this window + target_window: xproto::Window, // The window we're sending to + message_type: xproto::Atom, + event_mask: Option, + data: impl Into, + ) -> Result, X11Error> { + let event = xproto::ClientMessageEvent { + response_type: xproto::CLIENT_MESSAGE_EVENT, + window, + format: 32, + data: data.into(), + sequence: 0, + type_: message_type, + }; + + self.xcb_connection() + .send_event( + false, + target_window, + event_mask.unwrap_or(xproto::EventMask::NO_EVENT), + event.serialize(), + ) + .map_err(Into::into) + } +} diff --git a/rio-window/src/platform_impl/linux/x11/util/cookie.rs b/rio-window/src/platform_impl/linux/x11/util/cookie.rs new file mode 100644 index 00000000..13bb2171 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/cookie.rs @@ -0,0 +1,58 @@ +use std::ffi::c_int; +use std::sync::Arc; + +use x11_dl::xlib::{self, XEvent, XGenericEventCookie}; + +use crate::platform_impl::x11::XConnection; + +/// XEvents of type GenericEvent store their actual data in an XGenericEventCookie data structure. +/// This is a wrapper to extract the cookie from a GenericEvent XEvent and release the cookie data +/// once it has been processed +pub struct GenericEventCookie { + cookie: XGenericEventCookie, + xconn: Arc, +} + +impl GenericEventCookie { + pub fn from_event( + xconn: Arc, + event: XEvent, + ) -> Option { + unsafe { + let mut cookie: XGenericEventCookie = From::from(event); + if (xconn.xlib.XGetEventData)(xconn.display, &mut cookie) == xlib::True { + Some(GenericEventCookie { cookie, xconn }) + } else { + None + } + } + } + + #[inline] + pub fn extension(&self) -> u8 { + self.cookie.extension as u8 + } + + #[inline] + pub fn evtype(&self) -> c_int { + self.cookie.evtype + } + + /// Borrow inner event data as `&T`. + /// + /// ## SAFETY + /// + /// The caller must ensure that the event has the `T` inside of it. + #[inline] + pub unsafe fn as_event(&self) -> &T { + unsafe { &*(self.cookie.data as *const _) } + } +} + +impl Drop for GenericEventCookie { + fn drop(&mut self) { + unsafe { + (self.xconn.xlib.XFreeEventData)(self.xconn.display, &mut self.cookie); + } + } +} diff --git a/rio-window/src/platform_impl/linux/x11/util/cursor.rs b/rio-window/src/platform_impl/linux/x11/util/cursor.rs new file mode 100644 index 00000000..1e8667a3 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/cursor.rs @@ -0,0 +1,257 @@ +use std::collections::hash_map::Entry; +use std::hash::{Hash, Hasher}; +use std::iter; +use std::sync::Arc; + +use x11rb::connection::Connection; +use x11rb::protocol::render::{self, ConnectionExt as _}; +use x11rb::protocol::xproto; + +use crate::error::ExternalError; +use crate::platform_impl::OsError; +use crate::platform_impl::PlatformCustomCursorSource; +use crate::window::CursorIcon; + +use super::super::ActiveEventLoop; +use super::*; + +impl XConnection { + pub fn set_cursor_icon( + &self, + window: xproto::Window, + cursor: Option, + ) -> Result<(), X11Error> { + let cursor = { + let mut cache = self.cursor_cache.lock().unwrap_or_else(|e| e.into_inner()); + + match cache.entry(cursor) { + Entry::Occupied(o) => *o.get(), + Entry::Vacant(v) => *v.insert(self.get_cursor(cursor)?), + } + }; + + self.update_cursor(window, cursor) + } + + pub(crate) fn set_custom_cursor( + &self, + window: xproto::Window, + cursor: &CustomCursor, + ) -> Result<(), X11Error> { + self.update_cursor(window, cursor.inner.cursor) + } + + /// Create a cursor from an image. + fn create_cursor_from_image( + &self, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + image: &[u8], + ) -> Result { + // Create a pixmap for the default root window. + let root = self.default_root().root; + let pixmap = xproto::PixmapWrapper::create_pixmap( + self.xcb_connection(), + 32, + root, + width, + height, + )?; + + // Create a GC to draw with. + let gc = xproto::GcontextWrapper::create_gc( + self.xcb_connection(), + pixmap.pixmap(), + &Default::default(), + )?; + + // Draw the data into it. + self.xcb_connection() + .put_image( + xproto::ImageFormat::Z_PIXMAP, + pixmap.pixmap(), + gc.gcontext(), + width, + height, + 0, + 0, + 0, + 32, + image, + )? + .ignore_error(); + drop(gc); + + // Create the XRender picture. + let picture = render::PictureWrapper::create_picture( + self.xcb_connection(), + pixmap.pixmap(), + self.find_argb32_format()?, + &Default::default(), + )?; + drop(pixmap); + + // Create the cursor. + let cursor = self.xcb_connection().generate_id()?; + self.xcb_connection() + .render_create_cursor(cursor, picture.picture(), hotspot_x, hotspot_y)? + .check()?; + + Ok(cursor) + } + + /// Find the render format that corresponds to ARGB32. + fn find_argb32_format(&self) -> Result { + macro_rules! direct { + ($format:expr, $shift_name:ident, $mask_name:ident, $shift:expr) => {{ + ($format).direct.$shift_name == $shift + && ($format).direct.$mask_name == 0xff + }}; + } + + self.render_formats() + .formats + .iter() + .find(|format| { + format.type_ == render::PictType::DIRECT + && format.depth == 32 + && direct!(format, red_shift, red_mask, 16) + && direct!(format, green_shift, green_mask, 8) + && direct!(format, blue_shift, blue_mask, 0) + && direct!(format, alpha_shift, alpha_mask, 24) + }) + .ok_or(X11Error::NoArgb32Format) + .map(|format| format.id) + } + + fn create_empty_cursor(&self) -> Result { + self.create_cursor_from_image(1, 1, 0, 0, &[0, 0, 0, 0]) + } + + fn get_cursor(&self, cursor: Option) -> Result { + let cursor = match cursor { + Some(cursor) => cursor, + None => return self.create_empty_cursor(), + }; + + let database = self.database(); + let handle = x11rb::cursor::Handle::new( + self.xcb_connection(), + self.default_screen_index(), + &database, + )? + .reply()?; + + let mut last_error = None; + for &name in iter::once(&cursor.name()).chain(cursor.alt_names().iter()) { + match handle.load_cursor(self.xcb_connection(), name) { + Ok(cursor) => return Ok(cursor), + Err(err) => last_error = Some(err.into()), + } + } + + Err(last_error.unwrap()) + } + + fn update_cursor( + &self, + window: xproto::Window, + cursor: xproto::Cursor, + ) -> Result<(), X11Error> { + self.xcb_connection() + .change_window_attributes( + window, + &xproto::ChangeWindowAttributesAux::new().cursor(cursor), + )? + .ignore_error(); + + self.xcb_connection().flush()?; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SelectedCursor { + Custom(CustomCursor), + Named(CursorIcon), +} + +#[derive(Debug, Clone)] +pub struct CustomCursor { + inner: Arc, +} + +impl Hash for CustomCursor { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.inner).hash(state); + } +} + +impl PartialEq for CustomCursor { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.inner, &other.inner) + } +} + +impl Eq for CustomCursor {} + +impl CustomCursor { + pub(crate) fn new( + event_loop: &ActiveEventLoop, + mut cursor: PlatformCustomCursorSource, + ) -> Result { + // Reverse RGBA order to BGRA. + cursor.0.rgba.chunks_mut(4).for_each(|chunk| { + let chunk: &mut [u8; 4] = chunk.try_into().unwrap(); + chunk[0..3].reverse(); + + // Byteswap if we need to. + if event_loop.xconn.needs_endian_swap() { + let value = u32::from_ne_bytes(*chunk).swap_bytes(); + *chunk = value.to_ne_bytes(); + } + }); + + let cursor = event_loop + .xconn + .create_cursor_from_image( + cursor.0.width, + cursor.0.height, + cursor.0.hotspot_x, + cursor.0.hotspot_y, + &cursor.0.rgba, + ) + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err.into()))))?; + + Ok(Self { + inner: Arc::new(CustomCursorInner { + xconn: event_loop.xconn.clone(), + cursor, + }), + }) + } +} + +#[derive(Debug)] +struct CustomCursorInner { + xconn: Arc, + cursor: xproto::Cursor, +} + +impl Drop for CustomCursorInner { + fn drop(&mut self) { + self.xconn + .xcb_connection() + .free_cursor(self.cursor) + .map(|r| r.ignore_error()) + .ok(); + } +} + +impl Default for SelectedCursor { + fn default() -> Self { + SelectedCursor::Named(Default::default()) + } +} diff --git a/rio-window/src/platform_impl/linux/x11/util/geometry.rs b/rio-window/src/platform_impl/linux/x11/util/geometry.rs new file mode 100644 index 00000000..e31caab0 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/geometry.rs @@ -0,0 +1,328 @@ +use std::cmp; + +use super::*; + +// Friendly neighborhood axis-aligned rectangle +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AaRect { + x: i64, + y: i64, + width: i64, + height: i64, +} + +impl AaRect { + pub fn new((x, y): (i32, i32), (width, height): (u32, u32)) -> Self { + let (x, y) = (x as i64, y as i64); + let (width, height) = (width as i64, height as i64); + AaRect { + x, + y, + width, + height, + } + } + + pub fn contains_point(&self, x: i64, y: i64) -> bool { + x >= self.x + && x <= self.x + self.width + && y >= self.y + && y <= self.y + self.height + } + + pub fn get_overlapping_area(&self, other: &Self) -> i64 { + let x_overlap = cmp::max( + 0, + cmp::min(self.x + self.width, other.x + other.width) + - cmp::max(self.x, other.x), + ); + let y_overlap = cmp::max( + 0, + cmp::min(self.y + self.height, other.y + other.height) + - cmp::max(self.y, other.y), + ); + x_overlap * y_overlap + } +} + +#[derive(Debug, Clone)] +pub struct FrameExtents { + pub left: u32, + pub right: u32, + pub top: u32, + pub bottom: u32, +} + +impl FrameExtents { + pub fn new(left: u32, right: u32, top: u32, bottom: u32) -> Self { + FrameExtents { + left, + right, + top, + bottom, + } + } + + pub fn from_border(border: u32) -> Self { + Self::new(border, border, border, border) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FrameExtentsHeuristicPath { + Supported, + UnsupportedNested, + UnsupportedBordered, +} + +#[derive(Debug, Clone)] +pub struct FrameExtentsHeuristic { + pub frame_extents: FrameExtents, + pub heuristic_path: FrameExtentsHeuristicPath, +} + +impl FrameExtentsHeuristic { + pub fn inner_pos_to_outer(&self, x: i32, y: i32) -> (i32, i32) { + use self::FrameExtentsHeuristicPath::*; + if self.heuristic_path != UnsupportedBordered { + ( + x - self.frame_extents.left as i32, + y - self.frame_extents.top as i32, + ) + } else { + (x, y) + } + } + + pub fn inner_size_to_outer(&self, width: u32, height: u32) -> (u32, u32) { + ( + width.saturating_add( + self.frame_extents + .left + .saturating_add(self.frame_extents.right) as _, + ), + height.saturating_add( + self.frame_extents + .top + .saturating_add(self.frame_extents.bottom) as _, + ), + ) + } +} + +impl XConnection { + // This is adequate for inner_position + pub fn translate_coords( + &self, + window: xproto::Window, + root: xproto::Window, + ) -> Result { + self.xcb_connection() + .translate_coordinates(window, root, 0, 0)? + .reply() + .map_err(Into::into) + } + + // This is adequate for inner_size + pub fn get_geometry( + &self, + window: xproto::Window, + ) -> Result { + self.xcb_connection() + .get_geometry(window)? + .reply() + .map_err(Into::into) + } + + fn get_frame_extents(&self, window: xproto::Window) -> Option { + let atoms = self.atoms(); + let extents_atom = atoms[_NET_FRAME_EXTENTS]; + + if !hint_is_supported(extents_atom) { + return None; + } + + // Of the WMs tested, xmonad, i3, dwm, IceWM (1.3.x and earlier), and blackbox don't + // support this. As this is part of EWMH (Extended Window Manager Hints), it's likely to + // be unsupported by many smaller WMs. + let extents: Option> = self + .get_property( + window, + extents_atom, + xproto::Atom::from(xproto::AtomEnum::CARDINAL), + ) + .ok(); + + extents.and_then(|extents| { + if extents.len() >= 4 { + Some(FrameExtents { + left: extents[0], + right: extents[1], + top: extents[2], + bottom: extents[3], + }) + } else { + None + } + }) + } + + pub fn is_top_level( + &self, + window: xproto::Window, + root: xproto::Window, + ) -> Option { + let atoms = self.atoms(); + let client_list_atom = atoms[_NET_CLIENT_LIST]; + + if !hint_is_supported(client_list_atom) { + return None; + } + + let client_list: Option> = self + .get_property( + root, + client_list_atom, + xproto::Atom::from(xproto::AtomEnum::WINDOW), + ) + .ok(); + + client_list.map(|client_list| client_list.contains(&(window as xproto::Window))) + } + + fn get_parent_window( + &self, + window: xproto::Window, + ) -> Result { + let parent = self.xcb_connection().query_tree(window)?.reply()?.parent; + Ok(parent) + } + + fn climb_hierarchy( + &self, + window: xproto::Window, + root: xproto::Window, + ) -> Result { + let mut outer_window = window; + loop { + let candidate = self.get_parent_window(outer_window)?; + if candidate == root { + break; + } + outer_window = candidate; + } + Ok(outer_window) + } + + pub fn get_frame_extents_heuristic( + &self, + window: xproto::Window, + root: xproto::Window, + ) -> FrameExtentsHeuristic { + use self::FrameExtentsHeuristicPath::*; + + // Position relative to root window. + // With rare exceptions, this is the position of a nested window. Cases where the window + // isn't nested are outlined in the comments throughout this function, but in addition to + // that, fullscreen windows often aren't nested. + let (inner_y_rel_root, child) = { + let coords = self + .translate_coords(window, root) + .expect("Failed to translate window coordinates"); + (coords.dst_y, coords.child) + }; + + let (width, height, border) = { + let inner_geometry = self + .get_geometry(window) + .expect("Failed to get inner window geometry"); + ( + inner_geometry.width, + inner_geometry.height, + inner_geometry.border_width, + ) + }; + + // The first condition is only false for un-nested windows, but isn't always false for + // un-nested windows. Mutter/Muffin/Budgie and Marco present a mysterious discrepancy: + // when y is on the range [0, 2] and if the window has been unfocused since being + // undecorated (or was undecorated upon construction), the first condition is true, + // requiring us to rely on the second condition. + let nested = !(window == child || self.is_top_level(child, root) == Some(true)); + + // Hopefully the WM supports EWMH, allowing us to get exact info on the window frames. + if let Some(mut frame_extents) = self.get_frame_extents(window) { + // Mutter/Muffin/Budgie and Marco preserve their decorated frame extents when + // decorations are disabled, but since the window becomes un-nested, it's easy to + // catch. + if !nested { + frame_extents = FrameExtents::new(0, 0, 0, 0); + } + + // The difference between the nested window's position and the outermost window's + // position is equivalent to the frame size. In most scenarios, this is equivalent to + // manually climbing the hierarchy as is done in the case below. Here's a list of + // known discrepancies: + // * Mutter/Muffin/Budgie gives decorated windows a margin of 9px (only 7px on top) in + // addition to a 1px semi-transparent border. The margin can be easily observed by + // using a screenshot tool to get a screenshot of a selected window, and is presumably + // used for drawing drop shadows. Getting window geometry information via + // hierarchy-climbing results in this margin being included in both the position and + // outer size, so a window positioned at (0, 0) would be reported as having a position + // (-10, -8). + // * Compiz has a drop shadow margin just like Mutter/Muffin/Budgie, though it's 10px on + // all sides, and there's no additional border. + // * Enlightenment otherwise gets a y position equivalent to inner_y_rel_root. Without + // decorations, there's no difference. This is presumably related to Enlightenment's + // fairly unique concept of window position; it interprets positions given to + // XMoveWindow as a client area position rather than a position of the overall window. + + FrameExtentsHeuristic { + frame_extents, + heuristic_path: Supported, + } + } else if nested { + // If the position value we have is for a nested window used as the client area, we'll + // just climb up the hierarchy and get the geometry of the outermost window we're + // nested in. + let outer_window = self + .climb_hierarchy(window, root) + .expect("Failed to climb window hierarchy"); + let (outer_y, outer_width, outer_height) = { + let outer_geometry = self + .get_geometry(outer_window) + .expect("Failed to get outer window geometry"); + ( + outer_geometry.y, + outer_geometry.width, + outer_geometry.height, + ) + }; + + // Since we have the geometry of the outermost window and the geometry of the client + // area, we can figure out what's in between. + let diff_x = outer_width.saturating_sub(width) as u32; + let diff_y = outer_height.saturating_sub(height) as u32; + let offset_y = inner_y_rel_root.saturating_sub(outer_y) as u32; + + let left = diff_x / 2; + let right = left; + let top = offset_y; + let bottom = diff_y.saturating_sub(offset_y); + + let frame_extents = FrameExtents::new(left, right, top, bottom); + FrameExtentsHeuristic { + frame_extents, + heuristic_path: UnsupportedNested, + } + } else { + // This is the case for xmonad and dwm, AKA the only WMs tested that supplied a + // border value. This is convenient, since we can use it to get an accurate frame. + let frame_extents = FrameExtents::from_border(border.into()); + FrameExtentsHeuristic { + frame_extents, + heuristic_path: UnsupportedBordered, + } + } + } +} diff --git a/rio-window/src/platform_impl/linux/x11/util/hint.rs b/rio-window/src/platform_impl/linux/x11/util/hint.rs new file mode 100644 index 00000000..ae95ea12 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/hint.rs @@ -0,0 +1,175 @@ +use crate::platform::x11::WindowType; +use std::sync::Arc; + +use super::*; + +#[derive(Debug)] +#[allow(dead_code)] +pub enum StateOperation { + Remove = 0, // _NET_WM_STATE_REMOVE + Add = 1, // _NET_WM_STATE_ADD + Toggle = 2, // _NET_WM_STATE_TOGGLE +} + +impl From for StateOperation { + fn from(op: bool) -> Self { + if op { + StateOperation::Add + } else { + StateOperation::Remove + } + } +} + +impl WindowType { + pub(crate) fn as_atom(&self, xconn: &Arc) -> xproto::Atom { + use self::WindowType::*; + let atom_name = match *self { + Desktop => _NET_WM_WINDOW_TYPE_DESKTOP, + Dock => _NET_WM_WINDOW_TYPE_DOCK, + Toolbar => _NET_WM_WINDOW_TYPE_TOOLBAR, + Menu => _NET_WM_WINDOW_TYPE_MENU, + Utility => _NET_WM_WINDOW_TYPE_UTILITY, + Splash => _NET_WM_WINDOW_TYPE_SPLASH, + Dialog => _NET_WM_WINDOW_TYPE_DIALOG, + DropdownMenu => _NET_WM_WINDOW_TYPE_DROPDOWN_MENU, + PopupMenu => _NET_WM_WINDOW_TYPE_POPUP_MENU, + Tooltip => _NET_WM_WINDOW_TYPE_TOOLTIP, + Notification => _NET_WM_WINDOW_TYPE_NOTIFICATION, + Combo => _NET_WM_WINDOW_TYPE_COMBO, + Dnd => _NET_WM_WINDOW_TYPE_DND, + Normal => _NET_WM_WINDOW_TYPE_NORMAL, + }; + + let atoms = xconn.atoms(); + atoms[atom_name] + } +} + +pub struct MotifHints { + hints: MwmHints, +} + +struct MwmHints { + flags: u32, + functions: u32, + decorations: u32, + input_mode: u32, + status: u32, +} + +#[allow(dead_code)] +mod mwm { + // Motif WM hints are obsolete, but still widely supported. + // https://stackoverflow.com/a/1909708 + pub const MWM_HINTS_FUNCTIONS: u32 = 1 << 0; + pub const MWM_HINTS_DECORATIONS: u32 = 1 << 1; + + pub const MWM_FUNC_ALL: u32 = 1 << 0; + pub const MWM_FUNC_RESIZE: u32 = 1 << 1; + pub const MWM_FUNC_MOVE: u32 = 1 << 2; + pub const MWM_FUNC_MINIMIZE: u32 = 1 << 3; + pub const MWM_FUNC_MAXIMIZE: u32 = 1 << 4; + pub const MWM_FUNC_CLOSE: u32 = 1 << 5; +} + +impl MotifHints { + pub fn new() -> MotifHints { + MotifHints { + hints: MwmHints { + flags: 0, + functions: 0, + decorations: 0, + input_mode: 0, + status: 0, + }, + } + } + + pub fn set_decorations(&mut self, decorations: bool) { + self.hints.flags |= mwm::MWM_HINTS_DECORATIONS; + self.hints.decorations = decorations as u32; + } + + pub fn set_maximizable(&mut self, maximizable: bool) { + if maximizable { + self.add_func(mwm::MWM_FUNC_MAXIMIZE); + } else { + self.remove_func(mwm::MWM_FUNC_MAXIMIZE); + } + } + + fn add_func(&mut self, func: u32) { + if self.hints.flags & mwm::MWM_HINTS_FUNCTIONS != 0 { + if self.hints.functions & mwm::MWM_FUNC_ALL != 0 { + self.hints.functions &= !func; + } else { + self.hints.functions |= func; + } + } + } + + fn remove_func(&mut self, func: u32) { + if self.hints.flags & mwm::MWM_HINTS_FUNCTIONS == 0 { + self.hints.flags |= mwm::MWM_HINTS_FUNCTIONS; + self.hints.functions = mwm::MWM_FUNC_ALL; + } + + if self.hints.functions & mwm::MWM_FUNC_ALL != 0 { + self.hints.functions |= func; + } else { + self.hints.functions &= !func; + } + } +} + +impl Default for MotifHints { + fn default() -> Self { + Self::new() + } +} + +impl XConnection { + pub fn get_motif_hints(&self, window: xproto::Window) -> MotifHints { + let atoms = self.atoms(); + let motif_hints = atoms[_MOTIF_WM_HINTS]; + + let mut hints = MotifHints::new(); + + if let Ok(props) = self.get_property::(window, motif_hints, motif_hints) { + hints.hints.flags = props.first().cloned().unwrap_or(0); + hints.hints.functions = props.get(1).cloned().unwrap_or(0); + hints.hints.decorations = props.get(2).cloned().unwrap_or(0); + hints.hints.input_mode = props.get(3).cloned().unwrap_or(0); + hints.hints.status = props.get(4).cloned().unwrap_or(0); + } + + hints + } + + #[allow(clippy::unnecessary_cast)] + pub fn set_motif_hints( + &self, + window: xproto::Window, + hints: &MotifHints, + ) -> Result, X11Error> { + let atoms = self.atoms(); + let motif_hints = atoms[_MOTIF_WM_HINTS]; + + let hints_data: [u32; 5] = [ + hints.hints.flags as u32, + hints.hints.functions as u32, + hints.hints.decorations as u32, + hints.hints.input_mode as u32, + hints.hints.status as u32, + ]; + + self.change_property( + window, + motif_hints, + motif_hints, + xproto::PropMode::REPLACE, + &hints_data, + ) + } +} diff --git a/rio-window/src/platform_impl/linux/x11/util/icon.rs b/rio-window/src/platform_impl/linux/x11/util/icon.rs new file mode 100644 index 00000000..07b5fee6 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/icon.rs @@ -0,0 +1,36 @@ +#![allow(clippy::assertions_on_constants)] + +use super::*; +use crate::icon::{Pixel, RgbaIcon, PIXEL_SIZE}; + +impl Pixel { + pub fn to_packed_argb(&self) -> Cardinal { + let mut cardinal = 0; + assert!(CARDINAL_SIZE >= PIXEL_SIZE); + let as_bytes = &mut cardinal as *mut _ as *mut u8; + unsafe { + *as_bytes.offset(0) = self.b; + *as_bytes.offset(1) = self.g; + *as_bytes.offset(2) = self.r; + *as_bytes.offset(3) = self.a; + } + cardinal + } +} + +impl RgbaIcon { + pub(crate) fn to_cardinals(&self) -> Vec { + assert_eq!(self.rgba.len() % PIXEL_SIZE, 0); + let pixel_count = self.rgba.len() / PIXEL_SIZE; + assert_eq!(pixel_count, (self.width * self.height) as usize); + let mut data = Vec::with_capacity(pixel_count); + data.push(self.width as Cardinal); + data.push(self.height as Cardinal); + let pixels = self.rgba.as_ptr() as *const Pixel; + for pixel_index in 0..pixel_count { + let pixel = unsafe { &*pixels.add(pixel_index) }; + data.push(pixel.to_packed_argb()); + } + data + } +} diff --git a/rio-window/src/platform_impl/linux/x11/util/input.rs b/rio-window/src/platform_impl/linux/x11/util/input.rs new file mode 100644 index 00000000..52461dfe --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/input.rs @@ -0,0 +1,116 @@ +use std::{slice, str}; +use x11rb::protocol::xinput::{self, ConnectionExt as _}; +use x11rb::protocol::xkb; + +use super::*; + +pub const VIRTUAL_CORE_POINTER: u16 = 2; +pub const VIRTUAL_CORE_KEYBOARD: u16 = 3; + +// A base buffer size of 1kB uses a negligible amount of RAM while preventing us from having to +// re-allocate (and make another round-trip) in the *vast* majority of cases. +// To test if `lookup_utf8` works correctly, set this to 1. +const TEXT_BUFFER_SIZE: usize = 1024; + +impl XConnection { + pub fn select_xinput_events( + &self, + window: xproto::Window, + device_id: u16, + mask: xinput::XIEventMask, + ) -> Result, X11Error> { + self.xcb_connection() + .xinput_xi_select_events( + window, + &[xinput::EventMask { + deviceid: device_id, + mask: vec![mask], + }], + ) + .map_err(Into::into) + } + + pub fn select_xkb_events( + &self, + device_id: xkb::DeviceSpec, + mask: xkb::EventType, + ) -> Result { + let mask = u16::from(mask) as _; + let status = unsafe { + (self.xlib.XkbSelectEvents)(self.display, device_id as _, mask, mask) + }; + + if status == ffi::True { + self.flush_requests()?; + Ok(true) + } else { + tracing::error!( + "Could not select XKB events: The XKB extension is not initialized!" + ); + Ok(false) + } + } + + pub fn query_pointer( + &self, + window: xproto::Window, + device_id: u16, + ) -> Result { + self.xcb_connection() + .xinput_xi_query_pointer(window, device_id)? + .reply() + .map_err(Into::into) + } + + fn lookup_utf8_inner( + &self, + ic: ffi::XIC, + key_event: &mut ffi::XKeyEvent, + buffer: *mut u8, + size: usize, + ) -> (ffi::KeySym, ffi::Status, c_int) { + let mut keysym: ffi::KeySym = 0; + let mut status: ffi::Status = 0; + let count = unsafe { + (self.xlib.Xutf8LookupString)( + ic, + key_event, + buffer as *mut c_char, + size as c_int, + &mut keysym, + &mut status, + ) + }; + (keysym, status, count) + } + + pub fn lookup_utf8(&self, ic: ffi::XIC, key_event: &mut ffi::XKeyEvent) -> String { + // `assume_init` is safe here because the array consists of `MaybeUninit` values, + // which do not require initialization. + let mut buffer: [MaybeUninit; TEXT_BUFFER_SIZE] = + unsafe { MaybeUninit::uninit().assume_init() }; + // If the buffer overflows, we'll make a new one on the heap. + let mut vec; + + let (_, status, count) = self.lookup_utf8_inner( + ic, + key_event, + buffer.as_mut_ptr() as *mut u8, + buffer.len(), + ); + + let bytes = if status == ffi::XBufferOverflow { + vec = Vec::with_capacity(count as usize); + let (_, _, new_count) = + self.lookup_utf8_inner(ic, key_event, vec.as_mut_ptr(), vec.capacity()); + debug_assert_eq!(count, new_count); + + unsafe { vec.set_len(count as usize) }; + &vec[..count as usize] + } else { + unsafe { slice::from_raw_parts(buffer.as_ptr() as *const u8, count as usize) } + }; + + str::from_utf8(bytes).unwrap_or("").to_string() + } +} diff --git a/rio-window/src/platform_impl/linux/x11/util/keys.rs b/rio-window/src/platform_impl/linux/x11/util/keys.rs new file mode 100644 index 00000000..8dfaab30 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/keys.rs @@ -0,0 +1,79 @@ +use std::iter::Enumerate; +use std::slice::Iter; + +use super::*; + +pub struct Keymap { + keys: [u8; 32], +} + +pub struct KeymapIter<'a> { + iter: Enumerate>, + index: usize, + item: Option, +} + +impl Keymap { + pub fn iter(&self) -> KeymapIter<'_> { + KeymapIter { + iter: self.keys.iter().enumerate(), + index: 0, + item: None, + } + } +} + +impl<'a> IntoIterator for &'a Keymap { + type IntoIter = KeymapIter<'a>; + type Item = ffi::KeyCode; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl Iterator for KeymapIter<'_> { + type Item = ffi::KeyCode; + + fn next(&mut self) -> Option { + if self.item.is_none() { + for (index, &item) in self.iter.by_ref() { + if item != 0 { + self.index = index; + self.item = Some(item); + break; + } + } + } + + self.item.take().map(|item| { + debug_assert!(item != 0); + + let bit = first_bit(item); + + if item != bit { + // Remove the first bit; save the rest for further iterations + self.item = Some(item ^ bit); + } + + let shift = bit.trailing_zeros() + (self.index * 8) as u32; + shift as ffi::KeyCode + }) + } +} + +impl XConnection { + pub fn query_keymap(&self) -> Keymap { + let mut keys = [0; 32]; + + unsafe { + (self.xlib.XQueryKeymap)(self.display, keys.as_mut_ptr() as *mut c_char); + } + + Keymap { keys } + } +} + +fn first_bit(b: u8) -> u8 { + 1 << b.trailing_zeros() +} diff --git a/rio-window/src/platform_impl/linux/x11/util/memory.rs b/rio-window/src/platform_impl/linux/x11/util/memory.rs new file mode 100644 index 00000000..d32eb8ce --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/memory.rs @@ -0,0 +1,42 @@ +use std::ops::{Deref, DerefMut}; + +use super::*; + +pub(crate) struct XSmartPointer<'a, T> { + xconn: &'a XConnection, + pub ptr: *mut T, +} + +impl<'a, T> XSmartPointer<'a, T> { + // You're responsible for only passing things to this that should be XFree'd. + // Returns None if ptr is null. + pub fn new(xconn: &'a XConnection, ptr: *mut T) -> Option { + if !ptr.is_null() { + Some(XSmartPointer { xconn, ptr }) + } else { + None + } + } +} + +impl<'a, T> Deref for XSmartPointer<'a, T> { + type Target = T; + + fn deref(&self) -> &T { + unsafe { &*self.ptr } + } +} + +impl<'a, T> DerefMut for XSmartPointer<'a, T> { + fn deref_mut(&mut self) -> &mut T { + unsafe { &mut *self.ptr } + } +} + +impl<'a, T> Drop for XSmartPointer<'a, T> { + fn drop(&mut self) { + unsafe { + (self.xconn.xlib.XFree)(self.ptr as *mut _); + } + } +} diff --git a/rio-window/src/platform_impl/linux/x11/util/mod.rs b/rio-window/src/platform_impl/linux/x11/util/mod.rs new file mode 100644 index 00000000..5c30293c --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/mod.rs @@ -0,0 +1,77 @@ +// Welcome to the util module, where we try to keep you from shooting yourself in the foot. +// *results may vary + +use std::mem::{self, MaybeUninit}; +use std::ops::BitAnd; +use std::os::raw::*; + +mod client_msg; +pub mod cookie; +mod cursor; +mod geometry; +mod hint; +mod icon; +mod input; +pub mod keys; +pub(crate) mod memory; +mod mouse; +mod randr; +mod window_property; +mod wm; +mod xmodmap; + +pub use self::cursor::*; +pub use self::geometry::*; +pub use self::hint::*; +pub use self::input::*; +pub use self::mouse::*; +pub use self::window_property::*; +pub use self::wm::*; +pub use self::xmodmap::ModifierKeymap; + +use super::atoms::*; +use super::{ffi, VoidCookie, X11Error, XConnection, XError}; +use x11rb::protocol::xproto::{self, ConnectionExt as _}; + +pub fn maybe_change(field: &mut Option, value: T) -> bool { + let wrapped = Some(value); + if *field != wrapped { + *field = wrapped; + true + } else { + false + } +} + +pub fn has_flag(bitset: T, flag: T) -> bool +where + T: Copy + PartialEq + BitAnd, +{ + bitset & flag == flag +} + +impl XConnection { + // This is impoartant, so pay attention! + // Xlib has an output buffer, and tries to hide the async nature of X from you. + // This buffer contains the requests you make, and is flushed under various circumstances: + // 1. `XPending`, `XNextEvent`, and `XWindowEvent` flush "as needed" + // 2. `XFlush` explicitly flushes + // 3. `XSync` flushes and blocks until all requests are responded to + // 4. Calls that have a return dependent on a response (i.e. `XGetWindowProperty`) sync + // internally. When in doubt, check the X11 source; if a function calls `_XReply`, it flushes + // and waits. + // All util functions that abstract an async function will return a `Flusher`. + pub fn flush_requests(&self) -> Result<(), XError> { + unsafe { (self.xlib.XFlush)(self.display) }; + // println!("XFlush"); + // This isn't necessarily a useful time to check for errors (since our request hasn't + // necessarily been processed yet) + self.check_errors() + } + + pub fn sync_with_server(&self) -> Result<(), XError> { + unsafe { (self.xlib.XSync)(self.display, ffi::False) }; + // println!("XSync"); + self.check_errors() + } +} diff --git a/rio-window/src/platform_impl/linux/x11/util/modifiers.rs b/rio-window/src/platform_impl/linux/x11/util/modifiers.rs new file mode 100644 index 00000000..bb157e44 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/modifiers.rs @@ -0,0 +1,187 @@ +use std::{collections::HashMap, slice}; + +use super::*; + +use crate::event::{ElementState, ModifiersState}; + +// Offsets within XModifierKeymap to each set of keycodes. +// We are only interested in Shift, Control, Alt, and Logo. +// +// There are 8 sets total. The order of keycode sets is: +// Shift, Lock, Control, Mod1 (Alt), Mod2, Mod3, Mod4 (Logo), Mod5 +// +// https://tronche.com/gui/x/xlib/input/XSetModifierMapping.html +const SHIFT_OFFSET: usize = 0; +const CONTROL_OFFSET: usize = 2; +const ALT_OFFSET: usize = 3; +const LOGO_OFFSET: usize = 6; +const NUM_MODS: usize = 8; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Modifier { + Alt, + Ctrl, + Shift, + Logo, +} + +#[derive(Debug, Default)] +pub(crate) struct ModifierKeymap { + // Maps keycodes to modifiers + keys: HashMap, +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct ModifierKeyState { + // Contains currently pressed modifier keys and their corresponding modifiers + keys: HashMap, + state: ModifiersState, +} + +impl ModifierKeymap { + pub fn new() -> ModifierKeymap { + ModifierKeymap::default() + } + + pub fn get_modifier(&self, keycode: ffi::KeyCode) -> Option { + self.keys.get(&keycode).cloned() + } + + pub fn reset_from_x_connection(&mut self, xconn: &XConnection) { + { + let keymap = xconn.xcb_connection().get_modifier_mapping().expect("get_modifier_mapping failed").reply().expect("get_modifier_mapping failed"); + + if keymap.is_null() { + panic!("failed to allocate XModifierKeymap"); + } + + self.reset_from_x_keymap(&*keymap); + + (xconn.xlib.XFreeModifiermap)(keymap); + } + } + + pub fn reset_from_x_keymap(&mut self, keymap: &ffi::XModifierKeymap) { + let keys_per_mod = keymap.max_keypermod as usize; + + let keys = unsafe { + slice::from_raw_parts(keymap.modifiermap as *const _, keys_per_mod * NUM_MODS) + }; + + self.keys.clear(); + + self.read_x_keys(keys, SHIFT_OFFSET, keys_per_mod, Modifier::Shift); + self.read_x_keys(keys, CONTROL_OFFSET, keys_per_mod, Modifier::Ctrl); + self.read_x_keys(keys, ALT_OFFSET, keys_per_mod, Modifier::Alt); + self.read_x_keys(keys, LOGO_OFFSET, keys_per_mod, Modifier::Logo); + } + + fn read_x_keys( + &mut self, + keys: &[ffi::KeyCode], + offset: usize, + keys_per_mod: usize, + modifier: Modifier, + ) { + let start = offset * keys_per_mod; + let end = start + keys_per_mod; + + for &keycode in &keys[start..end] { + if keycode != 0 { + self.keys.insert(keycode, modifier); + } + } + } +} + +impl ModifierKeyState { + pub fn update_keymap(&mut self, mods: &ModifierKeymap) { + self.keys.retain(|k, v| { + if let Some(m) = mods.get_modifier(*k) { + *v = m; + true + } else { + false + } + }); + + self.reset_state(); + } + + pub fn update_state( + &mut self, + state: &ModifiersState, + except: Option, + ) -> Option { + let mut new_state = *state; + + match except { + Some(Modifier::Alt) => new_state.set(ModifiersState::ALT, self.state.alt()), + Some(Modifier::Ctrl) => new_state.set(ModifiersState::CTRL, self.state.ctrl()), + Some(Modifier::Shift) => new_state.set(ModifiersState::SHIFT, self.state.shift()), + Some(Modifier::Logo) => new_state.set(ModifiersState::LOGO, self.state.logo()), + None => (), + } + + if self.state == new_state { + None + } else { + self.keys.retain(|_k, v| get_modifier(&new_state, *v)); + self.state = new_state; + Some(new_state) + } + } + + pub fn modifiers(&self) -> ModifiersState { + self.state + } + + pub fn key_event(&mut self, state: ElementState, keycode: ffi::KeyCode, modifier: Modifier) { + match state { + ElementState::Pressed => self.key_press(keycode, modifier), + ElementState::Released => self.key_release(keycode), + } + } + + pub fn key_press(&mut self, keycode: ffi::KeyCode, modifier: Modifier) { + self.keys.insert(keycode, modifier); + + set_modifier(&mut self.state, modifier, true); + } + + pub fn key_release(&mut self, keycode: ffi::KeyCode) { + if let Some(modifier) = self.keys.remove(&keycode) { + if !self.keys.values().any(|&m| m == modifier) { + set_modifier(&mut self.state, modifier, false); + } + } + } + + fn reset_state(&mut self) { + let mut new_state = ModifiersState::default(); + + for &m in self.keys.values() { + set_modifier(&mut new_state, m, true); + } + + self.state = new_state; + } +} + +fn get_modifier(state: &ModifiersState, modifier: Modifier) -> bool { + match modifier { + Modifier::Alt => state.alt(), + Modifier::Ctrl => state.ctrl(), + Modifier::Shift => state.shift(), + Modifier::Logo => state.logo(), + } +} + +fn set_modifier(state: &mut ModifiersState, modifier: Modifier, value: bool) { + match modifier { + Modifier::Alt => state.set(ModifiersState::ALT, value), + Modifier::Ctrl => state.set(ModifiersState::CTRL, value), + Modifier::Shift => state.set(ModifiersState::SHIFT, value), + Modifier::Logo => state.set(ModifiersState::LOGO, value), + } +} diff --git a/rio-window/src/platform_impl/linux/x11/util/mouse.rs b/rio-window/src/platform_impl/linux/x11/util/mouse.rs new file mode 100644 index 00000000..9ee4e1dd --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/mouse.rs @@ -0,0 +1,53 @@ +//! Utilities for handling mouse events. + +/// Recorded mouse delta designed to filter out noise. +pub struct Delta { + x: T, + y: T, +} + +impl Default for Delta { + fn default() -> Self { + Self { + x: Default::default(), + y: Default::default(), + } + } +} + +impl Delta { + pub(crate) fn set_x(&mut self, x: T) { + self.x = x; + } + + pub(crate) fn set_y(&mut self, y: T) { + self.y = y; + } +} + +macro_rules! consume { + ($this:expr, $ty:ty) => {{ + let this = $this; + let (x, y) = match (this.x.abs() < <$ty>::EPSILON, this.y.abs() < <$ty>::EPSILON) + { + (true, true) => return None, + (false, true) => (this.x, 0.0), + (true, false) => (0.0, this.y), + (false, false) => (this.x, this.y), + }; + + Some((x, y)) + }}; +} + +impl Delta { + pub(crate) fn consume(self) -> Option<(f32, f32)> { + consume!(self, f32) + } +} + +impl Delta { + pub(crate) fn consume(self) -> Option<(f64, f64)> { + consume!(self, f64) + } +} diff --git a/rio-window/src/platform_impl/linux/x11/util/randr.rs b/rio-window/src/platform_impl/linux/x11/util/randr.rs new file mode 100644 index 00000000..bbf5d69b --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/randr.rs @@ -0,0 +1,198 @@ +use std::str::FromStr; +use std::{env, str}; + +use super::*; +use crate::dpi::validate_scale_factor; +use crate::platform_impl::platform::x11::{monitor, VideoModeHandle}; + +use tracing::warn; +use x11rb::protocol::randr::{self, ConnectionExt as _}; + +/// Represents values of `WINIT_HIDPI_FACTOR`. +pub enum EnvVarDPI { + Randr, + Scale(f64), + NotSet, +} + +pub fn calc_dpi_factor( + (width_px, height_px): (u32, u32), + (width_mm, height_mm): (u64, u64), +) -> f64 { + // See http://xpra.org/trac/ticket/728 for more information. + if width_mm == 0 || height_mm == 0 { + warn!( + "XRandR reported that the display's 0mm in size, which is certifiably insane" + ); + return 1.0; + } + + let ppmm = ((width_px as f64 * height_px as f64) + / (width_mm as f64 * height_mm as f64)) + .sqrt(); + // Quantize 1/12 step size + let dpi_factor = ((ppmm * (12.0 * 25.4 / 96.0)).round() / 12.0).max(1.0); + assert!(validate_scale_factor(dpi_factor)); + if dpi_factor <= 20. { + dpi_factor + } else { + 1. + } +} + +impl XConnection { + // Retrieve DPI from Xft.dpi property + pub fn get_xft_dpi(&self) -> Option { + // Try to get it from XSETTINGS first. + if let Some(xsettings_screen) = self.xsettings_screen() { + match self.xsettings_dpi(xsettings_screen) { + Ok(Some(dpi)) => return Some(dpi), + Ok(None) => {} + Err(err) => { + tracing::warn!("failed to fetch XSettings: {err}"); + } + } + } + + self.database() + .get_string("Xft.dpi", "") + .and_then(|s| f64::from_str(s).ok()) + } + + pub fn get_output_info( + &self, + resources: &monitor::ScreenResources, + crtc: &randr::GetCrtcInfoReply, + ) -> Option<(String, f64, Vec)> { + let output_info = match self + .xcb_connection() + .randr_get_output_info(crtc.outputs[0], x11rb::CURRENT_TIME) + .map_err(X11Error::from) + .and_then(|r| r.reply().map_err(X11Error::from)) + { + Ok(output_info) => output_info, + Err(err) => { + warn!("Failed to get output info: {:?}", err); + return None; + } + }; + + let bit_depth = self.default_root().root_depth; + let output_modes = &output_info.modes; + let resource_modes = resources.modes(); + + let modes = resource_modes + .iter() + // XRROutputInfo contains an array of mode ids that correspond to + // modes in the array in XRRScreenResources + .filter(|x| output_modes.iter().any(|id| x.id == *id)) + .map(|mode| { + VideoModeHandle { + size: (mode.width.into(), mode.height.into()), + refresh_rate_millihertz: monitor::mode_refresh_rate_millihertz(mode) + .unwrap_or(0), + bit_depth: bit_depth as u16, + native_mode: mode.id, + // This is populated in `MonitorHandle::video_modes` as the + // video mode is returned to the user + monitor: None, + } + }) + .collect(); + + let name = match str::from_utf8(&output_info.name) { + Ok(name) => name.to_owned(), + Err(err) => { + warn!("Failed to get output name: {:?}", err); + return None; + } + }; + // Override DPI if `WINIT_X11_SCALE_FACTOR` variable is set + let deprecated_dpi_override = env::var("WINIT_HIDPI_FACTOR").ok(); + if deprecated_dpi_override.is_some() { + warn!( + "The WINIT_HIDPI_FACTOR environment variable is deprecated; use \ + WINIT_X11_SCALE_FACTOR" + ) + } + let dpi_env = env::var("WINIT_X11_SCALE_FACTOR").ok().map_or_else( + || EnvVarDPI::NotSet, + |var| { + if var.to_lowercase() == "randr" { + EnvVarDPI::Randr + } else if let Ok(dpi) = f64::from_str(&var) { + EnvVarDPI::Scale(dpi) + } else if var.is_empty() { + EnvVarDPI::NotSet + } else { + panic!( + "`WINIT_X11_SCALE_FACTOR` invalid; DPI factors must be either normal \ + floats greater than 0, or `randr`. Got `{var}`" + ); + } + }, + ); + + let scale_factor = match dpi_env { + EnvVarDPI::Randr => calc_dpi_factor( + (crtc.width.into(), crtc.height.into()), + (output_info.mm_width as _, output_info.mm_height as _), + ), + EnvVarDPI::Scale(dpi_override) => { + if !validate_scale_factor(dpi_override) { + panic!( + "`WINIT_X11_SCALE_FACTOR` invalid; DPI factors must be either normal \ + floats greater than 0, or `randr`. Got `{dpi_override}`", + ); + } + dpi_override + } + EnvVarDPI::NotSet => { + if let Some(dpi) = self.get_xft_dpi() { + dpi / 96. + } else { + calc_dpi_factor( + (crtc.width.into(), crtc.height.into()), + (output_info.mm_width as _, output_info.mm_height as _), + ) + } + } + }; + + Some((name, scale_factor, modes)) + } + + pub fn set_crtc_config( + &self, + crtc_id: randr::Crtc, + mode_id: randr::Mode, + ) -> Result<(), X11Error> { + let crtc = self + .xcb_connection() + .randr_get_crtc_info(crtc_id, x11rb::CURRENT_TIME)? + .reply()?; + + self.xcb_connection() + .randr_set_crtc_config( + crtc_id, + crtc.timestamp, + x11rb::CURRENT_TIME, + crtc.x, + crtc.y, + mode_id, + crtc.rotation, + &crtc.outputs, + )? + .reply() + .map(|_| ()) + .map_err(Into::into) + } + + pub fn get_crtc_mode(&self, crtc_id: randr::Crtc) -> Result { + Ok(self + .xcb_connection() + .randr_get_crtc_info(crtc_id, x11rb::CURRENT_TIME)? + .reply()? + .mode) + } +} diff --git a/rio-window/src/platform_impl/linux/x11/util/window_property.rs b/rio-window/src/platform_impl/linux/x11/util/window_property.rs new file mode 100644 index 00000000..f72086f2 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/window_property.rs @@ -0,0 +1,201 @@ +use std::error::Error; +use std::fmt; +use std::sync::Arc; + +use bytemuck::{NoUninit, Pod}; + +use x11rb::connection::Connection; +use x11rb::errors::ReplyError; + +use super::*; + +pub const CARDINAL_SIZE: usize = mem::size_of::(); + +pub type Cardinal = u32; + +#[derive(Debug, Clone)] +pub enum GetPropertyError { + X11rbError(Arc), + TypeMismatch(xproto::Atom), + FormatMismatch(c_int), +} + +impl GetPropertyError { + pub fn is_actual_property_type(&self, t: xproto::Atom) -> bool { + if let GetPropertyError::TypeMismatch(actual_type) = *self { + actual_type == t + } else { + false + } + } +} + +impl> From for GetPropertyError { + fn from(e: T) -> Self { + Self::X11rbError(Arc::new(e.into())) + } +} + +impl fmt::Display for GetPropertyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GetPropertyError::X11rbError(err) => err.fmt(f), + GetPropertyError::TypeMismatch(err) => write!(f, "type mismatch: {err}"), + GetPropertyError::FormatMismatch(err) => write!(f, "format mismatch: {err}"), + } + } +} + +impl Error for GetPropertyError {} + +// Number of 32-bit chunks to retrieve per iteration of get_property's inner loop. +// To test if `get_property` works correctly, set this to 1. +const PROPERTY_BUFFER_SIZE: u32 = 1024; // 4k of RAM ought to be enough for anyone! + +impl XConnection { + pub fn get_property( + &self, + window: xproto::Window, + property: xproto::Atom, + property_type: xproto::Atom, + ) -> Result, GetPropertyError> { + let mut iter = + PropIterator::new(self.xcb_connection(), window, property, property_type); + let mut data = vec![]; + + loop { + if !iter.next_window(&mut data)? { + break; + } + } + + Ok(data) + } + + pub fn change_property<'a, T: NoUninit>( + &'a self, + window: xproto::Window, + property: xproto::Atom, + property_type: xproto::Atom, + mode: xproto::PropMode, + new_value: &[T], + ) -> Result, X11Error> { + assert!([1usize, 2, 4].contains(&mem::size_of::())); + self.xcb_connection() + .change_property( + mode, + window, + property, + property_type, + (mem::size_of::() * 8) as u8, + new_value + .len() + .try_into() + .expect("too many items for property"), + bytemuck::cast_slice::(new_value), + ) + .map_err(Into::into) + } +} + +/// An iterator over the "windows" of the property that we are fetching. +struct PropIterator<'a, C: ?Sized, T> { + /// Handle to the connection. + conn: &'a C, + + /// The window that we're fetching the property from. + window: xproto::Window, + + /// The property that we're fetching. + property: xproto::Atom, + + /// The type of the property that we're fetching. + property_type: xproto::Atom, + + /// The offset of the next window, in 32-bit chunks. + offset: u32, + + /// The format of the type. + format: u8, + + /// Keep a reference to `T`. + _phantom: std::marker::PhantomData, +} + +impl<'a, C: Connection + ?Sized, T: Pod> PropIterator<'a, C, T> { + /// Create a new property iterator. + fn new( + conn: &'a C, + window: xproto::Window, + property: xproto::Atom, + property_type: xproto::Atom, + ) -> Self { + let format = match mem::size_of::() { + 1 => 8, + 2 => 16, + 4 => 32, + _ => unreachable!(), + }; + + Self { + conn, + window, + property, + property_type, + offset: 0, + format, + _phantom: Default::default(), + } + } + + /// Get the next window and append it to `data`. + /// + /// Returns whether there are more windows to fetch. + fn next_window(&mut self, data: &mut Vec) -> Result { + // Send the request and wait for the reply. + let reply = self + .conn + .get_property( + false, + self.window, + self.property, + self.property_type, + self.offset, + PROPERTY_BUFFER_SIZE, + )? + .reply()?; + + // Make sure that the reply is of the correct type. + if reply.type_ != self.property_type { + return Err(GetPropertyError::TypeMismatch(reply.type_)); + } + + // Make sure that the reply is of the correct format. + if reply.format != self.format { + return Err(GetPropertyError::FormatMismatch(reply.format.into())); + } + + // Append the data to the output. + if mem::size_of::() == 1 && mem::align_of::() == 1 { + // We can just do a bytewise append. + data.extend_from_slice(bytemuck::cast_slice(&reply.value)); + } else { + // Rust's borrowing and types system makes this a bit tricky. + // + // We need to make sure that the data is properly aligned. Unfortunately the best + // safe way to do this is to copy the data to another buffer and then append. + // + // TODO(notgull): It may be worth it to use `unsafe` to copy directly from + // `reply.value` to `data`; check if this is faster. Use benchmarks! + let old_len = data.len(); + let added_len = reply.value.len() / mem::size_of::(); + data.resize(old_len + added_len, T::zeroed()); + bytemuck::cast_slice_mut::(&mut data[old_len..]) + .copy_from_slice(&reply.value); + } + + // Check `bytes_after` to see if there are more windows to fetch. + self.offset += PROPERTY_BUFFER_SIZE; + Ok(reply.bytes_after != 0) + } +} diff --git a/rio-window/src/platform_impl/linux/x11/util/wm.rs b/rio-window/src/platform_impl/linux/x11/util/wm.rs new file mode 100644 index 00000000..899d5b11 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/wm.rs @@ -0,0 +1,144 @@ +use std::sync::Mutex; + +use super::*; + +// https://specifications.freedesktop.org/wm-spec/latest/ar01s04.html#idm46075117309248 +pub const MOVERESIZE_TOPLEFT: isize = 0; +pub const MOVERESIZE_TOP: isize = 1; +pub const MOVERESIZE_TOPRIGHT: isize = 2; +pub const MOVERESIZE_RIGHT: isize = 3; +pub const MOVERESIZE_BOTTOMRIGHT: isize = 4; +pub const MOVERESIZE_BOTTOM: isize = 5; +pub const MOVERESIZE_BOTTOMLEFT: isize = 6; +pub const MOVERESIZE_LEFT: isize = 7; +pub const MOVERESIZE_MOVE: isize = 8; + +// This info is global to the window manager. +static SUPPORTED_HINTS: Mutex> = Mutex::new(Vec::new()); +static WM_NAME: Mutex> = Mutex::new(None); + +pub fn hint_is_supported(hint: xproto::Atom) -> bool { + (*SUPPORTED_HINTS.lock().unwrap()).contains(&hint) +} + +pub fn wm_name_is_one_of(names: &[&str]) -> bool { + if let Some(ref name) = *WM_NAME.lock().unwrap() { + names.contains(&name.as_str()) + } else { + false + } +} + +impl XConnection { + pub fn update_cached_wm_info(&self, root: xproto::Window) { + *SUPPORTED_HINTS.lock().unwrap() = self.get_supported_hints(root); + *WM_NAME.lock().unwrap() = self.get_wm_name(root); + } + + fn get_supported_hints(&self, root: xproto::Window) -> Vec { + let atoms = self.atoms(); + let supported_atom = atoms[_NET_SUPPORTED]; + self.get_property( + root, + supported_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + ) + .unwrap_or_else(|_| Vec::with_capacity(0)) + } + + #[allow(clippy::useless_conversion)] + fn get_wm_name(&self, root: xproto::Window) -> Option { + let atoms = self.atoms(); + let check_atom = atoms[_NET_SUPPORTING_WM_CHECK]; + let wm_name_atom = atoms[_NET_WM_NAME]; + + // Mutter/Muffin/Budgie doesn't have _NET_SUPPORTING_WM_CHECK in its _NET_SUPPORTED, despite + // it working and being supported. This has been reported upstream, but due to the + // inavailability of time machines, we'll just try to get _NET_SUPPORTING_WM_CHECK + // regardless of whether or not the WM claims to support it. + // + // Blackbox 0.70 also incorrectly reports not supporting this, though that appears to be + // fixed in 0.72. + // if !supported_hints.contains(&check_atom) { + // return None; + // } + + // IceWM (1.3.x and earlier) doesn't report supporting _NET_WM_NAME, but will nonetheless + // provide us with a value for it. Note that the unofficial 1.4 fork of IceWM works fine. + // if !supported_hints.contains(&wm_name_atom) { + // return None; + // } + + // Of the WMs tested, only xmonad and dwm fail to provide a WM name. + + // Querying this property on the root window will give us the ID of a child window created + // by the WM. + let root_window_wm_check = { + let result = self.get_property::( + root, + check_atom, + xproto::Atom::from(xproto::AtomEnum::WINDOW), + ); + + let wm_check = result.ok().and_then(|wm_check| wm_check.first().cloned()); + + wm_check? + }; + + // Querying the same property on the child window we were given, we should get this child + // window's ID again. + let child_window_wm_check = { + let result = self.get_property::( + root_window_wm_check.into(), + check_atom, + xproto::Atom::from(xproto::AtomEnum::WINDOW), + ); + + let wm_check = result.ok().and_then(|wm_check| wm_check.first().cloned()); + + wm_check? + }; + + // These values should be the same. + if root_window_wm_check != child_window_wm_check { + return None; + } + + // All of that work gives us a window ID that we can get the WM name from. + let wm_name = { + let atoms = self.atoms(); + let utf8_string_atom = atoms[UTF8_STRING]; + + let result = self.get_property( + root_window_wm_check.into(), + wm_name_atom, + utf8_string_atom, + ); + + // IceWM requires this. IceWM was also the only WM tested that returns a null-terminated + // string. For more fun trivia, IceWM is also unique in including version and uname + // information in this string (this means you'll have to be careful if you want to match + // against it, though). + // The unofficial 1.4 fork of IceWM still includes the extra details, but properly + // returns a UTF8 string that isn't null-terminated. + let no_utf8 = if let Err(ref err) = result { + err.is_actual_property_type(xproto::Atom::from(xproto::AtomEnum::STRING)) + } else { + false + }; + + if no_utf8 { + self.get_property( + root_window_wm_check.into(), + wm_name_atom, + xproto::Atom::from(xproto::AtomEnum::STRING), + ) + } else { + result + } + } + .ok(); + + wm_name.and_then(|wm_name| String::from_utf8(wm_name).ok()) + } +} diff --git a/rio-window/src/platform_impl/linux/x11/util/xmodmap.rs b/rio-window/src/platform_impl/linux/x11/util/xmodmap.rs new file mode 100644 index 00000000..565fd729 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/util/xmodmap.rs @@ -0,0 +1,56 @@ +use std::collections::HashSet; +use std::slice; + +use x11_dl::xlib::{KeyCode as XKeyCode, XModifierKeymap}; + +// Offsets within XModifierKeymap to each set of keycodes. +// We are only interested in Shift, Control, Alt, and Logo. +// +// There are 8 sets total. The order of keycode sets is: +// Shift, Lock, Control, Mod1 (Alt), Mod2, Mod3, Mod4 (Logo), Mod5 +// +// https://tronche.com/gui/x/xlib/input/XSetModifierMapping.html +const NUM_MODS: usize = 8; + +/// Track which keys are modifiers, so we can properly replay them when they were filtered. +#[derive(Debug, Default)] +pub struct ModifierKeymap { + // Maps keycodes to modifiers + modifiers: HashSet, +} + +impl ModifierKeymap { + pub fn new() -> ModifierKeymap { + ModifierKeymap::default() + } + + pub fn is_modifier(&self, keycode: XKeyCode) -> bool { + self.modifiers.contains(&keycode) + } + + pub fn reload_from_x_connection(&mut self, xconn: &super::XConnection) { + unsafe { + let keymap = (xconn.xlib.XGetModifierMapping)(xconn.display); + + if keymap.is_null() { + return; + } + + self.reset_from_x_keymap(&*keymap); + + (xconn.xlib.XFreeModifiermap)(keymap); + } + } + + fn reset_from_x_keymap(&mut self, keymap: &XModifierKeymap) { + let keys_per_mod = keymap.max_keypermod as usize; + + let keys = unsafe { + slice::from_raw_parts(keymap.modifiermap as *const _, keys_per_mod * NUM_MODS) + }; + self.modifiers.clear(); + for key in keys { + self.modifiers.insert(*key); + } + } +} diff --git a/rio-window/src/platform_impl/linux/x11/window.rs b/rio-window/src/platform_impl/linux/x11/window.rs new file mode 100644 index 00000000..7f86a9ad --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/window.rs @@ -0,0 +1,2063 @@ +use std::ffi::CString; +use std::mem::replace; +use std::os::raw::*; +use std::path::Path; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::{cmp, env}; + +use tracing::{debug, info, warn}; +use x11rb::connection::Connection; +use x11rb::properties::{WmHints, WmSizeHints, WmSizeHintsSpecification}; +use x11rb::protocol::shape::SK; +use x11rb::protocol::xfixes::{ConnectionExt, RegionWrapper}; +use x11rb::protocol::xproto::{self, ConnectionExt as _, Rectangle}; +use x11rb::protocol::{randr, xinput}; + +use crate::cursor::{Cursor, CustomCursor as RootCustomCursor}; +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event::{Event, InnerSizeWriter, WindowEvent}; +use crate::event_loop::AsyncRequestSerial; +use crate::platform::x11::WindowType; +use crate::platform_impl::x11::atoms::*; +use crate::platform_impl::x11::{ + xinput_fp1616_to_float, MonitorHandle as X11MonitorHandle, WakeSender, X11Error, +}; +use crate::platform_impl::{ + Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, PlatformCustomCursor, + PlatformIcon, VideoModeHandle as PlatformVideoModeHandle, +}; +use crate::window::{ + CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowLevel, +}; + +use super::util::{self, SelectedCursor}; +use super::{ + ffi, ActiveEventLoop, CookieResultExt, ImeRequest, ImeSender, VoidCookie, WindowId, + XConnection, +}; + +#[derive(Debug)] +pub struct SharedState { + pub cursor_pos: Option<(f64, f64)>, + pub size: Option<(u32, u32)>, + pub position: Option<(i32, i32)>, + pub inner_position: Option<(i32, i32)>, + pub inner_position_rel_parent: Option<(i32, i32)>, + pub is_resizable: bool, + pub is_decorated: bool, + pub last_monitor: X11MonitorHandle, + pub dpi_adjusted: Option<(u32, u32)>, + pub(crate) fullscreen: Option, + // Set when application calls `set_fullscreen` when window is not visible + pub(crate) desired_fullscreen: Option>, + // Used to restore position after exiting fullscreen + pub restore_position: Option<(i32, i32)>, + // Used to restore video mode after exiting fullscreen + pub desktop_video_mode: Option<(randr::Crtc, randr::Mode)>, + pub frame_extents: Option, + pub min_inner_size: Option, + pub max_inner_size: Option, + pub resize_increments: Option, + pub base_size: Option, + pub visibility: Visibility, + pub has_focus: bool, + // Use `Option` to not apply hittest logic when it was never requested. + pub cursor_hittest: Option, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Visibility { + No, + Yes, + // Waiting for VisibilityNotify + YesWait, +} + +impl SharedState { + fn new( + last_monitor: X11MonitorHandle, + window_attributes: &WindowAttributes, + ) -> Mutex { + let visibility = if window_attributes.visible { + Visibility::YesWait + } else { + Visibility::No + }; + + Mutex::new(SharedState { + last_monitor, + visibility, + + is_resizable: window_attributes.resizable, + is_decorated: window_attributes.decorations, + cursor_pos: None, + size: None, + position: None, + inner_position: None, + inner_position_rel_parent: None, + dpi_adjusted: None, + fullscreen: None, + desired_fullscreen: None, + restore_position: None, + desktop_video_mode: None, + frame_extents: None, + min_inner_size: None, + max_inner_size: None, + resize_increments: None, + base_size: None, + has_focus: false, + cursor_hittest: None, + }) + } +} + +unsafe impl Send for UnownedWindow {} +unsafe impl Sync for UnownedWindow {} + +pub struct UnownedWindow { + pub(crate) xconn: Arc, // never changes + xwindow: xproto::Window, // never changes + #[allow(dead_code)] + visual: u32, // never changes + root: xproto::Window, // never changes + #[allow(dead_code)] + screen_id: i32, // never changes + selected_cursor: Mutex, + cursor_grabbed_mode: Mutex, + #[allow(clippy::mutex_atomic)] + cursor_visible: Mutex, + ime_sender: Mutex, + pub shared_state: Mutex, + redraw_sender: WakeSender, + activation_sender: WakeSender, +} + +macro_rules! leap { + ($e:expr) => { + match $e { + Ok(x) => x, + Err(err) => { + return Err(os_error!(OsError::XError(X11Error::from(err).into()))) + } + } + }; +} + +impl UnownedWindow { + #[allow(clippy::unnecessary_cast)] + pub(crate) fn new( + event_loop: &ActiveEventLoop, + window_attrs: WindowAttributes, + ) -> Result { + let xconn = &event_loop.xconn; + let atoms = xconn.atoms(); + #[cfg(feature = "rwh_06")] + let root = match window_attrs.parent_window.as_ref().map(|handle| handle.0) { + Some(rwh_06::RawWindowHandle::Xlib(handle)) => { + handle.window as xproto::Window + } + Some(rwh_06::RawWindowHandle::Xcb(handle)) => handle.window.get(), + Some(raw) => unreachable!("Invalid raw window handle {raw:?} on X11"), + None => event_loop.root, + }; + #[cfg(not(feature = "rwh_06"))] + let root = event_loop.root; + + let mut monitors = leap!(xconn.available_monitors()); + let guessed_monitor = if monitors.is_empty() { + X11MonitorHandle::dummy() + } else { + xconn + .query_pointer(root, util::VIRTUAL_CORE_POINTER) + .ok() + .and_then(|pointer_state| { + let (x, y) = + (pointer_state.root_x as i64, pointer_state.root_y as i64); + + for i in 0..monitors.len() { + if monitors[i].rect.contains_point(x, y) { + return Some(monitors.swap_remove(i)); + } + } + + None + }) + .unwrap_or_else(|| monitors.swap_remove(0)) + }; + let scale_factor = guessed_monitor.scale_factor(); + + info!("Guessed window scale factor: {}", scale_factor); + + let max_inner_size: Option<(u32, u32)> = window_attrs + .max_inner_size + .map(|size| size.to_physical::(scale_factor).into()); + let min_inner_size: Option<(u32, u32)> = window_attrs + .min_inner_size + .map(|size| size.to_physical::(scale_factor).into()); + + let position = window_attrs + .position + .map(|position| position.to_physical::(scale_factor)); + + let dimensions = { + // x11 only applies constraints when the window is actively resized + // by the user, so we have to manually apply the initial constraints + let mut dimensions: (u32, u32) = window_attrs + .inner_size + .map(|size| size.to_physical::(scale_factor)) + .or_else(|| Some((800, 600).into())) + .map(Into::into) + .unwrap(); + if let Some(max) = max_inner_size { + dimensions.0 = cmp::min(dimensions.0, max.0); + dimensions.1 = cmp::min(dimensions.1, max.1); + } + if let Some(min) = min_inner_size { + dimensions.0 = cmp::max(dimensions.0, min.0); + dimensions.1 = cmp::max(dimensions.1, min.1); + } + debug!( + "Calculated physical dimensions: {}x{}", + dimensions.0, dimensions.1 + ); + dimensions + }; + + let screen_id = match window_attrs.platform_specific.x11.screen_id { + Some(id) => id, + None => xconn.default_screen_index() as c_int, + }; + + // An iterator over all of the visuals combined with their depths. + let mut all_visuals = xconn + .xcb_connection() + .setup() + .roots + .iter() + .flat_map(|root| &root.allowed_depths) + .flat_map(|depth| { + depth + .visuals + .iter() + .map(move |visual| (visual, depth.depth)) + }); + + // creating + let (visualtype, depth, require_colormap) = match window_attrs + .platform_specific + .x11 + .visual_id + { + Some(vi) => { + // Find this specific visual. + let (visualtype, depth) = all_visuals + .find(|(visual, _)| visual.visual_id == vi) + .ok_or_else(|| { + os_error!(OsError::XError(X11Error::NoSuchVisual(vi).into())) + })?; + + (Some(visualtype), depth, true) + } + None if window_attrs.transparent => { + // Find a suitable visual, true color with 32 bits of depth. + all_visuals + .find_map(|(visual, depth)| { + (depth == 32 && visual.class == xproto::VisualClass::TRUE_COLOR) + .then_some((Some(visual), depth, true)) + }) + .unwrap_or_else(|| { + debug!( + "Could not set transparency, because XMatchVisualInfo returned \ + zero for the required parameters" + ); + (None as _, x11rb::COPY_FROM_PARENT as _, false) + }) + } + _ => (None, x11rb::COPY_FROM_PARENT as _, false), + }; + let mut visual = visualtype.map_or(x11rb::COPY_FROM_PARENT, |v| v.visual_id); + + let window_attributes = { + use xproto::EventMask; + + let mut aux = xproto::CreateWindowAux::new(); + let event_mask = EventMask::EXPOSURE + | EventMask::STRUCTURE_NOTIFY + | EventMask::VISIBILITY_CHANGE + | EventMask::KEY_PRESS + | EventMask::KEY_RELEASE + | EventMask::KEYMAP_STATE + | EventMask::BUTTON_PRESS + | EventMask::BUTTON_RELEASE + | EventMask::POINTER_MOTION + | EventMask::PROPERTY_CHANGE; + + aux = aux.event_mask(event_mask).border_pixel(0); + + if window_attrs.platform_specific.x11.override_redirect { + aux = aux.override_redirect(true as u32); + } + + // Add a colormap if needed. + let colormap_visual = match window_attrs.platform_specific.x11.visual_id { + Some(vi) => Some(vi), + None if require_colormap => Some(visual), + _ => None, + }; + + if let Some(visual) = colormap_visual { + let colormap = leap!(xconn.xcb_connection().generate_id()); + leap!(xconn.xcb_connection().create_colormap( + xproto::ColormapAlloc::NONE, + colormap, + root, + visual, + )); + aux = aux.colormap(colormap); + } else { + aux = aux.colormap(0); + } + + aux + }; + + // Figure out the window's parent. + let parent = window_attrs + .platform_specific + .x11 + .embed_window + .unwrap_or(root); + + // finally creating the window + let xwindow = { + let (x, y) = position.map_or((0, 0), Into::into); + let wid = leap!(xconn.xcb_connection().generate_id()); + let result = xconn.xcb_connection().create_window( + depth, + wid, + parent, + x, + y, + dimensions.0.try_into().unwrap(), + dimensions.1.try_into().unwrap(), + 0, + xproto::WindowClass::INPUT_OUTPUT, + visual, + &window_attributes, + ); + leap!(leap!(result).check()); + + wid + }; + + // The COPY_FROM_PARENT is a special value for the visual used to copy + // the visual from the parent window, thus we have to query the visual + // we've got when we built the window above. + if visual == x11rb::COPY_FROM_PARENT { + visual = leap!(leap!(xconn + .xcb_connection() + .get_window_attributes(xwindow as xproto::Window)) + .reply()) + .visual; + } + + #[allow(clippy::mutex_atomic)] + let mut window = UnownedWindow { + xconn: Arc::clone(xconn), + xwindow: xwindow as xproto::Window, + visual, + root, + screen_id, + selected_cursor: Default::default(), + cursor_grabbed_mode: Mutex::new(CursorGrabMode::None), + cursor_visible: Mutex::new(true), + ime_sender: Mutex::new(event_loop.ime_sender.clone()), + shared_state: SharedState::new(guessed_monitor, &window_attrs), + redraw_sender: event_loop.redraw_sender.clone(), + activation_sender: event_loop.activation_sender.clone(), + }; + + // Title must be set before mapping. Some tiling window managers (i.e. i3) use the window + // title to determine placement/etc., so doing this after mapping would cause the WM to + // act on the wrong title state. + leap!(window.set_title_inner(&window_attrs.title)).ignore_error(); + leap!(window.set_decorations_inner(window_attrs.decorations)).ignore_error(); + + if let Some(theme) = window_attrs.preferred_theme { + leap!(window.set_theme_inner(Some(theme))).ignore_error(); + } + + // Embed the window if needed. + if window_attrs.platform_specific.x11.embed_window.is_some() { + window.embed_window()?; + } + + { + // Enable drag and drop (TODO: extend API to make this toggleable) + { + let dnd_aware_atom = atoms[XdndAware]; + let version = &[5u32]; // Latest version; hasn't changed since 2002 + leap!(xconn.change_property( + window.xwindow, + dnd_aware_atom, + u32::from(xproto::AtomEnum::ATOM), + xproto::PropMode::REPLACE, + version, + )) + .ignore_error(); + } + + // WM_CLASS must be set *before* mapping the window, as per ICCCM! + { + let (instance, class) = + if let Some(name) = window_attrs.platform_specific.name { + (name.instance, name.general) + } else { + let class = env::args_os() + .next() + .as_ref() + // Default to the name of the binary (via argv[0]) + .and_then(|path| Path::new(path).file_name()) + .and_then(|bin_name| bin_name.to_str()) + .map(|bin_name| bin_name.to_owned()) + .unwrap_or_else(|| window_attrs.title.clone()); + // This environment variable is extraordinarily unlikely to actually be used... + let instance = env::var("RESOURCE_NAME") + .ok() + .unwrap_or_else(|| class.clone()); + (instance, class) + }; + + let class = format!("{instance}\0{class}\0"); + leap!(xconn.change_property( + window.xwindow, + xproto::Atom::from(xproto::AtomEnum::WM_CLASS), + xproto::Atom::from(xproto::AtomEnum::STRING), + xproto::PropMode::REPLACE, + class.as_bytes(), + )) + .ignore_error(); + } + + if let Some(flusher) = leap!(window.set_pid()) { + flusher.ignore_error() + } + + leap!(window + .set_window_types(window_attrs.platform_specific.x11.x11_window_types)) + .ignore_error(); + + // Set size hints. + let mut min_inner_size = window_attrs + .min_inner_size + .map(|size| size.to_physical::(scale_factor)); + let mut max_inner_size = window_attrs + .max_inner_size + .map(|size| size.to_physical::(scale_factor)); + + if !window_attrs.resizable { + if util::wm_name_is_one_of(&["Xfwm4"]) { + warn!("To avoid a WM bug, disabling resizing has no effect on Xfwm4"); + } else { + max_inner_size = Some(dimensions.into()); + min_inner_size = Some(dimensions.into()); + } + } + + let shared_state = window.shared_state.get_mut().unwrap(); + shared_state.min_inner_size = min_inner_size.map(Into::into); + shared_state.max_inner_size = max_inner_size.map(Into::into); + shared_state.resize_increments = window_attrs.resize_increments; + shared_state.base_size = window_attrs.platform_specific.x11.base_size; + + let normal_hints = WmSizeHints { + position: position.map(|PhysicalPosition { x, y }| { + (WmSizeHintsSpecification::UserSpecified, x, y) + }), + size: Some(( + WmSizeHintsSpecification::UserSpecified, + cast_dimension_to_hint(dimensions.0), + cast_dimension_to_hint(dimensions.1), + )), + max_size: max_inner_size.map(cast_physical_size_to_hint), + min_size: min_inner_size.map(cast_physical_size_to_hint), + size_increment: window_attrs + .resize_increments + .map(|size| cast_size_to_hint(size, scale_factor)), + base_size: window_attrs + .platform_specific + .x11 + .base_size + .map(|size| cast_size_to_hint(size, scale_factor)), + aspect: None, + win_gravity: None, + }; + leap!(leap!(normal_hints.set( + xconn.xcb_connection(), + window.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + )) + .check()); + + // Set window icons + if let Some(icon) = window_attrs.window_icon { + leap!(window.set_icon_inner(icon.inner)).ignore_error(); + } + + // Opt into handling window close + let result = xconn.xcb_connection().change_property( + xproto::PropMode::REPLACE, + window.xwindow, + atoms[WM_PROTOCOLS], + xproto::AtomEnum::ATOM, + 32, + 2, + bytemuck::cast_slice::(&[ + atoms[WM_DELETE_WINDOW], + atoms[_NET_WM_PING], + ]), + ); + leap!(result).ignore_error(); + + // Set visibility (map window) + if window_attrs.visible { + leap!(xconn.xcb_connection().map_window(window.xwindow)).ignore_error(); + leap!(xconn.xcb_connection().configure_window( + xwindow, + &xproto::ConfigureWindowAux::new() + .stack_mode(xproto::StackMode::ABOVE) + )) + .ignore_error(); + } + + // Attempt to make keyboard input repeat detectable + unsafe { + let mut supported_ptr = ffi::False; + (xconn.xlib.XkbSetDetectableAutoRepeat)( + xconn.display, + ffi::True, + &mut supported_ptr, + ); + if supported_ptr == ffi::False { + return Err(os_error!(OsError::Misc( + "`XkbSetDetectableAutoRepeat` failed" + ))); + } + } + + // Select XInput2 events + let mask = xinput::XIEventMask::MOTION + | xinput::XIEventMask::BUTTON_PRESS + | xinput::XIEventMask::BUTTON_RELEASE + | xinput::XIEventMask::ENTER + | xinput::XIEventMask::LEAVE + | xinput::XIEventMask::FOCUS_IN + | xinput::XIEventMask::FOCUS_OUT + | xinput::XIEventMask::TOUCH_BEGIN + | xinput::XIEventMask::TOUCH_UPDATE + | xinput::XIEventMask::TOUCH_END; + leap!(xconn.select_xinput_events( + window.xwindow, + super::ALL_MASTER_DEVICES, + mask + )) + .ignore_error(); + + // Try to create input context for the window. + if let Some(ime) = event_loop.ime.as_ref() { + let result = ime + .borrow_mut() + .create_context(window.xwindow as ffi::Window, false); + leap!(result); + } + + // These properties must be set after mapping + if window_attrs.maximized { + leap!(window.set_maximized_inner(window_attrs.maximized)).ignore_error(); + } + if window_attrs.fullscreen.is_some() { + if let Some(flusher) = leap!(window.set_fullscreen_inner( + window_attrs.fullscreen.clone().map(Into::into) + )) { + flusher.ignore_error() + } + + if let Some(PhysicalPosition { x, y }) = position { + let shared_state = window.shared_state.get_mut().unwrap(); + + shared_state.restore_position = Some((x, y)); + } + } + + leap!(window.set_window_level_inner(window_attrs.window_level)) + .ignore_error(); + } + + window.set_cursor(window_attrs.cursor); + + // Remove the startup notification if we have one. + if let Some(startup) = window_attrs.platform_specific.activation_token.as_ref() { + leap!(xconn.remove_activation_token(xwindow, &startup._token)); + } + + // We never want to give the user a broken window, since by then, it's too late to handle. + let window = leap!(xconn.sync_with_server().map(|_| window)); + + Ok(window) + } + + /// Embed this window into a parent window. + pub(super) fn embed_window(&self) -> Result<(), RootOsError> { + let atoms = self.xconn.atoms(); + leap!(leap!(self.xconn.change_property( + self.xwindow, + atoms[_XEMBED], + atoms[_XEMBED], + xproto::PropMode::REPLACE, + &[0u32, 1u32], + )) + .check()); + + Ok(()) + } + + pub(super) fn shared_state_lock(&self) -> MutexGuard<'_, SharedState> { + self.shared_state.lock().unwrap() + } + + fn set_pid(&self) -> Result>, X11Error> { + let atoms = self.xconn.atoms(); + let pid_atom = atoms[_NET_WM_PID]; + let client_machine_atom = atoms[WM_CLIENT_MACHINE]; + + // Get the hostname and the PID. + let uname = rustix::system::uname(); + let pid = rustix::process::getpid(); + + self.xconn + .change_property( + self.xwindow, + pid_atom, + xproto::Atom::from(xproto::AtomEnum::CARDINAL), + xproto::PropMode::REPLACE, + &[pid.as_raw_nonzero().get() as util::Cardinal], + )? + .ignore_error(); + let flusher = self.xconn.change_property( + self.xwindow, + client_machine_atom, + xproto::Atom::from(xproto::AtomEnum::STRING), + xproto::PropMode::REPLACE, + uname.nodename().to_bytes(), + ); + flusher.map(Some) + } + + fn set_window_types( + &self, + window_types: Vec, + ) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let hint_atom = atoms[_NET_WM_WINDOW_TYPE]; + let atoms: Vec<_> = window_types + .iter() + .map(|t| t.as_atom(&self.xconn)) + .collect(); + + self.xconn.change_property( + self.xwindow, + hint_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + xproto::PropMode::REPLACE, + &atoms, + ) + } + + pub fn set_theme_inner( + &self, + theme: Option, + ) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let hint_atom = atoms[_GTK_THEME_VARIANT]; + let utf8_atom = atoms[UTF8_STRING]; + let variant = match theme { + Some(Theme::Dark) => "dark", + Some(Theme::Light) => "light", + None => "dark", + }; + let variant = + CString::new(variant).expect("`_GTK_THEME_VARIANT` contained null byte"); + self.xconn.change_property( + self.xwindow, + hint_atom, + utf8_atom, + xproto::PropMode::REPLACE, + variant.as_bytes(), + ) + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + self.set_theme_inner(theme) + .expect("Failed to change window theme") + .ignore_error(); + + self.xconn + .flush_requests() + .expect("Failed to change window theme"); + } + + fn set_netwm( + &self, + operation: util::StateOperation, + properties: (u32, u32, u32, u32), + ) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let state_atom = atoms[_NET_WM_STATE]; + self.xconn.send_client_msg( + self.xwindow, + self.root, + state_atom, + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [ + operation as u32, + properties.0, + properties.1, + properties.2, + properties.3, + ], + ) + } + + fn set_fullscreen_hint(&self, fullscreen: bool) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let fullscreen_atom = atoms[_NET_WM_STATE_FULLSCREEN]; + let flusher = self.set_netwm(fullscreen.into(), (fullscreen_atom, 0, 0, 0)); + + if fullscreen { + // Ensure that the fullscreen window receives input focus to prevent + // locking up the user's display. + self.xconn + .xcb_connection() + .set_input_focus( + xproto::InputFocus::PARENT, + self.xwindow, + x11rb::CURRENT_TIME, + )? + .ignore_error(); + } + + flusher + } + + fn set_fullscreen_inner( + &self, + fullscreen: Option, + ) -> Result>, X11Error> { + let mut shared_state_lock = self.shared_state_lock(); + + match shared_state_lock.visibility { + // Setting fullscreen on a window that is not visible will generate an error. + Visibility::No | Visibility::YesWait => { + shared_state_lock.desired_fullscreen = Some(fullscreen); + return Ok(None); + } + Visibility::Yes => (), + } + + let old_fullscreen = shared_state_lock.fullscreen.clone(); + if old_fullscreen == fullscreen { + return Ok(None); + } + shared_state_lock.fullscreen.clone_from(&fullscreen); + + match (&old_fullscreen, &fullscreen) { + // Store the desktop video mode before entering exclusive + // fullscreen, so we can restore it upon exit, as XRandR does not + // provide a mechanism to set this per app-session or restore this + // to the desktop video mode as macOS and Windows do + ( + &None, + &Some(Fullscreen::Exclusive(PlatformVideoModeHandle::X(ref video_mode))), + ) + | ( + &Some(Fullscreen::Borderless(_)), + &Some(Fullscreen::Exclusive(PlatformVideoModeHandle::X(ref video_mode))), + ) => { + let monitor = video_mode.monitor.as_ref().unwrap(); + shared_state_lock.desktop_video_mode = Some(( + monitor.id, + self.xconn + .get_crtc_mode(monitor.id) + .expect("Failed to get desktop video mode"), + )); + } + // Restore desktop video mode upon exiting exclusive fullscreen + (&Some(Fullscreen::Exclusive(_)), &None) + | (&Some(Fullscreen::Exclusive(_)), &Some(Fullscreen::Borderless(_))) => { + let (monitor_id, mode_id) = + shared_state_lock.desktop_video_mode.take().unwrap(); + self.xconn + .set_crtc_config(monitor_id, mode_id) + .expect("failed to restore desktop video mode"); + } + _ => (), + } + + drop(shared_state_lock); + + match fullscreen { + None => { + let flusher = self.set_fullscreen_hint(false); + let mut shared_state_lock = self.shared_state_lock(); + if let Some(position) = shared_state_lock.restore_position.take() { + drop(shared_state_lock); + self.set_position_inner(position.0, position.1) + .expect_then_ignore_error("Failed to restore window position"); + } + flusher.map(Some) + } + Some(fullscreen) => { + let (video_mode, monitor) = match fullscreen { + Fullscreen::Exclusive(PlatformVideoModeHandle::X(ref video_mode)) => { + (Some(video_mode), video_mode.monitor.clone().unwrap()) + } + Fullscreen::Borderless(Some(PlatformMonitorHandle::X(monitor))) => { + (None, monitor) + } + Fullscreen::Borderless(None) => { + (None, self.shared_state_lock().last_monitor.clone()) + } + #[cfg(wayland_platform)] + _ => unreachable!(), + }; + + // Don't set fullscreen on an invalid dummy monitor handle + if monitor.is_dummy() { + return Ok(None); + } + + if let Some(video_mode) = video_mode { + // FIXME: this is actually not correct if we're setting the + // video mode to a resolution higher than the current + // desktop resolution, because XRandR does not automatically + // reposition the monitors to the right and below this + // monitor. + // + // What ends up happening is we will get the fullscreen + // window showing up on those monitors as well, because + // their virtual position now overlaps with the monitor that + // we just made larger.. + // + // It'd be quite a bit of work to handle this correctly (and + // nobody else seems to bother doing this correctly either), + // so we're just leaving this broken. Fixing this would + // involve storing all CRTCs upon entering fullscreen, + // restoring them upon exit, and after entering fullscreen, + // repositioning displays to the right and below this + // display. I think there would still be edge cases that are + // difficult or impossible to handle correctly, e.g. what if + // a new monitor was plugged in while in fullscreen? + // + // I think we might just want to disallow setting the video + // mode higher than the current desktop video mode (I'm sure + // this will make someone unhappy, but it's very unusual for + // games to want to do this anyway). + self.xconn + .set_crtc_config(monitor.id, video_mode.native_mode) + .expect("failed to set video mode"); + } + + let window_position = self.outer_position_physical(); + self.shared_state_lock().restore_position = Some(window_position); + let monitor_origin: (i32, i32) = monitor.position().into(); + self.set_position_inner(monitor_origin.0, monitor_origin.1) + .expect_then_ignore_error("Failed to set window position"); + self.set_fullscreen_hint(true).map(Some) + } + } + } + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + let shared_state = self.shared_state_lock(); + + shared_state + .desired_fullscreen + .clone() + .unwrap_or_else(|| shared_state.fullscreen.clone()) + } + + #[inline] + pub(crate) fn set_fullscreen(&self, fullscreen: Option) { + if let Some(flusher) = self + .set_fullscreen_inner(fullscreen) + .expect("Failed to change window fullscreen state") + { + flusher + .check() + .expect("Failed to change window fullscreen state"); + self.invalidate_cached_frame_extents(); + } + } + + // Called by EventProcessor when a VisibilityNotify event is received + pub(crate) fn visibility_notify(&self) { + let mut shared_state = self.shared_state_lock(); + + match shared_state.visibility { + Visibility::No => self + .xconn + .xcb_connection() + .unmap_window(self.xwindow) + .expect_then_ignore_error("Failed to unmap window"), + Visibility::Yes => (), + Visibility::YesWait => { + shared_state.visibility = Visibility::Yes; + + if let Some(fullscreen) = shared_state.desired_fullscreen.take() { + drop(shared_state); + self.set_fullscreen(fullscreen); + } + } + } + } + + pub fn current_monitor(&self) -> Option { + Some(self.shared_state_lock().last_monitor.clone()) + } + + pub fn available_monitors(&self) -> Vec { + self.xconn + .available_monitors() + .expect("Failed to get available monitors") + } + + pub fn primary_monitor(&self) -> Option { + Some( + self.xconn + .primary_monitor() + .expect("Failed to get primary monitor"), + ) + } + + #[inline] + pub fn is_minimized(&self) -> Option { + let atoms = self.xconn.atoms(); + let state_atom = atoms[_NET_WM_STATE]; + let state = self.xconn.get_property( + self.xwindow, + state_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + ); + let hidden_atom = atoms[_NET_WM_STATE_HIDDEN]; + + Some(match state { + Ok(atoms) => atoms + .iter() + .any(|atom: &xproto::Atom| *atom as xproto::Atom == hidden_atom), + _ => false, + }) + } + + /// Refresh the API for the given monitor. + #[inline] + pub(super) fn refresh_dpi_for_monitor( + &self, + new_monitor: &X11MonitorHandle, + maybe_prev_scale_factor: Option, + mut callback: impl FnMut(Event), + ) { + // Check if the self is on this monitor + let monitor = self.shared_state_lock().last_monitor.clone(); + if monitor.name == new_monitor.name { + let (width, height) = self.inner_size_physical(); + let (new_width, new_height) = self.adjust_for_dpi( + // If we couldn't determine the previous scale + // factor (e.g., because all monitors were closed + // before), just pick whatever the current monitor + // has set as a baseline. + maybe_prev_scale_factor.unwrap_or(monitor.scale_factor), + new_monitor.scale_factor, + width, + height, + &self.shared_state_lock(), + ); + + let window_id = crate::window::WindowId(self.id()); + let old_inner_size = PhysicalSize::new(width, height); + let inner_size = + Arc::new(Mutex::new(PhysicalSize::new(new_width, new_height))); + callback(Event::WindowEvent { + window_id, + event: WindowEvent::ScaleFactorChanged { + scale_factor: new_monitor.scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&inner_size)), + }, + }); + + let new_inner_size = *inner_size.lock().unwrap(); + drop(inner_size); + + if new_inner_size != old_inner_size { + let (new_width, new_height) = new_inner_size.into(); + self.request_inner_size_physical(new_width, new_height); + } + } + } + + fn set_minimized_inner(&self, minimized: bool) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + + if minimized { + let root_window = self.xconn.default_root().root; + + self.xconn.send_client_msg( + self.xwindow, + root_window, + atoms[WM_CHANGE_STATE], + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [3u32, 0, 0, 0, 0], + ) + } else { + self.xconn.send_client_msg( + self.xwindow, + self.root, + atoms[_NET_ACTIVE_WINDOW], + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [1, x11rb::CURRENT_TIME, 0, 0, 0], + ) + } + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + self.set_minimized_inner(minimized) + .expect_then_ignore_error("Failed to change window minimization"); + + self.xconn + .flush_requests() + .expect("Failed to change window minimization"); + } + + #[inline] + pub fn is_maximized(&self) -> bool { + let atoms = self.xconn.atoms(); + let state_atom = atoms[_NET_WM_STATE]; + let state = self.xconn.get_property( + self.xwindow, + state_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + ); + let horz_atom = atoms[_NET_WM_STATE_MAXIMIZED_HORZ]; + let vert_atom = atoms[_NET_WM_STATE_MAXIMIZED_VERT]; + match state { + Ok(atoms) => { + let horz_maximized = + atoms.iter().any(|atom: &xproto::Atom| *atom == horz_atom); + let vert_maximized = + atoms.iter().any(|atom: &xproto::Atom| *atom == vert_atom); + horz_maximized && vert_maximized + } + _ => false, + } + } + + fn set_maximized_inner(&self, maximized: bool) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let horz_atom = atoms[_NET_WM_STATE_MAXIMIZED_HORZ]; + let vert_atom = atoms[_NET_WM_STATE_MAXIMIZED_VERT]; + + self.set_netwm(maximized.into(), (horz_atom, vert_atom, 0, 0)) + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + self.set_maximized_inner(maximized) + .expect_then_ignore_error("Failed to change window maximization"); + self.xconn + .flush_requests() + .expect("Failed to change window maximization"); + self.invalidate_cached_frame_extents(); + } + + fn set_title_inner(&self, title: &str) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + + let title = CString::new(title).expect("Window title contained null byte"); + self.xconn + .change_property( + self.xwindow, + xproto::Atom::from(xproto::AtomEnum::WM_NAME), + xproto::Atom::from(xproto::AtomEnum::STRING), + xproto::PropMode::REPLACE, + title.as_bytes(), + )? + .ignore_error(); + self.xconn.change_property( + self.xwindow, + atoms[_NET_WM_NAME], + atoms[UTF8_STRING], + xproto::PropMode::REPLACE, + title.as_bytes(), + ) + } + + #[inline] + pub fn set_title(&self, title: &str) { + self.set_title_inner(title) + .expect_then_ignore_error("Failed to set window title"); + + self.xconn + .flush_requests() + .expect("Failed to set window title"); + } + + #[inline] + pub fn set_transparent(&self, _transparent: bool) {} + + #[inline] + pub fn set_blur(&self, _blur: bool) {} + + fn set_decorations_inner( + &self, + decorations: bool, + ) -> Result, X11Error> { + self.shared_state_lock().is_decorated = decorations; + let mut hints = self.xconn.get_motif_hints(self.xwindow); + + hints.set_decorations(decorations); + + self.xconn.set_motif_hints(self.xwindow, &hints) + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + self.set_decorations_inner(decorations) + .expect_then_ignore_error("Failed to set decoration state"); + self.xconn + .flush_requests() + .expect("Failed to set decoration state"); + self.invalidate_cached_frame_extents(); + } + + #[inline] + pub fn is_decorated(&self) -> bool { + self.shared_state_lock().is_decorated + } + + fn set_maximizable_inner( + &self, + maximizable: bool, + ) -> Result, X11Error> { + let mut hints = self.xconn.get_motif_hints(self.xwindow); + + hints.set_maximizable(maximizable); + + self.xconn.set_motif_hints(self.xwindow, &hints) + } + + fn toggle_atom( + &self, + atom_name: AtomName, + enable: bool, + ) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let atom = atoms[atom_name]; + self.set_netwm(enable.into(), (atom, 0, 0, 0)) + } + + fn set_window_level_inner( + &self, + level: WindowLevel, + ) -> Result, X11Error> { + self.toggle_atom(_NET_WM_STATE_ABOVE, level == WindowLevel::AlwaysOnTop)? + .ignore_error(); + self.toggle_atom(_NET_WM_STATE_BELOW, level == WindowLevel::AlwaysOnBottom) + } + + #[inline] + pub fn set_window_level(&self, level: WindowLevel) { + self.set_window_level_inner(level) + .expect_then_ignore_error("Failed to set window-level state"); + self.xconn + .flush_requests() + .expect("Failed to set window-level state"); + } + + fn set_icon_inner(&self, icon: PlatformIcon) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let icon_atom = atoms[_NET_WM_ICON]; + let data = icon.to_cardinals(); + self.xconn.change_property( + self.xwindow, + icon_atom, + xproto::Atom::from(xproto::AtomEnum::CARDINAL), + xproto::PropMode::REPLACE, + data.as_slice(), + ) + } + + fn unset_icon_inner(&self) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let icon_atom = atoms[_NET_WM_ICON]; + let empty_data: [util::Cardinal; 0] = []; + self.xconn.change_property( + self.xwindow, + icon_atom, + xproto::Atom::from(xproto::AtomEnum::CARDINAL), + xproto::PropMode::REPLACE, + &empty_data, + ) + } + + #[inline] + pub(crate) fn set_window_icon(&self, icon: Option) { + match icon { + Some(icon) => self.set_icon_inner(icon), + None => self.unset_icon_inner(), + } + .expect_then_ignore_error("Failed to set icons"); + + self.xconn.flush_requests().expect("Failed to set icons"); + } + + #[inline] + pub fn set_visible(&self, visible: bool) { + let mut shared_state = self.shared_state_lock(); + + match (visible, shared_state.visibility) { + (true, Visibility::Yes) + | (true, Visibility::YesWait) + | (false, Visibility::No) => return, + _ => (), + } + + if visible { + self.xconn + .xcb_connection() + .map_window(self.xwindow) + .expect_then_ignore_error("Failed to call `xcb_map_window`"); + self.xconn + .xcb_connection() + .configure_window( + self.xwindow, + &xproto::ConfigureWindowAux::new() + .stack_mode(xproto::StackMode::ABOVE), + ) + .expect_then_ignore_error("Failed to call `xcb_configure_window`"); + self.xconn + .flush_requests() + .expect("Failed to call XMapRaised"); + shared_state.visibility = Visibility::YesWait; + } else { + self.xconn + .xcb_connection() + .unmap_window(self.xwindow) + .expect_then_ignore_error("Failed to call `xcb_unmap_window`"); + self.xconn + .flush_requests() + .expect("Failed to call XUnmapWindow"); + shared_state.visibility = Visibility::No; + } + } + + #[inline] + pub fn is_visible(&self) -> Option { + Some(self.shared_state_lock().visibility == Visibility::Yes) + } + + fn update_cached_frame_extents(&self) { + let extents = self + .xconn + .get_frame_extents_heuristic(self.xwindow, self.root); + self.shared_state_lock().frame_extents = Some(extents); + } + + pub(crate) fn invalidate_cached_frame_extents(&self) { + self.shared_state_lock().frame_extents.take(); + } + + pub(crate) fn outer_position_physical(&self) -> (i32, i32) { + let extents = self.shared_state_lock().frame_extents.clone(); + if let Some(extents) = extents { + let (x, y) = self.inner_position_physical(); + extents.inner_pos_to_outer(x, y) + } else { + self.update_cached_frame_extents(); + self.outer_position_physical() + } + } + + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + let extents = self.shared_state_lock().frame_extents.clone(); + if let Some(extents) = extents { + let (x, y) = self.inner_position_physical(); + Ok(extents.inner_pos_to_outer(x, y).into()) + } else { + self.update_cached_frame_extents(); + self.outer_position() + } + } + + pub(crate) fn inner_position_physical(&self) -> (i32, i32) { + // This should be okay to unwrap since the only error XTranslateCoordinates can return + // is BadWindow, and if the window handle is bad we have bigger problems. + self.xconn + .translate_coords(self.xwindow, self.root) + .map(|coords| (coords.dst_x.into(), coords.dst_y.into())) + .unwrap() + } + + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + Ok(self.inner_position_physical().into()) + } + + pub(crate) fn set_position_inner( + &self, + mut x: i32, + mut y: i32, + ) -> Result, X11Error> { + // There are a few WMs that set client area position rather than window position, so + // we'll translate for consistency. + if util::wm_name_is_one_of(&["Enlightenment", "FVWM"]) { + let extents = self.shared_state_lock().frame_extents.clone(); + if let Some(extents) = extents { + x += cast_dimension_to_hint(extents.frame_extents.left); + y += cast_dimension_to_hint(extents.frame_extents.top); + } else { + self.update_cached_frame_extents(); + return self.set_position_inner(x, y); + } + } + + self.xconn + .xcb_connection() + .configure_window(self.xwindow, &xproto::ConfigureWindowAux::new().x(x).y(y)) + .map_err(Into::into) + } + + pub(crate) fn set_position_physical(&self, x: i32, y: i32) { + self.set_position_inner(x, y) + .expect_then_ignore_error("Failed to call `XMoveWindow`"); + } + + #[inline] + pub fn set_outer_position(&self, position: Position) { + let (x, y) = position.to_physical::(self.scale_factor()).into(); + self.set_position_physical(x, y); + } + + pub(crate) fn inner_size_physical(&self) -> (u32, u32) { + // This should be okay to unwrap since the only error XGetGeometry can return + // is BadWindow, and if the window handle is bad we have bigger problems. + self.xconn + .get_geometry(self.xwindow) + .map(|geo| (geo.width.into(), geo.height.into())) + .unwrap() + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + self.inner_size_physical().into() + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + let extents = self.shared_state_lock().frame_extents.clone(); + if let Some(extents) = extents { + let (width, height) = self.inner_size_physical(); + extents.inner_size_to_outer(width, height).into() + } else { + self.update_cached_frame_extents(); + self.outer_size() + } + } + + pub(crate) fn request_inner_size_physical(&self, width: u32, height: u32) { + self.xconn + .xcb_connection() + .configure_window( + self.xwindow, + &xproto::ConfigureWindowAux::new() + .width(width) + .height(height), + ) + .expect_then_ignore_error("Failed to call `xcb_configure_window`"); + self.xconn + .flush_requests() + .expect("Failed to call XResizeWindow"); + // cursor_hittest needs to be reapplied after each window resize. + if self.shared_state_lock().cursor_hittest.unwrap_or(false) { + let _ = self.set_cursor_hittest(true); + } + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let scale_factor = self.scale_factor(); + let size = size.to_physical::(scale_factor).into(); + if !self.shared_state_lock().is_resizable { + self.update_normal_hints(|normal_hints| { + normal_hints.min_size = Some(size); + normal_hints.max_size = Some(size); + }) + .expect("Failed to call `XSetWMNormalHints`"); + } + self.request_inner_size_physical(size.0 as u32, size.1 as u32); + + None + } + + fn update_normal_hints(&self, callback: F) -> Result<(), X11Error> + where + F: FnOnce(&mut WmSizeHints), + { + let mut normal_hints = WmSizeHints::get( + self.xconn.xcb_connection(), + self.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + )? + .reply()? + .unwrap_or_default(); + callback(&mut normal_hints); + normal_hints + .set( + self.xconn.xcb_connection(), + self.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + )? + .ignore_error(); + Ok(()) + } + + pub(crate) fn set_min_inner_size_physical(&self, dimensions: Option<(u32, u32)>) { + self.update_normal_hints(|normal_hints| { + normal_hints.min_size = dimensions + .map(|(w, h)| (cast_dimension_to_hint(w), cast_dimension_to_hint(h))) + }) + .expect("Failed to call `XSetWMNormalHints`"); + } + + #[inline] + pub fn set_min_inner_size(&self, dimensions: Option) { + self.shared_state_lock().min_inner_size = dimensions; + let physical_dimensions = dimensions + .map(|dimensions| dimensions.to_physical::(self.scale_factor()).into()); + self.set_min_inner_size_physical(physical_dimensions); + } + + pub(crate) fn set_max_inner_size_physical(&self, dimensions: Option<(u32, u32)>) { + self.update_normal_hints(|normal_hints| { + normal_hints.max_size = dimensions + .map(|(w, h)| (cast_dimension_to_hint(w), cast_dimension_to_hint(h))) + }) + .expect("Failed to call `XSetWMNormalHints`"); + } + + #[inline] + pub fn set_max_inner_size(&self, dimensions: Option) { + self.shared_state_lock().max_inner_size = dimensions; + let physical_dimensions = dimensions + .map(|dimensions| dimensions.to_physical::(self.scale_factor()).into()); + self.set_max_inner_size_physical(physical_dimensions); + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + WmSizeHints::get( + self.xconn.xcb_connection(), + self.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + ) + .ok() + .and_then(|cookie| cookie.reply().ok()) + .flatten() + .and_then(|hints| hints.size_increment) + .map(|(width, height)| (width as u32, height as u32).into()) + } + + #[inline] + pub fn set_resize_increments(&self, increments: Option) { + self.shared_state_lock().resize_increments = increments; + let physical_increments = increments + .map(|increments| cast_size_to_hint(increments, self.scale_factor())); + self.update_normal_hints(|hints| hints.size_increment = physical_increments) + .expect("Failed to call `XSetWMNormalHints`"); + } + + pub(crate) fn adjust_for_dpi( + &self, + old_scale_factor: f64, + new_scale_factor: f64, + width: u32, + height: u32, + shared_state: &SharedState, + ) -> (u32, u32) { + let scale_factor = new_scale_factor / old_scale_factor; + self.update_normal_hints(|normal_hints| { + let dpi_adjuster = + |size: Size| -> (i32, i32) { cast_size_to_hint(size, scale_factor) }; + let max_size = shared_state.max_inner_size.map(dpi_adjuster); + let min_size = shared_state.min_inner_size.map(dpi_adjuster); + let resize_increments = shared_state.resize_increments.map(dpi_adjuster); + let base_size = shared_state.base_size.map(dpi_adjuster); + + normal_hints.max_size = max_size; + normal_hints.min_size = min_size; + normal_hints.size_increment = resize_increments; + normal_hints.base_size = base_size; + }) + .expect("Failed to update normal hints"); + + let new_width = (width as f64 * scale_factor).round() as u32; + let new_height = (height as f64 * scale_factor).round() as u32; + + (new_width, new_height) + } + + pub fn set_resizable(&self, resizable: bool) { + if util::wm_name_is_one_of(&["Xfwm4"]) { + // Making the window unresizable on Xfwm prevents further changes to `WM_NORMAL_HINTS` + // from being detected. This makes it impossible for resizing to be + // re-enabled, and also breaks DPI scaling. As such, we choose the lesser of + // two evils and do nothing. + warn!("To avoid a WM bug, disabling resizing has no effect on Xfwm4"); + return; + } + + let (min_size, max_size) = if resizable { + let shared_state_lock = self.shared_state_lock(); + ( + shared_state_lock.min_inner_size, + shared_state_lock.max_inner_size, + ) + } else { + let window_size = Some(Size::from(self.inner_size())); + (window_size, window_size) + }; + self.shared_state_lock().is_resizable = resizable; + + self.set_maximizable_inner(resizable) + .expect_then_ignore_error("Failed to call `XSetWMNormalHints`"); + + let scale_factor = self.scale_factor(); + let min_inner_size = min_size.map(|size| cast_size_to_hint(size, scale_factor)); + let max_inner_size = max_size.map(|size| cast_size_to_hint(size, scale_factor)); + self.update_normal_hints(|normal_hints| { + normal_hints.min_size = min_inner_size; + normal_hints.max_size = max_inner_size; + }) + .expect("Failed to call `XSetWMNormalHints`"); + } + + #[inline] + pub fn is_resizable(&self) -> bool { + self.shared_state_lock().is_resizable + } + + #[inline] + pub fn set_enabled_buttons(&self, _buttons: WindowButtons) {} + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + WindowButtons::all() + } + + #[allow(dead_code)] + #[inline] + pub fn xlib_display(&self) -> *mut c_void { + self.xconn.display as _ + } + + #[allow(dead_code)] + #[inline] + pub fn xlib_window(&self) -> c_ulong { + self.xwindow as ffi::Window + } + + #[inline] + pub fn set_cursor(&self, cursor: Cursor) { + match cursor { + Cursor::Icon(icon) => { + let old_cursor = replace( + &mut *self.selected_cursor.lock().unwrap(), + SelectedCursor::Named(icon), + ); + + #[allow(clippy::mutex_atomic)] + if SelectedCursor::Named(icon) != old_cursor + && *self.cursor_visible.lock().unwrap() + { + if let Err(err) = self.xconn.set_cursor_icon(self.xwindow, Some(icon)) + { + tracing::error!("failed to set cursor icon: {err}"); + } + } + } + Cursor::Custom(RootCustomCursor { + inner: PlatformCustomCursor::X(cursor), + }) => { + #[allow(clippy::mutex_atomic)] + if *self.cursor_visible.lock().unwrap() { + if let Err(err) = self.xconn.set_custom_cursor(self.xwindow, &cursor) + { + tracing::error!("failed to set window icon: {err}"); + } + } + + *self.selected_cursor.lock().unwrap() = SelectedCursor::Custom(cursor); + } + #[cfg(wayland_platform)] + Cursor::Custom(RootCustomCursor { + inner: PlatformCustomCursor::Wayland(_), + }) => { + tracing::error!("passed a Wayland cursor to X11 backend") + } + } + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let mut grabbed_lock = self.cursor_grabbed_mode.lock().unwrap(); + if mode == *grabbed_lock { + return Ok(()); + } + + // We ungrab before grabbing to prevent passive grabs from causing `AlreadyGrabbed`. + // Therefore, this is common to both codepaths. + self.xconn + .xcb_connection() + .ungrab_pointer(x11rb::CURRENT_TIME) + .expect_then_ignore_error("Failed to call `xcb_ungrab_pointer`"); + + let result = match mode { + CursorGrabMode::None => self.xconn.flush_requests().map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) + }), + CursorGrabMode::Confined => { + let result = { + self.xconn + .xcb_connection() + .grab_pointer( + true as _, + self.xwindow, + xproto::EventMask::BUTTON_PRESS + | xproto::EventMask::BUTTON_RELEASE + | xproto::EventMask::ENTER_WINDOW + | xproto::EventMask::LEAVE_WINDOW + | xproto::EventMask::POINTER_MOTION + | xproto::EventMask::POINTER_MOTION_HINT + | xproto::EventMask::BUTTON1_MOTION + | xproto::EventMask::BUTTON2_MOTION + | xproto::EventMask::BUTTON3_MOTION + | xproto::EventMask::BUTTON4_MOTION + | xproto::EventMask::BUTTON5_MOTION + | xproto::EventMask::KEYMAP_STATE, + xproto::GrabMode::ASYNC, + xproto::GrabMode::ASYNC, + self.xwindow, + 0u32, + x11rb::CURRENT_TIME, + ) + .expect("Failed to call `grab_pointer`") + .reply() + .expect("Failed to receive reply from `grab_pointer`") + }; + + match result.status { + xproto::GrabStatus::SUCCESS => Ok(()), + xproto::GrabStatus::ALREADY_GRABBED => { + Err("Cursor could not be confined: already confined by another client") + }, + xproto::GrabStatus::INVALID_TIME => { + Err("Cursor could not be confined: invalid time") + }, + xproto::GrabStatus::NOT_VIEWABLE => { + Err("Cursor could not be confined: confine location not viewable") + }, + xproto::GrabStatus::FROZEN => { + Err("Cursor could not be confined: frozen by another client") + }, + _ => unreachable!(), + } + .map_err(|err| ExternalError::Os(os_error!(OsError::Misc(err)))) + } + CursorGrabMode::Locked => { + return Err(ExternalError::NotSupported(NotSupportedError::new())); + } + }; + + if result.is_ok() { + *grabbed_lock = mode; + } + + result + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + #[allow(clippy::mutex_atomic)] + let mut visible_lock = self.cursor_visible.lock().unwrap(); + if visible == *visible_lock { + return; + } + let cursor = if visible { + Some((*self.selected_cursor.lock().unwrap()).clone()) + } else { + None + }; + *visible_lock = visible; + drop(visible_lock); + let result = match cursor { + Some(SelectedCursor::Custom(cursor)) => { + self.xconn.set_custom_cursor(self.xwindow, &cursor) + } + Some(SelectedCursor::Named(cursor)) => { + self.xconn.set_cursor_icon(self.xwindow, Some(cursor)) + } + None => self.xconn.set_cursor_icon(self.xwindow, None), + }; + + if let Err(err) = result { + tracing::error!("failed to set cursor icon: {err}"); + } + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.shared_state_lock().last_monitor.scale_factor + } + + pub fn set_cursor_position_physical( + &self, + x: i32, + y: i32, + ) -> Result<(), ExternalError> { + { + self.xconn + .xcb_connection() + .warp_pointer(x11rb::NONE, self.xwindow, 0, 0, 0, 0, x as _, y as _) + .map_err(|e| { + ExternalError::Os(os_error!(OsError::XError( + X11Error::from(e).into() + ))) + })?; + self.xconn.flush_requests().map_err(|e| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(e).into()))) + }) + } + } + + #[inline] + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + let (x, y) = position.to_physical::(self.scale_factor()).into(); + self.set_cursor_position_physical(x, y) + } + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + let mut rectangles: Vec = Vec::new(); + if hittest { + let size = self.inner_size(); + rectangles.push(Rectangle { + x: 0, + y: 0, + width: size.width as u16, + height: size.height as u16, + }) + } + let region = + RegionWrapper::create_region(self.xconn.xcb_connection(), &rectangles) + .map_err(|_e| ExternalError::Ignored)?; + self.xconn + .xcb_connection() + .xfixes_set_window_shape_region( + self.xwindow, + SK::INPUT, + 0, + 0, + region.region(), + ) + .map_err(|_e| ExternalError::Ignored)?; + self.shared_state_lock().cursor_hittest = Some(hittest); + Ok(()) + } + + /// Moves the window while it is being dragged. + pub fn drag_window(&self) -> Result<(), ExternalError> { + self.drag_initiate(util::MOVERESIZE_MOVE) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + /// Resizes the window while it is being dragged. + pub fn drag_resize_window( + &self, + direction: ResizeDirection, + ) -> Result<(), ExternalError> { + self.drag_initiate(match direction { + ResizeDirection::East => util::MOVERESIZE_RIGHT, + ResizeDirection::North => util::MOVERESIZE_TOP, + ResizeDirection::NorthEast => util::MOVERESIZE_TOPRIGHT, + ResizeDirection::NorthWest => util::MOVERESIZE_TOPLEFT, + ResizeDirection::South => util::MOVERESIZE_BOTTOM, + ResizeDirection::SouthEast => util::MOVERESIZE_BOTTOMRIGHT, + ResizeDirection::SouthWest => util::MOVERESIZE_BOTTOMLEFT, + ResizeDirection::West => util::MOVERESIZE_LEFT, + }) + } + + /// Initiates a drag operation while the left mouse button is pressed. + fn drag_initiate(&self, action: isize) -> Result<(), ExternalError> { + let pointer = self + .xconn + .query_pointer(self.xwindow, util::VIRTUAL_CORE_POINTER) + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err.into()))))?; + + let window = self.inner_position().map_err(ExternalError::NotSupported)?; + + let atoms = self.xconn.atoms(); + let message = atoms[_NET_WM_MOVERESIZE]; + + // we can't use `set_cursor_grab(false)` here because it doesn't run `XUngrabPointer` + // if the cursor isn't currently grabbed + let mut grabbed_lock = self.cursor_grabbed_mode.lock().unwrap(); + self.xconn + .xcb_connection() + .ungrab_pointer(x11rb::CURRENT_TIME) + .map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::from(err).into()))) + })? + .ignore_error(); + self.xconn.flush_requests().map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) + })?; + *grabbed_lock = CursorGrabMode::None; + + // we keep the lock until we are done + self.xconn + .send_client_msg( + self.xwindow, + self.root, + message, + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [ + (window.x + xinput_fp1616_to_float(pointer.win_x) as i32) as u32, + (window.y + xinput_fp1616_to_float(pointer.win_y) as i32) as u32, + action.try_into().unwrap(), + 1, // Button 1 + 1, + ], + ) + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err.into()))))?; + + self.xconn.flush_requests().map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) + }) + } + + #[inline] + pub fn set_ime_cursor_area(&self, spot: Position, _size: Size) { + let (x, y) = spot.to_physical::(self.scale_factor()).into(); + let _ = self.ime_sender.lock().unwrap().send(ImeRequest::Position( + self.xwindow as ffi::Window, + x, + y, + )); + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + let _ = self + .ime_sender + .lock() + .unwrap() + .send(ImeRequest::Allow(self.xwindow as ffi::Window, allowed)); + } + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + + #[inline] + pub fn focus_window(&self) { + let atoms = self.xconn.atoms(); + let state_atom = atoms[WM_STATE]; + let state_type_atom = atoms[CARD32]; + let is_minimized = if let Ok(state) = + self.xconn + .get_property::(self.xwindow, state_atom, state_type_atom) + { + state.contains(&super::ICONIC_STATE) + } else { + false + }; + let is_visible = match self.shared_state_lock().visibility { + Visibility::Yes => true, + Visibility::YesWait | Visibility::No => false, + }; + + if is_visible && !is_minimized { + self.xconn + .send_client_msg( + self.xwindow, + self.root, + atoms[_NET_ACTIVE_WINDOW], + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [1, x11rb::CURRENT_TIME, 0, 0, 0], + ) + .expect_then_ignore_error("Failed to send client message"); + if let Err(e) = self.xconn.flush_requests() { + tracing::error!( + "`flush` returned an error when focusing the window. Error was: {}", + e + ); + } + } + } + + #[inline] + pub fn request_user_attention(&self, request_type: Option) { + let mut wm_hints = + WmHints::get(self.xconn.xcb_connection(), self.xwindow as xproto::Window) + .ok() + .and_then(|cookie| cookie.reply().ok()) + .flatten() + .unwrap_or_default(); + + wm_hints.urgent = request_type.is_some(); + wm_hints + .set(self.xconn.xcb_connection(), self.xwindow as xproto::Window) + .expect_then_ignore_error("Failed to set WM hints"); + } + + #[inline] + pub(crate) fn generate_activation_token(&self) -> Result { + // Get the title from the WM_NAME property. + let atoms = self.xconn.atoms(); + let title = { + let title_bytes = self + .xconn + .get_property(self.xwindow, atoms[_NET_WM_NAME], atoms[UTF8_STRING]) + .expect("Failed to get title"); + + String::from_utf8(title_bytes).expect("Bad title") + }; + + // Get the activation token and then put it in the event queue. + let token = self.xconn.request_activation_token(&title)?; + + Ok(token) + } + + #[inline] + pub fn request_activation_token( + &self, + ) -> Result { + let serial = AsyncRequestSerial::get(); + self.activation_sender + .send((self.id(), serial)) + .expect("activation token channel should never be closed"); + Ok(serial) + } + + #[inline] + pub fn id(&self) -> WindowId { + WindowId(self.xwindow as _) + } + + #[inline] + pub fn request_redraw(&self) { + self.redraw_sender + .send(WindowId(self.xwindow as _)) + .unwrap(); + } + + #[inline] + pub fn pre_present_notify(&self) { + // TODO timer + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06( + &self, + ) -> Result { + let mut window_handle = rwh_06::XlibWindowHandle::new(self.xlib_window()); + window_handle.visual_id = self.visual as c_ulong; + Ok(window_handle.into()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::XlibDisplayHandle::new( + Some( + std::ptr::NonNull::new(self.xlib_display()) + .expect("display pointer should never be null"), + ), + self.screen_id, + ) + .into()) + } + + #[inline] + pub fn theme(&self) -> Option { + None + } + + pub fn set_content_protected(&self, _protected: bool) {} + + #[inline] + pub fn has_focus(&self) -> bool { + self.shared_state_lock().has_focus + } + + pub fn title(&self) -> String { + String::new() + } +} + +/// Cast a dimension value into a hinted dimension for `WmSizeHints`, clamping if too large. +fn cast_dimension_to_hint(val: u32) -> i32 { + val.try_into().unwrap_or(i32::MAX) +} + +/// Use the above strategy to cast a physical size into a hinted size. +fn cast_physical_size_to_hint(size: PhysicalSize) -> (i32, i32) { + let PhysicalSize { width, height } = size; + ( + cast_dimension_to_hint(width), + cast_dimension_to_hint(height), + ) +} + +/// Use the above strategy to cast a size into a hinted size. +fn cast_size_to_hint(size: Size, scale_factor: f64) -> (i32, i32) { + match size { + Size::Physical(size) => cast_physical_size_to_hint(size), + Size::Logical(size) => size.to_physical::(scale_factor).into(), + } +} diff --git a/rio-window/src/platform_impl/linux/x11/xdisplay.rs b/rio-window/src/platform_impl/linux/x11/xdisplay.rs new file mode 100644 index 00000000..1c8f0a55 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/xdisplay.rs @@ -0,0 +1,407 @@ +use std::collections::HashMap; +use std::error::Error; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard}; +use std::{fmt, ptr}; + +use crate::window::CursorIcon; + +use super::atoms::Atoms; +use super::ffi; +use super::monitor::MonitorHandle; +use x11rb::connection::Connection; +use x11rb::protocol::randr::ConnectionExt as _; +use x11rb::protocol::render; +use x11rb::protocol::xproto::{self, ConnectionExt}; +use x11rb::resource_manager; +use x11rb::xcb_ffi::XCBConnection; + +/// A connection to an X server. +pub struct XConnection { + pub xlib: ffi::Xlib, + + // TODO(notgull): I'd like to remove this, but apparently Xlib and Xinput2 are tied together + // for some reason. + pub xinput2: ffi::XInput2, + + pub display: *mut ffi::Display, + + /// The manager for the XCB connection. + /// + /// The `Option` ensures that we can drop it before we close the `Display`. + xcb: Option, + + /// The atoms used by `winit`. + /// + /// This is a large structure, so I've elected to Box it to make accessing the fields of + /// this struct easier. Feel free to unbox it if you like kicking puppies. + atoms: Box, + + /// The index of the default screen. + default_screen: usize, + + /// The last timestamp received by this connection. + timestamp: AtomicU32, + + /// List of monitor handles. + pub monitor_handles: Mutex>>, + + /// The resource database. + database: RwLock, + + /// RandR version. + randr_version: (u32, u32), + + /// Atom for the XSettings screen. + xsettings_screen: Option, + + /// XRender format information. + render_formats: render::QueryPictFormatsReply, + + pub latest_error: Mutex>, + pub cursor_cache: Mutex, xproto::Cursor>>, +} + +unsafe impl Send for XConnection {} +unsafe impl Sync for XConnection {} + +pub type XErrorHandler = Option< + unsafe extern "C" fn(*mut ffi::Display, *mut ffi::XErrorEvent) -> std::os::raw::c_int, +>; + +impl XConnection { + pub fn new(error_handler: XErrorHandler) -> Result { + // opening the libraries + let xlib = ffi::Xlib::open()?; + let xlib_xcb = ffi::Xlib_xcb::open()?; + let xinput2 = ffi::XInput2::open()?; + + unsafe { (xlib.XInitThreads)() }; + unsafe { (xlib.XSetErrorHandler)(error_handler) }; + + // calling XOpenDisplay + let display = unsafe { + let display = (xlib.XOpenDisplay)(ptr::null()); + if display.is_null() { + return Err(XNotSupported::XOpenDisplayFailed); + } + display + }; + + // Open the x11rb XCB connection. + let xcb = { + // Get a pointer to the underlying XCB connection + let xcb_connection = + unsafe { (xlib_xcb.XGetXCBConnection)(display as *mut ffi::Display) }; + assert!(!xcb_connection.is_null()); + + // Wrap the XCB connection in an x11rb XCB connection + let conn = unsafe { + XCBConnection::from_raw_xcb_connection(xcb_connection.cast(), false) + }; + + conn.map_err(|e| { + XNotSupported::XcbConversionError(Arc::new(WrapConnectError(e))) + })? + }; + + // Get the default screen. + let default_screen = unsafe { (xlib.XDefaultScreen)(display) } as usize; + + // Load the database. + let database = resource_manager::new_from_default(&xcb) + .map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?; + + // Load the RandR version. + let randr_version = xcb + .randr_query_version(1, 3) + .expect("failed to request XRandR version") + .reply() + .expect("failed to query XRandR version"); + + let xsettings_screen = Self::new_xsettings_screen(&xcb, default_screen); + if xsettings_screen.is_none() { + tracing::warn!( + "error setting XSETTINGS; Xft options won't reload automatically" + ) + } + + // Start getting the XRender formats. + let formats_cookie = render::query_pict_formats(&xcb) + .map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?; + + // Fetch atoms. + let atoms = Atoms::new(&xcb) + .map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))? + .reply() + .map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?; + + // Finish getting everything else. + let formats = formats_cookie + .reply() + .map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?; + + Ok(XConnection { + xlib, + xinput2, + display, + xcb: Some(xcb), + atoms: Box::new(atoms), + default_screen, + timestamp: AtomicU32::new(0), + latest_error: Mutex::new(None), + monitor_handles: Mutex::new(None), + database: RwLock::new(database), + cursor_cache: Default::default(), + randr_version: (randr_version.major_version, randr_version.minor_version), + render_formats: formats, + xsettings_screen, + }) + } + + fn new_xsettings_screen( + xcb: &XCBConnection, + default_screen: usize, + ) -> Option { + // Fetch the _XSETTINGS_S[screen number] atom. + let xsettings_screen = xcb + .intern_atom(false, format!("_XSETTINGS_S{}", default_screen).as_bytes()) + .ok()? + .reply() + .ok()? + .atom; + + // Get PropertyNotify events from the XSETTINGS window. + // TODO: The XSETTINGS window here can change. In the future, listen for DestroyNotify on + // this window in order to accommodate for a changed window here. + let selector_window = xcb + .get_selection_owner(xsettings_screen) + .ok()? + .reply() + .ok()? + .owner; + + xcb.change_window_attributes( + selector_window, + &xproto::ChangeWindowAttributesAux::new() + .event_mask(xproto::EventMask::PROPERTY_CHANGE), + ) + .ok()? + .check() + .ok()?; + + Some(xsettings_screen) + } + + /// Checks whether an error has been triggered by the previous function calls. + #[inline] + pub fn check_errors(&self) -> Result<(), XError> { + let error = self.latest_error.lock().unwrap().take(); + if let Some(error) = error { + Err(error) + } else { + Ok(()) + } + } + + #[inline] + pub fn randr_version(&self) -> (u32, u32) { + self.randr_version + } + + /// Get the underlying XCB connection. + #[inline] + pub fn xcb_connection(&self) -> &XCBConnection { + self.xcb + .as_ref() + .expect("xcb_connection somehow called after drop?") + } + + /// Get the list of atoms. + #[inline] + pub fn atoms(&self) -> &Atoms { + &self.atoms + } + + /// Get the index of the default screen. + #[inline] + pub fn default_screen_index(&self) -> usize { + self.default_screen + } + + /// Get the default screen. + #[inline] + pub fn default_root(&self) -> &xproto::Screen { + &self.xcb_connection().setup().roots[self.default_screen] + } + + /// Get the resource database. + #[inline] + pub fn database(&self) -> RwLockReadGuard<'_, resource_manager::Database> { + self.database.read().unwrap_or_else(|e| e.into_inner()) + } + + /// Reload the resource database. + #[inline] + pub fn reload_database(&self) -> Result<(), super::X11Error> { + let database = resource_manager::new_from_default(self.xcb_connection())?; + *self.database.write().unwrap_or_else(|e| e.into_inner()) = database; + Ok(()) + } + + /// Get the latest timestamp. + #[inline] + pub fn timestamp(&self) -> u32 { + self.timestamp.load(Ordering::Relaxed) + } + + /// Set the last witnessed timestamp. + #[inline] + pub fn set_timestamp(&self, timestamp: u32) { + // Store the timestamp in the slot if it's greater than the last one. + let mut last_timestamp = self.timestamp.load(Ordering::Relaxed); + loop { + let wrapping_sub = + |a: xproto::Timestamp, b: xproto::Timestamp| (a as i32) - (b as i32); + + if wrapping_sub(timestamp, last_timestamp) <= 0 { + break; + } + + match self.timestamp.compare_exchange( + last_timestamp, + timestamp, + Ordering::Relaxed, + Ordering::Relaxed, + ) { + Ok(_) => break, + Err(x) => last_timestamp = x, + } + } + } + + /// Get the atom for Xsettings. + #[inline] + pub fn xsettings_screen(&self) -> Option { + self.xsettings_screen + } + + /// Get the data containing our rendering formats. + #[inline] + pub fn render_formats(&self) -> &render::QueryPictFormatsReply { + &self.render_formats + } + + /// Do we need to do an endian swap? + #[inline] + pub fn needs_endian_swap(&self) -> bool { + #[cfg(target_endian = "big")] + let endian = xproto::ImageOrder::MSB_FIRST; + #[cfg(not(target_endian = "big"))] + let endian = xproto::ImageOrder::LSB_FIRST; + + self.xcb_connection().setup().image_byte_order != endian + } +} + +impl fmt::Debug for XConnection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.display.fmt(f) + } +} + +impl Drop for XConnection { + #[inline] + fn drop(&mut self) { + self.xcb = None; + unsafe { (self.xlib.XCloseDisplay)(self.display) }; + } +} + +/// Error triggered by xlib. +#[derive(Debug, Clone)] +pub struct XError { + pub description: String, + pub error_code: u8, + pub request_code: u8, + pub minor_code: u8, +} + +impl Error for XError {} + +impl fmt::Display for XError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!( + formatter, + "X error: {} (code: {}, request code: {}, minor code: {})", + self.description, self.error_code, self.request_code, self.minor_code + ) + } +} + +/// Error returned if this system doesn't have XLib or can't create an X connection. +#[derive(Clone, Debug)] +pub enum XNotSupported { + /// Failed to load one or several shared libraries. + LibraryOpenError(ffi::OpenError), + + /// Connecting to the X server with `XOpenDisplay` failed. + XOpenDisplayFailed, // TODO: add better message. + + /// We encountered an error while converting the connection to XCB. + XcbConversionError(Arc), +} + +impl From for XNotSupported { + #[inline] + fn from(err: ffi::OpenError) -> XNotSupported { + XNotSupported::LibraryOpenError(err) + } +} + +impl XNotSupported { + fn description(&self) -> &'static str { + match self { + XNotSupported::LibraryOpenError(_) => { + "Failed to load one of xlib's shared libraries" + } + XNotSupported::XOpenDisplayFailed => "Failed to open connection to X server", + XNotSupported::XcbConversionError(_) => { + "Failed to convert Xlib connection to XCB" + } + } + } +} + +impl Error for XNotSupported { + #[inline] + fn source(&self) -> Option<&(dyn Error + 'static)> { + match *self { + XNotSupported::LibraryOpenError(ref err) => Some(err), + XNotSupported::XcbConversionError(ref err) => Some(&**err), + _ => None, + } + } +} + +impl fmt::Display for XNotSupported { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + formatter.write_str(self.description()) + } +} + +/// A newtype wrapper around a `ConnectError` that can't be accessed by downstream libraries. +/// +/// Without this, `x11rb` would become a public dependency. +#[derive(Debug)] +struct WrapConnectError(x11rb::rust_connection::ConnectError); + +impl fmt::Display for WrapConnectError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl Error for WrapConnectError { + // We can't implement `source()` here or otherwise risk exposing `x11rb`. +} diff --git a/rio-window/src/platform_impl/linux/x11/xsettings.rs b/rio-window/src/platform_impl/linux/x11/xsettings.rs new file mode 100644 index 00000000..bd53c004 --- /dev/null +++ b/rio-window/src/platform_impl/linux/x11/xsettings.rs @@ -0,0 +1,345 @@ +//! Parser for the xsettings data format. +//! +//! Some of this code is referenced from [here]. +//! +//! [here]: https://github.com/derat/xsettingsd + +use std::iter; +use std::num::NonZeroUsize; + +use x11rb::protocol::xproto::{self, ConnectionExt}; + +use super::atoms::*; +use super::XConnection; + +type Result = core::result::Result; + +const DPI_NAME: &[u8] = b"Xft/DPI"; +const DPI_MULTIPLIER: f64 = 1024.0; +const LITTLE_ENDIAN: u8 = b'l'; +const BIG_ENDIAN: u8 = b'B'; + +impl XConnection { + /// Get the DPI from XSettings. + pub(crate) fn xsettings_dpi( + &self, + xsettings_screen: xproto::Atom, + ) -> core::result::Result, super::X11Error> { + let atoms = self.atoms(); + + // Get the current owner of the screen's settings. + let owner = self + .xcb_connection() + .get_selection_owner(xsettings_screen)? + .reply()?; + + // Read the _XSETTINGS_SETTINGS property. + let data: Vec = self.get_property( + owner.owner, + atoms[_XSETTINGS_SETTINGS], + atoms[_XSETTINGS_SETTINGS], + )?; + + // Parse the property. + let dpi_setting = read_settings(&data)? + .find(|res| res.as_ref().map_or(true, |s| s.name == DPI_NAME)) + .transpose()?; + if let Some(dpi_setting) = dpi_setting { + let base_dpi = match dpi_setting.data { + SettingData::Integer(dpi) => dpi as f64, + SettingData::String(_) => { + return Err(ParserError::BadType(SettingType::String).into()) + } + SettingData::Color(_) => { + return Err(ParserError::BadType(SettingType::Color).into()) + } + }; + + Ok(Some(base_dpi / DPI_MULTIPLIER)) + } else { + Ok(None) + } + } +} + +/// Read over the settings in the block of data. +fn read_settings(data: &[u8]) -> Result>> + '_> { + // Create a parser. This automatically parses the first 8 bytes for metadata. + let mut parser = Parser::new(data)?; + + // Read the total number of settings. + let total_settings = parser.i32()?; + + // Iterate over the settings. + let iter = iter::repeat_with(move || Setting::parse(&mut parser)) + .take(total_settings as usize); + Ok(iter) +} + +/// A setting in the settings list. +struct Setting<'a> { + /// The name of the setting. + name: &'a [u8], + + /// The data contained in the setting. + data: SettingData<'a>, +} + +/// The data contained in a setting. +enum SettingData<'a> { + Integer(i32), + String(#[allow(dead_code)] &'a [u8]), + Color(#[allow(dead_code)] [i16; 4]), +} + +impl<'a> Setting<'a> { + /// Parse a new `SettingData`. + fn parse(parser: &mut Parser<'a>) -> Result { + // Read the type. + let ty: SettingType = parser.i8()?.try_into()?; + + // Read another byte of padding. + parser.advance(1)?; + + // Read the name of the setting. + let name_len = parser.i16()?; + let name = parser.advance(name_len as usize)?; + parser.pad(name.len(), 4)?; + + // Ignore the serial number. + parser.advance(4)?; + + let data = match ty { + SettingType::Integer => { + // Read a 32-bit integer. + SettingData::Integer(parser.i32()?) + } + + SettingType::String => { + // Read the data. + let data_len = parser.i32()?; + let data = parser.advance(data_len as usize)?; + parser.pad(data.len(), 4)?; + + SettingData::String(data) + } + + SettingType::Color => { + // Read i16's of color. + let (red, blue, green, alpha) = + (parser.i16()?, parser.i16()?, parser.i16()?, parser.i16()?); + + SettingData::Color([red, blue, green, alpha]) + } + }; + + Ok(Setting { name, data }) + } +} + +#[derive(Debug)] +pub enum SettingType { + Integer = 0, + String = 1, + Color = 2, +} + +impl TryFrom for SettingType { + type Error = ParserError; + + fn try_from(value: i8) -> Result { + Ok(match value { + 0 => Self::Integer, + 1 => Self::String, + 2 => Self::Color, + x => return Err(ParserError::InvalidType(x)), + }) + } +} + +/// Parser for the incoming byte stream. +struct Parser<'a> { + bytes: &'a [u8], + endianness: Endianness, +} + +impl<'a> Parser<'a> { + /// Create a new parser. + fn new(bytes: &'a [u8]) -> Result { + let (endianness, bytes) = bytes + .split_first() + .ok_or_else(|| ParserError::ran_out(1, 0))?; + let endianness = match *endianness { + BIG_ENDIAN => Endianness::Big, + LITTLE_ENDIAN => Endianness::Little, + _ => Endianness::native(), + }; + + Ok(Self { + // Ignore three bytes of padding and the four-byte serial. + bytes: bytes + .get(7..) + .ok_or_else(|| ParserError::ran_out(7, bytes.len()))?, + endianness, + }) + } + + /// Get a slice of bytes. + fn advance(&mut self, n: usize) -> Result<&'a [u8]> { + if n == 0 { + return Ok(&[]); + } + + if n > self.bytes.len() { + Err(ParserError::ran_out(n, self.bytes.len())) + } else { + let (part, rem) = self.bytes.split_at(n); + self.bytes = rem; + Ok(part) + } + } + + /// Skip some padding. + fn pad(&mut self, size: usize, pad: usize) -> Result<()> { + let advance = (pad - (size % pad)) % pad; + self.advance(advance)?; + Ok(()) + } + + /// Get a single byte. + fn i8(&mut self) -> Result { + self.advance(1).map(|s| s[0] as i8) + } + + /// Get two bytes. + fn i16(&mut self) -> Result { + self.advance(2).map(|s| { + let bytes: &[u8; 2] = s.try_into().unwrap(); + match self.endianness { + Endianness::Big => i16::from_be_bytes(*bytes), + Endianness::Little => i16::from_le_bytes(*bytes), + } + }) + } + + /// Get four bytes. + fn i32(&mut self) -> Result { + self.advance(4).map(|s| { + let bytes: &[u8; 4] = s.try_into().unwrap(); + match self.endianness { + Endianness::Big => i32::from_be_bytes(*bytes), + Endianness::Little => i32::from_le_bytes(*bytes), + } + }) + } +} + +/// Endianness of the incoming data. +enum Endianness { + Little, + Big, +} + +impl Endianness { + #[cfg(target_endian = "little")] + fn native() -> Self { + Endianness::Little + } + + #[cfg(target_endian = "big")] + fn native() -> Self { + Endianness::Big + } +} + +/// Parser errors. +#[allow(dead_code)] +#[derive(Debug)] +pub enum ParserError { + /// Ran out of bytes. + NoMoreBytes { + expected: NonZeroUsize, + found: usize, + }, + + /// Invalid type. + InvalidType(i8), + + /// Bad setting type. + BadType(SettingType), +} + +impl ParserError { + fn ran_out(expected: usize, found: usize) -> ParserError { + let expected = NonZeroUsize::new(expected).unwrap(); + Self::NoMoreBytes { expected, found } + } +} + +#[cfg(test)] +/// Tests for the XSETTINGS parser. +mod tests { + use super::*; + + const XSETTINGS: &str = include_str!("tests/xsettings.dat"); + + #[test] + fn empty() { + let err = match read_settings(&[]) { + Ok(_) => panic!(), + Err(err) => err, + }; + match err { + ParserError::NoMoreBytes { expected, found } => { + assert_eq!(expected.get(), 1); + assert_eq!(found, 0); + } + + _ => panic!(), + } + } + + #[test] + fn parse_xsettings() { + let data = XSETTINGS + .trim() + .split(',') + .map(|tok| { + let val = tok.strip_prefix("0x").unwrap(); + u8::from_str_radix(val, 16).unwrap() + }) + .collect::>(); + + let settings = read_settings(&data) + .unwrap() + .collect::>>() + .unwrap(); + + let dpi = settings.iter().find(|s| s.name == b"Xft/DPI").unwrap(); + assert_int(&dpi.data, 96 * 1024); + let hinting = settings.iter().find(|s| s.name == b"Xft/Hinting").unwrap(); + assert_int(&hinting.data, 1); + + let rgba = settings.iter().find(|s| s.name == b"Xft/RGBA").unwrap(); + assert_string(&rgba.data, "rgb"); + let lcd = settings + .iter() + .find(|s| s.name == b"Xft/Lcdfilter") + .unwrap(); + assert_string(&lcd.data, "lcddefault"); + } + + fn assert_string(dat: &SettingData<'_>, s: &str) { + match dat { + SettingData::String(left) => assert_eq!(*left, s.as_bytes()), + _ => panic!("invalid data type"), + } + } + + fn assert_int(dat: &SettingData<'_>, i: i32) { + match dat { + SettingData::Integer(left) => assert_eq!(*left, i), + _ => panic!("invalid data type"), + } + } +} diff --git a/rio-window/src/platform_impl/macos/app.rs b/rio-window/src/platform_impl/macos/app.rs new file mode 100644 index 00000000..acade5ec --- /dev/null +++ b/rio-window/src/platform_impl/macos/app.rs @@ -0,0 +1,99 @@ +#![allow(clippy::unnecessary_cast)] + +use objc2::{declare_class, msg_send, mutability, ClassType, DeclaredClass}; +use objc2_app_kit::{ + NSApplication, NSEvent, NSEventModifierFlags, NSEventType, NSResponder, +}; +use objc2_foundation::{MainThreadMarker, NSObject}; + +use super::app_delegate::ApplicationDelegate; +use crate::event::{DeviceEvent, ElementState}; + +declare_class!( + pub(super) struct WinitApplication; + + unsafe impl ClassType for WinitApplication { + #[inherits(NSResponder, NSObject)] + type Super = NSApplication; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitApplication"; + } + + impl DeclaredClass for WinitApplication {} + + unsafe impl WinitApplication { + // Normally, holding Cmd + any key never sends us a `keyUp` event for that key. + // Overriding `sendEvent:` like this fixes that. (https://stackoverflow.com/a/15294196) + // Fun fact: Firefox still has this bug! (https://bugzilla.mozilla.org/show_bug.cgi?id=1299553) + #[method(sendEvent:)] + fn send_event(&self, event: &NSEvent) { + // For posterity, there are some undocumented event types + // (https://github.com/servo/cocoa-rs/issues/155) + // but that doesn't really matter here. + let event_type = unsafe { event.r#type() }; + let modifier_flags = unsafe { event.modifierFlags() }; + if event_type == NSEventType::KeyUp + && modifier_flags.contains(NSEventModifierFlags::NSEventModifierFlagCommand) + { + if let Some(key_window) = self.keyWindow() { + key_window.sendEvent(event); + } + } else { + let delegate = ApplicationDelegate::get(MainThreadMarker::from(self)); + maybe_dispatch_device_event(&delegate, event); + unsafe { msg_send![super(self), sendEvent: event] } + } + } + } +); + +fn maybe_dispatch_device_event(delegate: &ApplicationDelegate, event: &NSEvent) { + let event_type = unsafe { event.r#type() }; + #[allow(non_upper_case_globals)] + match event_type { + NSEventType::MouseMoved + | NSEventType::LeftMouseDragged + | NSEventType::OtherMouseDragged + | NSEventType::RightMouseDragged => { + let delta_x = unsafe { event.deltaX() } as f64; + let delta_y = unsafe { event.deltaY() } as f64; + + if delta_x != 0.0 { + delegate.queue_device_event(DeviceEvent::Motion { + axis: 0, + value: delta_x, + }); + } + + if delta_y != 0.0 { + delegate.queue_device_event(DeviceEvent::Motion { + axis: 1, + value: delta_y, + }) + } + + if delta_x != 0.0 || delta_y != 0.0 { + delegate.queue_device_event(DeviceEvent::MouseMotion { + delta: (delta_x, delta_y), + }); + } + } + NSEventType::LeftMouseDown + | NSEventType::RightMouseDown + | NSEventType::OtherMouseDown => { + delegate.queue_device_event(DeviceEvent::Button { + button: unsafe { event.buttonNumber() } as u32, + state: ElementState::Pressed, + }); + } + NSEventType::LeftMouseUp + | NSEventType::RightMouseUp + | NSEventType::OtherMouseUp => { + delegate.queue_device_event(DeviceEvent::Button { + button: unsafe { event.buttonNumber() } as u32, + state: ElementState::Released, + }); + } + _ => (), + } +} diff --git a/rio-window/src/platform_impl/macos/app_delegate.rs b/rio-window/src/platform_impl/macos/app_delegate.rs new file mode 100644 index 00000000..0004abe8 --- /dev/null +++ b/rio-window/src/platform_impl/macos/app_delegate.rs @@ -0,0 +1,525 @@ +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::mem; +use std::rc::Weak; +use std::sync::{Arc, Mutex}; +use std::time::Instant; + +use objc2::rc::Retained; +use objc2::runtime::AnyObject; +use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; +use objc2_app_kit::{ + NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate, +}; +use objc2_foundation::{ + MainThreadMarker, NSArray, NSObject, NSObjectProtocol, NSSize, NSURL, +}; + +use super::event_handler::EventHandler; +use super::event_loop::{stop_app_immediately, ActiveEventLoop, PanicInfo}; +use super::observer::{EventLoopWaker, RunLoop}; +use super::window::WinitWindow; +use super::{menu, WindowId, DEVICE_ID}; +use crate::dpi::PhysicalSize; +use crate::event::{DeviceEvent, Event, InnerSizeWriter, StartCause, WindowEvent}; +use crate::event_loop::{ActiveEventLoop as RootActiveEventLoop, ControlFlow}; +use crate::window::WindowId as RootWindowId; + +#[derive(Debug)] +struct Policy(NSApplicationActivationPolicy); + +impl Default for Policy { + fn default() -> Self { + Self(NSApplicationActivationPolicy::Regular) + } +} + +#[derive(Debug, Default)] +pub(super) struct State { + activation_policy: Policy, + default_menu: bool, + activate_ignoring_other_apps: bool, + event_handler: EventHandler, + stop_on_launch: Cell, + stop_before_wait: Cell, + stop_after_wait: Cell, + stop_on_redraw: Cell, + /// Whether `applicationDidFinishLaunching:` has been run or not. + is_launched: Cell, + /// Whether an `EventLoop` is currently running. + is_running: Cell, + /// Whether the user has requested the event loop to exit. + exit: Cell, + control_flow: Cell, + waker: RefCell, + start_time: Cell>, + wait_timeout: Cell>, + pending_events: RefCell>, + pending_redraw: RefCell>, + // NOTE: This is strongly referenced by our `NSWindowDelegate` and our `NSView` subclass, and + // as such should be careful to not add fields that, in turn, strongly reference those. +} + +declare_class!( + #[derive(Debug)] + pub(super) struct ApplicationDelegate; + + unsafe impl ClassType for ApplicationDelegate { + type Super = NSObject; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitApplicationDelegate"; + } + + impl DeclaredClass for ApplicationDelegate { + type Ivars = State; + } + + unsafe impl NSObjectProtocol for ApplicationDelegate {} + + unsafe impl NSApplicationDelegate for ApplicationDelegate { + // NOTE: This will, globally, only be run once, no matter how many + // `EventLoop`s the user creates. + #[method(applicationDidFinishLaunching:)] + fn did_finish_launching(&self, _sender: Option<&AnyObject>) { + trace_scope!("applicationDidFinishLaunching:"); + self.ivars().is_launched.set(true); + + let mtm = MainThreadMarker::from(self); + let app = NSApplication::sharedApplication(mtm); + // We need to delay setting the activation policy and activating the app + // until `applicationDidFinishLaunching` has been called. Otherwise the + // menu bar is initially unresponsive on macOS 10.15. + app.setActivationPolicy(self.ivars().activation_policy.0); + + window_activation_hack(&app); + #[allow(deprecated)] + app.activateIgnoringOtherApps(self.ivars().activate_ignoring_other_apps); + + if self.ivars().default_menu { + // The menubar initialization should be before the `NewEvents` event, to allow + // overriding of the default menu even if it's created + menu::initialize(&app); + } + + self.ivars().waker.borrow_mut().start(); + + self.set_is_running(true); + self.dispatch_init_events(); + + // If the application is being launched via `EventLoop::pump_app_events()` then we'll + // want to stop the app once it is launched (and return to the external loop) + // + // In this case we still want to consider Winit's `EventLoop` to be "running", + // so we call `start_running()` above. + if self.ivars().stop_on_launch.get() { + // NOTE: the original idea had been to only stop the underlying `RunLoop` + // for the app but that didn't work as expected (`-[NSApplication run]` + // effectively ignored the attempt to stop the RunLoop and re-started it). + // + // So we return from `pump_events` by stopping the application. + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + } + + #[method(applicationWillTerminate:)] + fn will_terminate(&self, _sender: Option<&AnyObject>) { + trace_scope!("applicationWillTerminate:"); + // TODO: Notify every window that it will be destroyed, like done in iOS? + self.internal_exit(); + } + + #[method(application:openURLs:)] + fn application_open_urls(&self, _application: &NSApplication, urls: &NSArray) { + trace_scope!("Trigger `application:openURLs:`"); + + let mut open_urls: Vec = Vec::new(); + urls.into_iter().for_each(|url| { + unsafe { + if let Some(ns_str) = url.absoluteString() { + open_urls.push(ns_str.to_string()); + } + } + }); + + self.open_urls(open_urls); + + trace_scope!("Completed `application:openURLs:`"); + } + } +); + +impl ApplicationDelegate { + pub(super) fn new( + mtm: MainThreadMarker, + activation_policy: NSApplicationActivationPolicy, + default_menu: bool, + activate_ignoring_other_apps: bool, + ) -> Retained { + let this = mtm.alloc().set_ivars(State { + activation_policy: Policy(activation_policy), + default_menu, + activate_ignoring_other_apps, + ..Default::default() + }); + unsafe { msg_send_id![super(this), init] } + } + + pub fn get(mtm: MainThreadMarker) -> Retained { + let app = NSApplication::sharedApplication(mtm); + let delegate = unsafe { app.delegate() } + .expect("a delegate was not configured on the application"); + if delegate.is_kind_of::() { + // SAFETY: Just checked that the delegate is an instance of `ApplicationDelegate` + unsafe { Retained::cast(delegate) } + } else { + panic!("tried to get a delegate that was not the one Winit has registered") + } + } + + /// Place the event handler in the application delegate for the duration + /// of the given closure. + pub fn set_event_handler( + &self, + handler: impl FnMut(Event, &RootActiveEventLoop), + closure: impl FnOnce() -> R, + ) -> R { + self.ivars().event_handler.set(handler, closure) + } + + /// If `pump_events` is called to progress the event loop then we + /// bootstrap the event loop via `-[NSApplication run]` but will use + /// `CFRunLoopRunInMode` for subsequent calls to `pump_events`. + pub fn set_stop_on_launch(&self) { + self.ivars().stop_on_launch.set(true); + } + + pub fn set_stop_before_wait(&self, value: bool) { + self.ivars().stop_before_wait.set(value) + } + + pub fn set_stop_after_wait(&self, value: bool) { + self.ivars().stop_after_wait.set(value) + } + + pub fn set_stop_on_redraw(&self, value: bool) { + self.ivars().stop_on_redraw.set(value) + } + + pub fn set_wait_timeout(&self, value: Option) { + self.ivars().wait_timeout.set(value) + } + + /// Clears the `running` state and resets the `control_flow` state when an `EventLoop` exits. + /// + /// NOTE: that if the `NSApplication` has been launched then that state is preserved, + /// and we won't need to re-launch the app if subsequent EventLoops are run. + pub fn internal_exit(&self) { + self.handle_event(Event::LoopExiting); + + self.set_is_running(false); + self.set_stop_on_redraw(false); + self.set_stop_before_wait(false); + self.set_stop_after_wait(false); + self.set_wait_timeout(None); + } + + pub fn is_launched(&self) -> bool { + self.ivars().is_launched.get() + } + + pub fn set_is_running(&self, value: bool) { + self.ivars().is_running.set(value) + } + + pub fn is_running(&self) -> bool { + self.ivars().is_running.get() + } + + pub fn exit(&self) { + self.ivars().exit.set(true) + } + + pub fn clear_exit(&self) { + self.ivars().exit.set(false) + } + + pub fn exiting(&self) -> bool { + self.ivars().exit.get() + } + + pub fn set_control_flow(&self, value: ControlFlow) { + self.ivars().control_flow.set(value) + } + + pub fn control_flow(&self) -> ControlFlow { + self.ivars().control_flow.get() + } + + pub fn queue_window_event(&self, window_id: WindowId, event: WindowEvent) { + self.ivars() + .pending_events + .borrow_mut() + .push_back(QueuedEvent::WindowEvent(window_id, event)); + } + + pub fn queue_device_event(&self, event: DeviceEvent) { + self.ivars() + .pending_events + .borrow_mut() + .push_back(QueuedEvent::DeviceEvent(event)); + } + + pub fn queue_static_scale_factor_changed_event( + &self, + window: Retained, + suggested_size: PhysicalSize, + scale_factor: f64, + ) { + self.ivars().pending_events.borrow_mut().push_back( + QueuedEvent::ScaleFactorChanged { + window, + suggested_size, + scale_factor, + }, + ); + } + + pub fn handle_redraw(&self, window_id: WindowId) { + let mtm = MainThreadMarker::from(self); + // Redraw request might come out of order from the OS. + // -> Don't go back into the event handler when our callstack originates from there + if !self.ivars().event_handler.in_use() { + self.handle_event(Event::WindowEvent { + window_id: RootWindowId(window_id), + event: WindowEvent::RedrawRequested, + }); + + // `pump_events` will request to stop immediately _after_ dispatching RedrawRequested + // events as a way to ensure that `pump_events` can't block an external loop + // indefinitely + if self.ivars().stop_on_redraw.get() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + } + } + + pub fn queue_redraw(&self, window_id: WindowId) { + let mut pending_redraw = self.ivars().pending_redraw.borrow_mut(); + if !pending_redraw.contains(&window_id) { + pending_redraw.push(window_id); + } + unsafe { RunLoop::get() }.wakeup(); + } + + fn handle_event(&self, event: Event) { + self.ivars() + .event_handler + .handle_event(event, &ActiveEventLoop::new_root(self.retain())) + } + + /// dispatch `NewEvents(Init)` + `Resumed` + pub fn dispatch_init_events(&self) { + self.handle_event(Event::NewEvents(StartCause::Init)); + // NB: For consistency all platforms must emit a 'resumed' event even though macOS + // applications don't themselves have a formal suspend/resume lifecycle. + self.handle_event(Event::Resumed); + } + + pub fn open_urls(&self, urls: Vec) { + self.handle_event(Event::Opened { urls }); + } + + // Called by RunLoopObserver after finishing waiting for new events + pub fn wakeup(&self, panic_info: Weak) { + let mtm = MainThreadMarker::from(self); + let panic_info = panic_info.upgrade().expect( + "The panic info must exist here. This failure indicates a developer error.", + ); + + // Return when in event handler due to https://github.com/rust-windowing/winit/issues/1779 + if panic_info.is_panicking() + || !self.ivars().event_handler.ready() + || !self.is_running() + { + return; + } + + if self.ivars().stop_after_wait.get() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + + let start = self.ivars().start_time.get().unwrap(); + let cause = match self.control_flow() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { + start, + requested_resume: None, + }, + ControlFlow::WaitUntil(requested_resume) => { + if Instant::now() >= requested_resume { + StartCause::ResumeTimeReached { + start, + requested_resume, + } + } else { + StartCause::WaitCancelled { + start, + requested_resume: Some(requested_resume), + } + } + } + }; + + self.handle_event(Event::NewEvents(cause)); + } + + // Called by RunLoopObserver before waiting for new events + pub fn cleared(&self, panic_info: Weak) { + let mtm = MainThreadMarker::from(self); + let panic_info = panic_info.upgrade().expect( + "The panic info must exist here. This failure indicates a developer error.", + ); + + // Return when in event handler due to https://github.com/rust-windowing/winit/issues/1779 + // XXX: how does it make sense that `event_handler.ready()` can ever return `false` here if + // we're about to return to the `CFRunLoop` to poll for new events? + if panic_info.is_panicking() + || !self.ivars().event_handler.ready() + || !self.is_running() + { + return; + } + + self.handle_event(Event::UserEvent(HandlePendingUserEvents)); + + let events = mem::take(&mut *self.ivars().pending_events.borrow_mut()); + for event in events { + match event { + QueuedEvent::WindowEvent(window_id, event) => { + self.handle_event(Event::WindowEvent { + window_id: RootWindowId(window_id), + event, + }); + } + QueuedEvent::DeviceEvent(event) => { + self.handle_event(Event::DeviceEvent { + device_id: DEVICE_ID, + event, + }); + } + QueuedEvent::ScaleFactorChanged { + window, + suggested_size, + scale_factor, + } => { + let new_inner_size = Arc::new(Mutex::new(suggested_size)); + let scale_factor_changed_event = Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade( + &new_inner_size, + )), + }, + }; + + self.handle_event(scale_factor_changed_event); + + let physical_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); + if physical_size != suggested_size { + let logical_size = physical_size.to_logical(scale_factor); + let size = NSSize::new(logical_size.width, logical_size.height); + window.setContentSize(size); + } + + let resized_event = Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Resized(physical_size), + }; + self.handle_event(resized_event); + } + } + } + + let redraw = mem::take(&mut *self.ivars().pending_redraw.borrow_mut()); + for window_id in redraw { + self.handle_event(Event::WindowEvent { + window_id: RootWindowId(window_id), + event: WindowEvent::RedrawRequested, + }); + } + + self.handle_event(Event::AboutToWait); + + if self.exiting() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + + if self.ivars().stop_before_wait.get() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + self.ivars().start_time.set(Some(Instant::now())); + let wait_timeout = self.ivars().wait_timeout.get(); // configured by pump_events + let app_timeout = match self.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Instant::now()), + ControlFlow::WaitUntil(instant) => Some(instant), + }; + self.ivars() + .waker + .borrow_mut() + .start_at(min_timeout(wait_timeout, app_timeout)); + } +} + +#[derive(Debug)] +pub(crate) enum QueuedEvent { + WindowEvent(WindowId, WindowEvent), + DeviceEvent(DeviceEvent), + ScaleFactorChanged { + window: Retained, + suggested_size: PhysicalSize, + scale_factor: f64, + }, +} + +#[derive(Debug)] +pub(crate) struct HandlePendingUserEvents; + +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| { + b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout))) + }) +} + +/// A hack to make activation of multiple windows work when creating them before +/// `applicationDidFinishLaunching:` / `Event::Event::NewEvents(StartCause::Init)`. +/// +/// Alternative to this would be the user calling `window.set_visible(true)` in +/// `StartCause::Init`. +/// +/// If this becomes too bothersome to maintain, it can probably be removed +/// without too much damage. +fn window_activation_hack(app: &NSApplication) { + // TODO: Proper ordering of the windows + app.windows().into_iter().for_each(|window| { + // Call `makeKeyAndOrderFront` if it was called on the window in `WinitWindow::new` + // This way we preserve the user's desired initial visibility status + // TODO: Also filter on the type/"level" of the window, and maybe other things? + if window.isVisible() { + tracing::trace!("Activating visible window"); + window.makeKeyAndOrderFront(None); + } else { + tracing::trace!("Skipping activating invisible window"); + } + }) +} diff --git a/rio-window/src/platform_impl/macos/appkit/appearance.rs b/rio-window/src/platform_impl/macos/appkit/appearance.rs new file mode 100644 index 00000000..6db7c6d0 --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/appearance.rs @@ -0,0 +1,28 @@ +use icrate::Foundation::{NSArray, NSObject, NSString}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, mutability, ClassType}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSAppearance; + + unsafe impl ClassType for NSAppearance { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + } +); + +type NSAppearanceName = NSString; + +extern_methods!( + unsafe impl NSAppearance { + #[method_id(appearanceNamed:)] + pub fn appearanceNamed(name: &NSAppearanceName) -> Id; + + #[method_id(bestMatchFromAppearancesWithNames:)] + pub fn bestMatchFromAppearancesWithNames( + &self, + appearances: &NSArray, + ) -> Id; + } +); diff --git a/rio-window/src/platform_impl/macos/appkit/application.rs b/rio-window/src/platform_impl/macos/appkit/application.rs new file mode 100644 index 00000000..f5f2e77d --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/application.rs @@ -0,0 +1,140 @@ +use icrate::Foundation::{MainThreadMarker, NSArray, NSInteger, NSObject, NSUInteger}; +use objc2::rc::Id; +use objc2::runtime::AnyObject; +use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType}; +use objc2::{Encode, Encoding}; + +use super::{NSAppearance, NSEvent, NSMenu, NSResponder, NSWindow}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSApplication; + + unsafe impl ClassType for NSApplication { + #[inherits(NSObject)] + type Super = NSResponder; + type Mutability = mutability::InteriorMutable; + } +); + +pub(crate) fn NSApp() -> Id { + // TODO: Only allow access from main thread + NSApplication::shared(unsafe { MainThreadMarker::new_unchecked() }) +} + +extern_methods!( + unsafe impl NSApplication { + /// This can only be called on the main thread since it may initialize + /// the application and since it's parameters may be changed by the main + /// thread at any time (hence it is only safe to access on the main thread). + pub fn shared(_mtm: MainThreadMarker) -> Id { + let app: Option<_> = unsafe { msg_send_id![Self::class(), sharedApplication] }; + // SAFETY: `sharedApplication` always initializes the app if it isn't already + unsafe { app.unwrap_unchecked() } + } + + #[method_id(currentEvent)] + pub fn currentEvent(&self) -> Option>; + + #[method(postEvent:atStart:)] + pub fn postEvent_atStart(&self, event: &NSEvent, front_of_queue: bool); + + #[method(presentationOptions)] + pub fn presentationOptions(&self) -> NSApplicationPresentationOptions; + + #[method_id(windows)] + pub fn windows(&self) -> Id>; + + #[method_id(keyWindow)] + pub fn keyWindow(&self) -> Option>; + + // TODO: NSApplicationDelegate + #[method(setDelegate:)] + pub fn setDelegate(&self, delegate: &AnyObject); + + #[method(setPresentationOptions:)] + pub fn setPresentationOptions(&self, options: NSApplicationPresentationOptions); + + #[method(hide:)] + pub fn hide(&self, sender: Option<&AnyObject>); + + #[method(orderFrontCharacterPalette:)] + #[allow(dead_code)] + pub fn orderFrontCharacterPalette(&self, sender: Option<&AnyObject>); + + #[method(hideOtherApplications:)] + pub fn hideOtherApplications(&self, sender: Option<&AnyObject>); + + #[method(stop:)] + pub fn stop(&self, sender: Option<&AnyObject>); + + #[method(activateIgnoringOtherApps:)] + pub fn activateIgnoringOtherApps(&self, ignore: bool); + + #[method(requestUserAttention:)] + pub fn requestUserAttention(&self, type_: NSRequestUserAttentionType) -> NSInteger; + + #[method(setActivationPolicy:)] + pub fn setActivationPolicy(&self, policy: NSApplicationActivationPolicy) -> bool; + + #[method(setMainMenu:)] + pub fn setMainMenu(&self, menu: &NSMenu); + + #[method_id(effectiveAppearance)] + pub fn effectiveAppearance(&self) -> Id; + + #[method(setAppearance:)] + pub fn setAppearance(&self, appearance: Option<&NSAppearance>); + + #[method(run)] + pub unsafe fn run(&self); + } +); + +#[allow(dead_code)] +#[repr(isize)] // NSInteger +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum NSApplicationActivationPolicy { + NSApplicationActivationPolicyRegular = 0, + NSApplicationActivationPolicyAccessory = 1, + NSApplicationActivationPolicyProhibited = 2, + NSApplicationActivationPolicyERROR = -1, +} + +unsafe impl Encode for NSApplicationActivationPolicy { + const ENCODING: Encoding = NSInteger::ENCODING; +} + +bitflags! { + #[derive(Debug, Clone, Copy)] + pub struct NSApplicationPresentationOptions: NSUInteger { + const NSApplicationPresentationDefault = 0; + const NSApplicationPresentationAutoHideDock = 1 << 0; + const NSApplicationPresentationHideDock = 1 << 1; + const NSApplicationPresentationAutoHideMenuBar = 1 << 2; + const NSApplicationPresentationHideMenuBar = 1 << 3; + const NSApplicationPresentationDisableAppleMenu = 1 << 4; + const NSApplicationPresentationDisableProcessSwitching = 1 << 5; + const NSApplicationPresentationDisableForceQuit = 1 << 6; + const NSApplicationPresentationDisableSessionTermination = 1 << 7; + const NSApplicationPresentationDisableHideApplication = 1 << 8; + const NSApplicationPresentationDisableMenuBarTransparency = 1 << 9; + const NSApplicationPresentationFullScreen = 1 << 10; + const NSApplicationPresentationAutoHideToolbar = 1 << 11; + } +} + +unsafe impl Encode for NSApplicationPresentationOptions { + const ENCODING: Encoding = NSUInteger::ENCODING; +} + +#[repr(usize)] // NSUInteger +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum NSRequestUserAttentionType { + NSCriticalRequest = 0, + NSInformationalRequest = 10, +} + +unsafe impl Encode for NSRequestUserAttentionType { + const ENCODING: Encoding = NSUInteger::ENCODING; +} diff --git a/rio-window/src/platform_impl/macos/appkit/button.rs b/rio-window/src/platform_impl/macos/appkit/button.rs new file mode 100644 index 00000000..eb3c57a5 --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/button.rs @@ -0,0 +1,15 @@ +use icrate::Foundation::NSObject; +use objc2::{extern_class, mutability, ClassType}; + +use super::{NSControl, NSResponder, NSView}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSButton; + + unsafe impl ClassType for NSButton { + #[inherits(NSView, NSResponder, NSObject)] + type Super = NSControl; + type Mutability = mutability::InteriorMutable; + } +); diff --git a/rio-window/src/platform_impl/macos/appkit/color.rs b/rio-window/src/platform_impl/macos/appkit/color.rs new file mode 100644 index 00000000..b5477a1e --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/color.rs @@ -0,0 +1,28 @@ +use icrate::Foundation::NSObject; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, mutability, ClassType}; + +extern_class!( + /// An object that stores color data and sometimes opacity (alpha value). + /// + /// + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSColor; + + unsafe impl ClassType for NSColor { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + } +); + +// SAFETY: Documentation clearly states: +// > Color objects are immutable and thread-safe +unsafe impl Send for NSColor {} +unsafe impl Sync for NSColor {} + +extern_methods!( + unsafe impl NSColor { + #[method_id(clearColor)] + pub fn clear() -> Id; + } +); diff --git a/rio-window/src/platform_impl/macos/appkit/control.rs b/rio-window/src/platform_impl/macos/appkit/control.rs new file mode 100644 index 00000000..d38f4b10 --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/control.rs @@ -0,0 +1,25 @@ +use icrate::Foundation::NSObject; +use objc2::{extern_class, extern_methods, mutability, ClassType}; + +use super::{NSResponder, NSView}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSControl; + + unsafe impl ClassType for NSControl { + #[inherits(NSResponder, NSObject)] + type Super = NSView; + type Mutability = mutability::InteriorMutable; + } +); + +extern_methods!( + unsafe impl NSControl { + #[method(setEnabled:)] + pub fn setEnabled(&self, enabled: bool); + + #[method(isEnabled)] + pub fn isEnabled(&self) -> bool; + } +); diff --git a/rio-window/src/platform_impl/macos/appkit/cursor.rs b/rio-window/src/platform_impl/macos/appkit/cursor.rs new file mode 100644 index 00000000..6377ad42 --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/cursor.rs @@ -0,0 +1,241 @@ +use once_cell::sync::Lazy; + +use icrate::ns_string; +use icrate::Foundation::{ + NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSString, +}; +use objc2::rc::{DefaultId, Id}; +use objc2::runtime::Sel; +use objc2::{extern_class, extern_methods, msg_send_id, mutability, sel, ClassType}; + +use super::NSImage; +use crate::window::CursorIcon; + +extern_class!( + /// + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSCursor; + + unsafe impl ClassType for NSCursor { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + } +); + +// SAFETY: NSCursor is immutable, stated here: +// https://developer.apple.com/documentation/appkit/nscursor/1527062-image?language=objc +unsafe impl Send for NSCursor {} +unsafe impl Sync for NSCursor {} + +macro_rules! def_cursor { + {$( + $(#[$($m:meta)*])* + pub fn $name:ident(); + )*} => {$( + $(#[$($m)*])* + pub fn $name() -> Id { + unsafe { msg_send_id![Self::class(), $name] } + } + )*}; +} + +macro_rules! def_undocumented_cursor { + {$( + $(#[$($m:meta)*])* + pub fn $name:ident(); + )*} => {$( + $(#[$($m)*])* + pub fn $name() -> Id { + unsafe { Self::from_selector(sel!($name)).unwrap_or_else(|| Default::default()) } + } + )*}; +} + +extern_methods!( + /// Documented cursors + unsafe impl NSCursor { + def_cursor!( + pub fn arrowCursor(); + pub fn pointingHandCursor(); + pub fn openHandCursor(); + pub fn closedHandCursor(); + pub fn IBeamCursor(); + pub fn IBeamCursorForVerticalLayout(); + pub fn dragCopyCursor(); + pub fn dragLinkCursor(); + pub fn operationNotAllowedCursor(); + pub fn contextualMenuCursor(); + pub fn crosshairCursor(); + pub fn resizeRightCursor(); + pub fn resizeUpCursor(); + pub fn resizeLeftCursor(); + pub fn resizeDownCursor(); + pub fn resizeLeftRightCursor(); + pub fn resizeUpDownCursor(); + ); + + // Creating cursors should be thread-safe, though using them for anything probably isn't. + pub fn new(image: &NSImage, hotSpot: NSPoint) -> Id { + unsafe { msg_send_id![Self::alloc(), initWithImage: image, hotSpot: hotSpot] } + } + + pub fn invisible() -> Id { + // 16x16 GIF data for invisible cursor + // You can reproduce this via ImageMagick. + // $ convert -size 16x16 xc:none cursor.gif + static CURSOR_BYTES: &[u8] = &[ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x10, 0x00, 0x10, 0x00, 0xF0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2C, + 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x02, 0x0E, 0x84, 0x8F, 0xA9, + 0xCB, 0xED, 0x0F, 0xA3, 0x9C, 0xB4, 0xDA, 0x8B, 0xB3, 0x3E, 0x05, 0x00, 0x3B, + ]; + + static CURSOR: Lazy> = Lazy::new(|| { + // TODO: Consider using `dataWithBytesNoCopy:` + let data = NSData::with_bytes(CURSOR_BYTES); + let image = NSImage::new_with_data(&data); + NSCursor::new(&image, NSPoint::new(0.0, 0.0)) + }); + + CURSOR.clone() + } + } + + /// Undocumented cursors + unsafe impl NSCursor { + #[method(respondsToSelector:)] + fn class_responds_to(sel: Sel) -> bool; + + #[method_id(performSelector:)] + unsafe fn from_selector_unchecked(sel: Sel) -> Id; + + unsafe fn from_selector(sel: Sel) -> Option> { + if Self::class_responds_to(sel) { + Some(unsafe { Self::from_selector_unchecked(sel) }) + } else { + warn!("Cursor `{:?}` appears to be invalid", sel); + None + } + } + + def_undocumented_cursor!( + // Undocumented cursors: https://stackoverflow.com/a/46635398/5435443 + pub fn _helpCursor(); + pub fn _zoomInCursor(); + pub fn _zoomOutCursor(); + pub fn _windowResizeNorthEastCursor(); + pub fn _windowResizeNorthWestCursor(); + pub fn _windowResizeSouthEastCursor(); + pub fn _windowResizeSouthWestCursor(); + pub fn _windowResizeNorthEastSouthWestCursor(); + pub fn _windowResizeNorthWestSouthEastCursor(); + + // While these two are available, the former just loads a white arrow, + // and the latter loads an ugly deflated beachball! + // pub fn _moveCursor(); + // pub fn _waitCursor(); + + // An even more undocumented cursor... + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=522349 + pub fn busyButClickableCursor(); + ); + } + + /// Webkit cursors + unsafe impl NSCursor { + // Note that loading `busybutclickable` with this code won't animate + // the frames; instead you'll just get them all in a column. + unsafe fn load_webkit_cursor(name: &NSString) -> Id { + // Snatch a cursor from WebKit; They fit the style of the native + // cursors, and will seem completely standard to macOS users. + // + // https://stackoverflow.com/a/21786835/5435443 + let root = ns_string!("/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors"); + let cursor_path = root.stringByAppendingPathComponent(name); + + let pdf_path = cursor_path.stringByAppendingPathComponent(ns_string!("cursor.pdf")); + let image = NSImage::new_by_referencing_file(&pdf_path); + + // TODO: Handle PLists better + let info_path = cursor_path.stringByAppendingPathComponent(ns_string!("info.plist")); + let info: Id> = unsafe { + msg_send_id![ + >::class(), + dictionaryWithContentsOfFile: &*info_path, + ] + }; + let mut x = 0.0; + if let Some(n) = info.get(&*ns_string!("hotx")) { + if n.is_kind_of::() { + let ptr: *const NSObject = n; + let ptr: *const NSNumber = ptr.cast(); + x = unsafe { &*ptr }.as_cgfloat() + } + } + let mut y = 0.0; + if let Some(n) = info.get(&*ns_string!("hotx")) { + if n.is_kind_of::() { + let ptr: *const NSObject = n; + let ptr: *const NSNumber = ptr.cast(); + y = unsafe { &*ptr }.as_cgfloat() + } + } + + let hotspot = NSPoint::new(x, y); + Self::new(&image, hotspot) + } + + pub fn moveCursor() -> Id { + unsafe { Self::load_webkit_cursor(ns_string!("move")) } + } + + pub fn cellCursor() -> Id { + unsafe { Self::load_webkit_cursor(ns_string!("cell")) } + } + } +); + +impl NSCursor { + pub fn from_icon(icon: CursorIcon) -> Id { + match icon { + CursorIcon::Default => Default::default(), + CursorIcon::Pointer => Self::pointingHandCursor(), + CursorIcon::Grab => Self::openHandCursor(), + CursorIcon::Grabbing => Self::closedHandCursor(), + CursorIcon::Text => Self::IBeamCursor(), + CursorIcon::VerticalText => Self::IBeamCursorForVerticalLayout(), + CursorIcon::Copy => Self::dragCopyCursor(), + CursorIcon::Alias => Self::dragLinkCursor(), + CursorIcon::NotAllowed | CursorIcon::NoDrop => Self::operationNotAllowedCursor(), + CursorIcon::ContextMenu => Self::contextualMenuCursor(), + CursorIcon::Crosshair => Self::crosshairCursor(), + CursorIcon::EResize => Self::resizeRightCursor(), + CursorIcon::NResize => Self::resizeUpCursor(), + CursorIcon::WResize => Self::resizeLeftCursor(), + CursorIcon::SResize => Self::resizeDownCursor(), + CursorIcon::EwResize | CursorIcon::ColResize => Self::resizeLeftRightCursor(), + CursorIcon::NsResize | CursorIcon::RowResize => Self::resizeUpDownCursor(), + CursorIcon::Help => Self::_helpCursor(), + CursorIcon::ZoomIn => Self::_zoomInCursor(), + CursorIcon::ZoomOut => Self::_zoomOutCursor(), + CursorIcon::NeResize => Self::_windowResizeNorthEastCursor(), + CursorIcon::NwResize => Self::_windowResizeNorthWestCursor(), + CursorIcon::SeResize => Self::_windowResizeSouthEastCursor(), + CursorIcon::SwResize => Self::_windowResizeSouthWestCursor(), + CursorIcon::NeswResize => Self::_windowResizeNorthEastSouthWestCursor(), + CursorIcon::NwseResize => Self::_windowResizeNorthWestSouthEastCursor(), + // This is the wrong semantics for `Wait`, but it's the same as + // what's used in Safari and Chrome. + CursorIcon::Wait | CursorIcon::Progress => Self::busyButClickableCursor(), + CursorIcon::Move | CursorIcon::AllScroll => Self::moveCursor(), + CursorIcon::Cell => Self::cellCursor(), + _ => Default::default(), + } + } +} + +impl DefaultId for NSCursor { + fn default_id() -> Id { + Self::arrowCursor() + } +} diff --git a/rio-window/src/platform_impl/macos/appkit/event.rs b/rio-window/src/platform_impl/macos/appkit/event.rs new file mode 100644 index 00000000..f7496399 --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/event.rs @@ -0,0 +1,308 @@ +use std::os::raw::c_ushort; + +use icrate::Foundation::{ + CGFloat, NSCopying, NSInteger, NSObject, NSPoint, NSString, NSTimeInterval, NSUInteger, +}; +use objc2::encode::{Encode, Encoding}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, mutability, ClassType}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSEvent; + + unsafe impl ClassType for NSEvent { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + } +); + +// > Safely handled only on the same thread, whether that be the main thread +// > or a secondary thread; otherwise you run the risk of having events get +// > out of sequence. +// +// + +extern_methods!( + unsafe impl NSEvent { + #[method_id( + otherEventWithType: + location: + modifierFlags: + timestamp: + windowNumber: + context: + subtype: + data1: + data2: + )] + unsafe fn otherEventWithType( + type_: NSEventType, + location: NSPoint, + flags: NSEventModifierFlags, + time: NSTimeInterval, + window_num: NSInteger, + context: Option<&NSObject>, // NSGraphicsContext + subtype: NSEventSubtype, + data1: NSInteger, + data2: NSInteger, + ) -> Id; + + pub fn dummy() -> Id { + unsafe { + Self::otherEventWithType( + NSEventType::NSApplicationDefined, + NSPoint::new(0.0, 0.0), + NSEventModifierFlags::empty(), + 0.0, + 0, + None, + NSEventSubtype::NSWindowExposedEventType, + 0, + 0, + ) + } + } + + #[method_id( + keyEventWithType: + location: + modifierFlags: + timestamp: + windowNumber: + context: + characters: + charactersIgnoringModifiers: + isARepeat: + keyCode: + )] + pub fn keyEventWithType( + type_: NSEventType, + location: NSPoint, + modifier_flags: NSEventModifierFlags, + timestamp: NSTimeInterval, + window_num: NSInteger, + context: Option<&NSObject>, + characters: &NSString, + characters_ignoring_modifiers: &NSString, + is_a_repeat: bool, + scancode: c_ushort, + ) -> Id; + + #[method(locationInWindow)] + pub fn locationInWindow(&self) -> NSPoint; + + // TODO: MainThreadMarker + #[method(pressedMouseButtons)] + pub fn pressedMouseButtons() -> NSUInteger; + + #[method(modifierFlags)] + pub fn modifierFlags(&self) -> NSEventModifierFlags; + + #[method(type)] + pub fn type_(&self) -> NSEventType; + + #[method(keyCode)] + pub fn key_code(&self) -> c_ushort; + + #[method(magnification)] + pub fn magnification(&self) -> CGFloat; + + #[method(phase)] + pub fn phase(&self) -> NSEventPhase; + + #[method(momentumPhase)] + pub fn momentumPhase(&self) -> NSEventPhase; + + #[method(deltaX)] + pub fn deltaX(&self) -> CGFloat; + + #[method(deltaY)] + pub fn deltaY(&self) -> CGFloat; + + #[method(buttonNumber)] + pub fn buttonNumber(&self) -> NSInteger; + + #[method(scrollingDeltaX)] + pub fn scrollingDeltaX(&self) -> CGFloat; + + #[method(scrollingDeltaY)] + pub fn scrollingDeltaY(&self) -> CGFloat; + + #[method(hasPreciseScrollingDeltas)] + pub fn hasPreciseScrollingDeltas(&self) -> bool; + + #[method(rotation)] + pub fn rotation(&self) -> f32; + + #[method(pressure)] + pub fn pressure(&self) -> f32; + + #[method(stage)] + pub fn stage(&self) -> NSInteger; + + #[method(isARepeat)] + pub fn is_a_repeat(&self) -> bool; + + #[method(windowNumber)] + pub fn window_number(&self) -> NSInteger; + + #[method(timestamp)] + pub fn timestamp(&self) -> NSTimeInterval; + + #[method_id(characters)] + pub fn characters(&self) -> Option>; + + #[method_id(charactersIgnoringModifiers)] + pub fn charactersIgnoringModifiers(&self) -> Option>; + + pub fn lshift_pressed(&self) -> bool { + let raw_modifiers = self.modifierFlags().bits() as u32; + raw_modifiers & NX_DEVICELSHIFTKEYMASK != 0 + } + + pub fn rshift_pressed(&self) -> bool { + let raw_modifiers = self.modifierFlags().bits() as u32; + raw_modifiers & NX_DEVICERSHIFTKEYMASK != 0 + } + + pub fn lctrl_pressed(&self) -> bool { + let raw_modifiers = self.modifierFlags().bits() as u32; + raw_modifiers & NX_DEVICELCTLKEYMASK != 0 + } + + pub fn rctrl_pressed(&self) -> bool { + let raw_modifiers = self.modifierFlags().bits() as u32; + raw_modifiers & NX_DEVICERCTLKEYMASK != 0 + } + + pub fn lalt_pressed(&self) -> bool { + let raw_modifiers = self.modifierFlags().bits() as u32; + raw_modifiers & NX_DEVICELALTKEYMASK != 0 + } + + pub fn ralt_pressed(&self) -> bool { + let raw_modifiers = self.modifierFlags().bits() as u32; + raw_modifiers & NX_DEVICERALTKEYMASK != 0 + } + + pub fn lcmd_pressed(&self) -> bool { + let raw_modifiers = self.modifierFlags().bits() as u32; + raw_modifiers & NX_DEVICELCMDKEYMASK != 0 + } + + pub fn rcmd_pressed(&self) -> bool { + let raw_modifiers = self.modifierFlags().bits() as u32; + raw_modifiers & NX_DEVICERCMDKEYMASK != 0 + } + } +); + +unsafe impl NSCopying for NSEvent {} + +// The values are from the https://github.com/apple-oss-distributions/IOHIDFamily/blob/19666c840a6d896468416ff0007040a10b7b46b8/IOHIDSystem/IOKit/hidsystem/IOLLEvent.h#L258-L259 +const NX_DEVICELCTLKEYMASK: u32 = 0x00000001; +const NX_DEVICELSHIFTKEYMASK: u32 = 0x00000002; +const NX_DEVICERSHIFTKEYMASK: u32 = 0x00000004; +const NX_DEVICELCMDKEYMASK: u32 = 0x00000008; +const NX_DEVICERCMDKEYMASK: u32 = 0x00000010; +const NX_DEVICELALTKEYMASK: u32 = 0x00000020; +const NX_DEVICERALTKEYMASK: u32 = 0x00000040; +const NX_DEVICERCTLKEYMASK: u32 = 0x00002000; + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct NSEventModifierFlags: NSUInteger { + const NSAlphaShiftKeyMask = 1 << 16; + const NSShiftKeyMask = 1 << 17; + const NSControlKeyMask = 1 << 18; + const NSAlternateKeyMask = 1 << 19; + const NSCommandKeyMask = 1 << 20; + const NSNumericPadKeyMask = 1 << 21; + const NSHelpKeyMask = 1 << 22; + const NSFunctionKeyMask = 1 << 23; + const NSDeviceIndependentModifierFlagsMask = 0xffff0000; + } +} + +unsafe impl Encode for NSEventModifierFlags { + const ENCODING: Encoding = NSUInteger::ENCODING; +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct NSEventPhase: NSUInteger { + const NSEventPhaseNone = 0; + const NSEventPhaseBegan = 0x1 << 0; + const NSEventPhaseStationary = 0x1 << 1; + const NSEventPhaseChanged = 0x1 << 2; + const NSEventPhaseEnded = 0x1 << 3; + const NSEventPhaseCancelled = 0x1 << 4; + const NSEventPhaseMayBegin = 0x1 << 5; + } +} + +unsafe impl Encode for NSEventPhase { + const ENCODING: Encoding = NSUInteger::ENCODING; +} + +#[allow(dead_code)] +#[repr(i16)] // short +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum NSEventSubtype { + // TODO: Not sure what these values are + // NSMouseEventSubtype = NX_SUBTYPE_DEFAULT, + // NSTabletPointEventSubtype = NX_SUBTYPE_TABLET_POINT, + // NSTabletProximityEventSubtype = NX_SUBTYPE_TABLET_PROXIMITY + // NSTouchEventSubtype = NX_SUBTYPE_MOUSE_TOUCH, + NSWindowExposedEventType = 0, + NSApplicationActivatedEventType = 1, + NSApplicationDeactivatedEventType = 2, + NSWindowMovedEventType = 4, + NSScreenChangedEventType = 8, + NSAWTEventType = 16, +} + +unsafe impl Encode for NSEventSubtype { + const ENCODING: Encoding = i16::ENCODING; +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(usize)] // NSUInteger +pub enum NSEventType { + NSLeftMouseDown = 1, + NSLeftMouseUp = 2, + NSRightMouseDown = 3, + NSRightMouseUp = 4, + NSMouseMoved = 5, + NSLeftMouseDragged = 6, + NSRightMouseDragged = 7, + NSMouseEntered = 8, + NSMouseExited = 9, + NSKeyDown = 10, + NSKeyUp = 11, + NSFlagsChanged = 12, + NSAppKitDefined = 13, + NSSystemDefined = 14, + NSApplicationDefined = 15, + NSPeriodic = 16, + NSCursorUpdate = 17, + NSScrollWheel = 22, + NSTabletPoint = 23, + NSTabletProximity = 24, + NSOtherMouseDown = 25, + NSOtherMouseUp = 26, + NSOtherMouseDragged = 27, + NSEventTypeGesture = 29, + NSEventTypeMagnify = 30, + NSEventTypeSwipe = 31, + NSEventTypeRotate = 18, + NSEventTypeBeginGesture = 19, + NSEventTypeEndGesture = 20, + NSEventTypePressure = 34, +} + +unsafe impl Encode for NSEventType { + const ENCODING: Encoding = NSUInteger::ENCODING; +} diff --git a/rio-window/src/platform_impl/macos/appkit/image.rs b/rio-window/src/platform_impl/macos/appkit/image.rs new file mode 100644 index 00000000..0b5944c3 --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/image.rs @@ -0,0 +1,36 @@ +use icrate::Foundation::{NSData, NSObject, NSString}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType}; + +extern_class!( + // TODO: Can this be mutable? + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSImage; + + unsafe impl ClassType for NSImage { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + } +); + +// Documented Thread-Unsafe, but: +// > One thread can create an NSImage object, draw to the image buffer, +// > and pass it off to the main thread for drawing. The underlying image +// > cache is shared among all threads. +// +// +// So really only unsafe to mutate on several threads. +unsafe impl Send for NSImage {} +unsafe impl Sync for NSImage {} + +extern_methods!( + unsafe impl NSImage { + pub fn new_by_referencing_file(path: &NSString) -> Id { + unsafe { msg_send_id![Self::alloc(), initByReferencingFile: path] } + } + + pub fn new_with_data(data: &NSData) -> Id { + unsafe { msg_send_id![Self::alloc(), initWithData: data] } + } + } +); diff --git a/rio-window/src/platform_impl/macos/appkit/menu.rs b/rio-window/src/platform_impl/macos/appkit/menu.rs new file mode 100644 index 00000000..e03ec959 --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/menu.rs @@ -0,0 +1,25 @@ +use icrate::Foundation::NSObject; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, mutability, ClassType}; + +use super::NSMenuItem; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSMenu; + + unsafe impl ClassType for NSMenu { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + } +); + +extern_methods!( + unsafe impl NSMenu { + #[method_id(new)] + pub fn new() -> Id; + + #[method(addItem:)] + pub fn addItem(&self, item: &NSMenuItem); + } +); diff --git a/rio-window/src/platform_impl/macos/appkit/menu_item.rs b/rio-window/src/platform_impl/macos/appkit/menu_item.rs new file mode 100644 index 00000000..63c9a501 --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/menu_item.rs @@ -0,0 +1,43 @@ +use icrate::Foundation::{NSObject, NSString}; +use objc2::rc::Id; +use objc2::runtime::Sel; +use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType}; + +use super::{NSEventModifierFlags, NSMenu}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSMenuItem; + + unsafe impl ClassType for NSMenuItem { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + } +); + +extern_methods!( + unsafe impl NSMenuItem { + #[method_id(new)] + pub fn new() -> Id; + + pub fn newWithTitle(title: &NSString, action: Sel, key_equivalent: &NSString) -> Id { + unsafe { + msg_send_id![ + Self::alloc(), + initWithTitle: title, + action: action, + keyEquivalent: key_equivalent, + ] + } + } + + #[method_id(separatorItem)] + pub fn separatorItem() -> Id; + + #[method(setKeyEquivalentModifierMask:)] + pub fn setKeyEquivalentModifierMask(&self, mask: NSEventModifierFlags); + + #[method(setSubmenu:)] + pub fn setSubmenu(&self, submenu: &NSMenu); + } +); diff --git a/rio-window/src/platform_impl/macos/appkit/mod.rs b/rio-window/src/platform_impl/macos/appkit/mod.rs new file mode 100644 index 00000000..832fc149 --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/mod.rs @@ -0,0 +1,66 @@ +//! Safe bindings for the AppKit framework. +//! +//! These are split out from the rest of `winit` to make safety easier to review. +//! In the future, these should probably live in another crate like `cacao`. +//! +//! TODO: Main thread safety. +// Objective-C methods have different conventions, and it's much easier to +// understand if we just use the same names +#![allow(non_snake_case)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::enum_variant_names)] +#![allow(non_upper_case_globals)] + +mod appearance; +mod application; +mod button; +mod color; +mod control; +mod cursor; +mod event; +mod image; +mod menu; +mod menu_item; +mod pasteboard; +mod responder; +mod screen; +mod tab_group; +mod text_input_client; +mod text_input_context; +mod version; +mod view; +mod window; + +pub(crate) use self::appearance::NSAppearance; +pub(crate) use self::application::{ + NSApp, NSApplication, NSApplicationActivationPolicy, NSApplicationPresentationOptions, + NSRequestUserAttentionType, +}; +pub(crate) use self::button::NSButton; +pub(crate) use self::color::NSColor; +pub(crate) use self::control::NSControl; +pub(crate) use self::cursor::NSCursor; +#[allow(unused_imports)] +pub(crate) use self::event::{ + NSEvent, NSEventModifierFlags, NSEventPhase, NSEventSubtype, NSEventType, +}; +pub(crate) use self::image::NSImage; +pub(crate) use self::menu::NSMenu; +pub(crate) use self::menu_item::NSMenuItem; +pub(crate) use self::pasteboard::{NSFilenamesPboardType, NSPasteboard, NSPasteboardType}; +pub(crate) use self::responder::NSResponder; +#[allow(unused_imports)] +pub(crate) use self::screen::{NSDeviceDescriptionKey, NSScreen}; +pub(crate) use self::tab_group::NSWindowTabGroup; +pub(crate) use self::text_input_client::NSTextInputClient; +pub(crate) use self::text_input_context::NSTextInputContext; +pub(crate) use self::version::NSAppKitVersion; +pub(crate) use self::view::{NSTrackingRectTag, NSView}; +pub(crate) use self::window::{ + NSBackingStoreType, NSWindow, NSWindowButton, NSWindowLevel, NSWindowOcclusionState, + NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, + NSWindowTitleVisibility, +}; + +#[link(name = "AppKit", kind = "framework")] +extern "C" {} diff --git a/rio-window/src/platform_impl/macos/appkit/pasteboard.rs b/rio-window/src/platform_impl/macos/appkit/pasteboard.rs new file mode 100644 index 00000000..01a94089 --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/pasteboard.rs @@ -0,0 +1,26 @@ +use icrate::Foundation::{NSObject, NSString}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, mutability, ClassType}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSPasteboard; + + unsafe impl ClassType for NSPasteboard { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + } +); + +extern_methods!( + unsafe impl NSPasteboard { + #[method_id(propertyListForType:)] + pub fn propertyListForType(&self, type_: &NSPasteboardType) -> Id; + } +); + +pub type NSPasteboardType = NSString; + +extern "C" { + pub static NSFilenamesPboardType: &'static NSPasteboardType; +} diff --git a/rio-window/src/platform_impl/macos/appkit/responder.rs b/rio-window/src/platform_impl/macos/appkit/responder.rs new file mode 100644 index 00000000..11f3bddf --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/responder.rs @@ -0,0 +1,23 @@ +use icrate::Foundation::{NSArray, NSObject}; +use objc2::{extern_class, extern_methods, mutability, ClassType}; + +use super::NSEvent; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub struct NSResponder; + + unsafe impl ClassType for NSResponder { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + } +); + +// Documented as "Thread-Unsafe". + +extern_methods!( + unsafe impl NSResponder { + #[method(interpretKeyEvents:)] + pub(crate) unsafe fn interpretKeyEvents(&self, events: &NSArray); + } +); diff --git a/rio-window/src/platform_impl/macos/appkit/screen.rs b/rio-window/src/platform_impl/macos/appkit/screen.rs new file mode 100644 index 00000000..aa101239 --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/screen.rs @@ -0,0 +1,65 @@ +use icrate::ns_string; +use icrate::Foundation::{CGFloat, NSArray, NSDictionary, NSNumber, NSObject, NSRect, NSString}; +use objc2::rc::Id; +use objc2::runtime::AnyObject; +use objc2::{extern_class, extern_methods, mutability, ClassType}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSScreen; + + unsafe impl ClassType for NSScreen { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + } +); + +// TODO: Main thread marker! + +extern_methods!( + unsafe impl NSScreen { + /// The application object must have been created. + #[method_id(mainScreen)] + pub fn main() -> Option>; + + /// The application object must have been created. + #[method_id(screens)] + pub fn screens() -> Id>; + + #[method(frame)] + pub fn frame(&self) -> NSRect; + + #[method(visibleFrame)] + pub fn visibleFrame(&self) -> NSRect; + + #[method_id(deviceDescription)] + pub fn deviceDescription(&self) -> Id>; + + pub fn display_id(&self) -> u32 { + let key = ns_string!("NSScreenNumber"); + + objc2::rc::autoreleasepool(|_| { + let device_description = self.deviceDescription(); + + // Retrieve the CGDirectDisplayID associated with this screen + // + // SAFETY: The value from @"NSScreenNumber" in deviceDescription is guaranteed + // to be an NSNumber. See documentation for `deviceDescription` for details: + // + let obj = device_description + .get(key) + .expect("failed getting screen display id from device description"); + let obj: *const AnyObject = obj; + let obj: *const NSNumber = obj.cast(); + let obj: &NSNumber = unsafe { &*obj }; + + obj.as_u32() + }) + } + + #[method(backingScaleFactor)] + pub fn backingScaleFactor(&self) -> CGFloat; + } +); + +pub type NSDeviceDescriptionKey = NSString; diff --git a/rio-window/src/platform_impl/macos/appkit/tab_group.rs b/rio-window/src/platform_impl/macos/appkit/tab_group.rs new file mode 100644 index 00000000..e52bf572 --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/tab_group.rs @@ -0,0 +1,31 @@ +use icrate::Foundation::{NSArray, NSObject}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, mutability, ClassType}; + +use super::NSWindow; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSWindowTabGroup; + + unsafe impl ClassType for NSWindowTabGroup { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + } +); + +extern_methods!( + unsafe impl NSWindowTabGroup { + #[method(selectNextTab)] + pub fn selectNextTab(&self); + + #[method(selectPreviousTab)] + pub fn selectPreviousTab(&self); + + #[method_id(windows)] + pub fn tabbedWindows(&self) -> Option>>; + + #[method(setSelectedWindow:)] + pub fn setSelectedWindow(&self, window: &NSWindow); + } +); diff --git a/rio-window/src/platform_impl/macos/appkit/text_input_client.rs b/rio-window/src/platform_impl/macos/appkit/text_input_client.rs new file mode 100644 index 00000000..12d03fcb --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/text_input_client.rs @@ -0,0 +1,9 @@ +use objc2::{extern_protocol, ProtocolType}; + +extern_protocol!( + pub(crate) unsafe trait NSTextInputClient { + // TODO: Methods + } + + unsafe impl ProtocolType for dyn NSTextInputClient {} +); diff --git a/rio-window/src/platform_impl/macos/appkit/text_input_context.rs b/rio-window/src/platform_impl/macos/appkit/text_input_context.rs new file mode 100644 index 00000000..ee720798 --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/text_input_context.rs @@ -0,0 +1,29 @@ +use icrate::Foundation::{NSObject, NSString}; +use objc2::rc::Id; +use objc2::{extern_class, extern_methods, mutability, ClassType}; + +type NSTextInputSourceIdentifier = NSString; + +extern_class!( + /// Main-Thread-Only! + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSTextInputContext; + + unsafe impl ClassType for NSTextInputContext { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + } +); + +extern_methods!( + unsafe impl NSTextInputContext { + #[method(invalidateCharacterCoordinates)] + pub fn invalidateCharacterCoordinates(&self); + + #[method(discardMarkedText)] + pub fn discardMarkedText(&self); + + #[method_id(selectedKeyboardInputSource)] + pub fn selectedKeyboardInputSource(&self) -> Option>; + } +); diff --git a/rio-window/src/platform_impl/macos/appkit/version.rs b/rio-window/src/platform_impl/macos/appkit/version.rs new file mode 100644 index 00000000..c121e0dc --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/version.rs @@ -0,0 +1,62 @@ +#[repr(transparent)] +#[derive(PartialEq, PartialOrd, Debug, Clone, Copy)] +pub struct NSAppKitVersion(f64); + +#[allow(dead_code)] +#[allow(non_upper_case_globals)] +impl NSAppKitVersion { + pub fn current() -> Self { + extern "C" { + static NSAppKitVersionNumber: NSAppKitVersion; + } + + unsafe { NSAppKitVersionNumber } + } + + pub fn floor(self) -> Self { + Self(self.0.floor()) + } + + pub const NSAppKitVersionNumber10_0: Self = Self(577.0); + pub const NSAppKitVersionNumber10_1: Self = Self(620.0); + pub const NSAppKitVersionNumber10_2: Self = Self(663.0); + pub const NSAppKitVersionNumber10_2_3: Self = Self(663.6); + pub const NSAppKitVersionNumber10_3: Self = Self(743.0); + pub const NSAppKitVersionNumber10_3_2: Self = Self(743.14); + pub const NSAppKitVersionNumber10_3_3: Self = Self(743.2); + pub const NSAppKitVersionNumber10_3_5: Self = Self(743.24); + pub const NSAppKitVersionNumber10_3_7: Self = Self(743.33); + pub const NSAppKitVersionNumber10_3_9: Self = Self(743.36); + pub const NSAppKitVersionNumber10_4: Self = Self(824.0); + pub const NSAppKitVersionNumber10_4_1: Self = Self(824.1); + pub const NSAppKitVersionNumber10_4_3: Self = Self(824.23); + pub const NSAppKitVersionNumber10_4_4: Self = Self(824.33); + pub const NSAppKitVersionNumber10_4_7: Self = Self(824.41); + pub const NSAppKitVersionNumber10_5: Self = Self(949.0); + pub const NSAppKitVersionNumber10_5_2: Self = Self(949.27); + pub const NSAppKitVersionNumber10_5_3: Self = Self(949.33); + pub const NSAppKitVersionNumber10_6: Self = Self(1038.0); + pub const NSAppKitVersionNumber10_7: Self = Self(1138.0); + pub const NSAppKitVersionNumber10_7_2: Self = Self(1138.23); + pub const NSAppKitVersionNumber10_7_3: Self = Self(1138.32); + pub const NSAppKitVersionNumber10_7_4: Self = Self(1138.47); + pub const NSAppKitVersionNumber10_8: Self = Self(1187.0); + pub const NSAppKitVersionNumber10_9: Self = Self(1265.0); + pub const NSAppKitVersionNumber10_10: Self = Self(1343.0); + pub const NSAppKitVersionNumber10_10_2: Self = Self(1344.0); + pub const NSAppKitVersionNumber10_10_3: Self = Self(1347.0); + pub const NSAppKitVersionNumber10_10_4: Self = Self(1348.0); + pub const NSAppKitVersionNumber10_10_5: Self = Self(1348.0); + pub const NSAppKitVersionNumber10_10_Max: Self = Self(1349.0); + pub const NSAppKitVersionNumber10_11: Self = Self(1404.0); + pub const NSAppKitVersionNumber10_11_1: Self = Self(1404.13); + pub const NSAppKitVersionNumber10_11_2: Self = Self(1404.34); + pub const NSAppKitVersionNumber10_11_3: Self = Self(1404.34); + pub const NSAppKitVersionNumber10_12: Self = Self(1504.0); + pub const NSAppKitVersionNumber10_12_1: Self = Self(1504.60); + pub const NSAppKitVersionNumber10_12_2: Self = Self(1504.76); + pub const NSAppKitVersionNumber10_13: Self = Self(1561.0); + pub const NSAppKitVersionNumber10_13_1: Self = Self(1561.1); + pub const NSAppKitVersionNumber10_13_2: Self = Self(1561.2); + pub const NSAppKitVersionNumber10_13_4: Self = Self(1561.4); +} diff --git a/rio-window/src/platform_impl/macos/appkit/view.rs b/rio-window/src/platform_impl/macos/appkit/view.rs new file mode 100644 index 00000000..d6720ec5 --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/view.rs @@ -0,0 +1,95 @@ +use std::ffi::c_void; +use std::num::NonZeroIsize; +use std::ptr; + +use icrate::Foundation::{NSObject, NSPoint, NSRect}; +use objc2::rc::Id; +use objc2::runtime::AnyObject; +use objc2::{extern_class, extern_methods, mutability, ClassType}; + +use super::{NSCursor, NSResponder, NSTextInputContext, NSWindow}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSView; + + unsafe impl ClassType for NSView { + #[inherits(NSObject)] + type Super = NSResponder; + type Mutability = mutability::InteriorMutable; + } +); + +// Documented as "Main Thread Only". +// > generally thread safe, although operations on views such as creating, +// > resizing, and moving should happen on the main thread. +// +// +// > If you want to use a thread to draw to a view, bracket all drawing code +// > between the lockFocusIfCanDraw and unlockFocus methods of NSView. +// + +extern_methods!( + /// Getter methods + unsafe impl NSView { + #[method(frame)] + pub fn frame(&self) -> NSRect; + + #[method(bounds)] + pub fn bounds(&self) -> NSRect; + + #[method_id(inputContext)] + pub fn inputContext( + &self, + // _mtm: MainThreadMarker, + ) -> Option>; + + #[method(hasMarkedText)] + pub fn hasMarkedText(&self) -> bool; + + #[method(convertPoint:fromView:)] + pub fn convertPoint_fromView(&self, point: NSPoint, view: Option<&NSView>) -> NSPoint; + + #[method_id(window)] + pub fn window(&self) -> Option>; + } + + unsafe impl NSView { + #[method(setWantsBestResolutionOpenGLSurface:)] + pub fn setWantsBestResolutionOpenGLSurface(&self, value: bool); + + #[method(setWantsLayer:)] + pub fn setWantsLayer(&self, wants_layer: bool); + + #[method(setPostsFrameChangedNotifications:)] + pub fn setPostsFrameChangedNotifications(&self, value: bool); + + #[method(removeTrackingRect:)] + pub fn removeTrackingRect(&self, tag: NSTrackingRectTag); + + #[method(addTrackingRect:owner:userData:assumeInside:)] + unsafe fn inner_addTrackingRect( + &self, + rect: NSRect, + owner: &AnyObject, + user_data: *mut c_void, + assume_inside: bool, + ) -> Option; + + pub fn add_tracking_rect(&self, rect: NSRect, assume_inside: bool) -> NSTrackingRectTag { + // SAFETY: The user data is NULL, so it is valid + unsafe { self.inner_addTrackingRect(rect, self, ptr::null_mut(), assume_inside) } + .expect("failed creating tracking rect") + } + + #[method(addCursorRect:cursor:)] + // NSCursor safe to take by shared reference since it is already immutable + pub fn addCursorRect(&self, rect: NSRect, cursor: &NSCursor); + + #[method(setHidden:)] + pub fn setHidden(&self, hidden: bool); + } +); + +/// +pub type NSTrackingRectTag = NonZeroIsize; // NSInteger, but non-zero! diff --git a/rio-window/src/platform_impl/macos/appkit/window.rs b/rio-window/src/platform_impl/macos/appkit/window.rs new file mode 100644 index 00000000..5f5d8f17 --- /dev/null +++ b/rio-window/src/platform_impl/macos/appkit/window.rs @@ -0,0 +1,440 @@ +use icrate::Foundation::{ + CGFloat, NSArray, NSInteger, NSObject, NSPoint, NSRect, NSSize, NSString, NSUInteger, +}; +use objc2::encode::{Encode, Encoding}; +use objc2::rc::Id; +use objc2::runtime::AnyObject; +use objc2::{extern_class, extern_methods, mutability, ClassType}; + +use super::{ + NSButton, NSColor, NSEvent, NSPasteboardType, NSResponder, NSScreen, NSView, NSWindowTabGroup, +}; + +extern_class!( + /// Main-Thread-Only! + #[derive(Debug, PartialEq, Eq, Hash)] + pub struct NSWindow; + + unsafe impl ClassType for NSWindow { + #[inherits(NSObject)] + type Super = NSResponder; + type Mutability = mutability::InteriorMutable; + } +); + +// Documented as "Main Thread Only", but: +// > Thread safe in that you can create and manage them on a secondary thread. +// +// +// +// So could in theory be `Send`, and perhaps also `Sync` - but we would like +// interior mutability on windows, since that's just much easier, and in that +// case, they can't be! + +extern_methods!( + unsafe impl NSWindow { + #[method(frame)] + pub(crate) fn frame(&self) -> NSRect; + + #[method(windowNumber)] + pub(crate) fn windowNumber(&self) -> NSInteger; + + #[method(backingScaleFactor)] + pub(crate) fn backingScaleFactor(&self) -> CGFloat; + + #[method_id(contentView)] + pub(crate) fn contentView(&self) -> Id; + + #[method(setContentView:)] + pub(crate) fn setContentView(&self, view: &NSView); + + #[method(setInitialFirstResponder:)] + pub(crate) fn setInitialFirstResponder(&self, view: &NSView); + + #[method(makeFirstResponder:)] + #[must_use] + pub(crate) fn makeFirstResponder(&self, responder: Option<&NSResponder>) -> bool; + + #[method(contentRectForFrameRect:)] + pub(crate) fn contentRectForFrameRect(&self, windowFrame: NSRect) -> NSRect; + + #[method_id(screen)] + pub(crate) fn screen(&self) -> Option>; + + #[method(setContentSize:)] + pub(crate) fn setContentSize(&self, contentSize: NSSize); + + #[method(setFrameTopLeftPoint:)] + pub(crate) fn setFrameTopLeftPoint(&self, point: NSPoint); + + #[method(setMinSize:)] + pub(crate) fn setMinSize(&self, minSize: NSSize); + + #[method(setMaxSize:)] + pub(crate) fn setMaxSize(&self, maxSize: NSSize); + + #[method(setResizeIncrements:)] + pub(crate) fn setResizeIncrements(&self, increments: NSSize); + + #[method(contentResizeIncrements)] + pub(crate) fn contentResizeIncrements(&self) -> NSSize; + + #[method(setContentResizeIncrements:)] + pub(crate) fn setContentResizeIncrements(&self, increments: NSSize); + + #[method(setFrame:display:)] + pub(crate) fn setFrame_display(&self, frameRect: NSRect, flag: bool); + + #[method(setMovable:)] + pub(crate) fn setMovable(&self, movable: bool); + + #[method(setSharingType:)] + pub(crate) fn setSharingType(&self, sharingType: NSWindowSharingType); + + #[method(setTabbingMode:)] + pub(crate) fn setTabbingMode(&self, tabbingMode: NSWindowTabbingMode); + + #[method(setOpaque:)] + pub(crate) fn setOpaque(&self, opaque: bool); + + #[method(hasShadow)] + pub(crate) fn hasShadow(&self) -> bool; + + #[method(setHasShadow:)] + pub(crate) fn setHasShadow(&self, has_shadow: bool); + + #[method(setIgnoresMouseEvents:)] + pub(crate) fn setIgnoresMouseEvents(&self, ignores: bool); + + #[method(setBackgroundColor:)] + pub(crate) fn setBackgroundColor(&self, color: &NSColor); + + #[method(styleMask)] + pub(crate) fn styleMask(&self) -> NSWindowStyleMask; + + #[method(setStyleMask:)] + pub(crate) fn setStyleMask(&self, mask: NSWindowStyleMask); + + #[method(registerForDraggedTypes:)] + pub(crate) fn registerForDraggedTypes(&self, types: &NSArray); + + #[method(makeKeyAndOrderFront:)] + pub(crate) fn makeKeyAndOrderFront(&self, sender: Option<&AnyObject>); + + #[method(orderFront:)] + pub(crate) fn orderFront(&self, sender: Option<&AnyObject>); + + #[method(miniaturize:)] + pub(crate) fn miniaturize(&self, sender: Option<&AnyObject>); + + #[method(deminiaturize:)] + pub(crate) fn deminiaturize(&self, sender: Option<&AnyObject>); + + #[method(toggleFullScreen:)] + pub(crate) fn toggleFullScreen(&self, sender: Option<&AnyObject>); + + #[method(orderOut:)] + pub(crate) fn orderOut(&self, sender: Option<&AnyObject>); + + #[method(zoom:)] + pub(crate) fn zoom(&self, sender: Option<&AnyObject>); + + #[method(selectNextKeyView:)] + pub(crate) fn selectNextKeyView(&self, sender: Option<&AnyObject>); + + #[method(selectPreviousKeyView:)] + pub(crate) fn selectPreviousKeyView(&self, sender: Option<&AnyObject>); + + #[method_id(firstResponder)] + pub(crate) fn firstResponder(&self) -> Option>; + + #[method_id(standardWindowButton:)] + pub(crate) fn standardWindowButton(&self, kind: NSWindowButton) -> Option>; + + #[method(setTitle:)] + pub(crate) fn setTitle(&self, title: &NSString); + + #[method_id(title)] + pub(crate) fn title_(&self) -> Id; + + #[method(setReleasedWhenClosed:)] + pub(crate) fn setReleasedWhenClosed(&self, val: bool); + + #[method(setAcceptsMouseMovedEvents:)] + pub(crate) fn setAcceptsMouseMovedEvents(&self, val: bool); + + #[method(setTitlebarAppearsTransparent:)] + pub(crate) fn setTitlebarAppearsTransparent(&self, val: bool); + + #[method(setTitleVisibility:)] + pub(crate) fn setTitleVisibility(&self, visibility: NSWindowTitleVisibility); + + #[method(setMovableByWindowBackground:)] + pub(crate) fn setMovableByWindowBackground(&self, val: bool); + + #[method(setLevel:)] + pub(crate) fn setLevel(&self, level: NSWindowLevel); + + #[method(setAllowsAutomaticWindowTabbing:)] + pub(crate) fn setAllowsAutomaticWindowTabbing(val: bool); + + #[method(setTabbingIdentifier:)] + pub(crate) fn setTabbingIdentifier(&self, identifier: &NSString); + + #[method(setDocumentEdited:)] + pub(crate) fn setDocumentEdited(&self, val: bool); + + #[method(occlusionState)] + pub(crate) fn occlusionState(&self) -> NSWindowOcclusionState; + + #[method(center)] + pub(crate) fn center(&self); + + #[method(isResizable)] + pub(crate) fn isResizable(&self) -> bool; + + #[method(isMiniaturizable)] + pub(crate) fn isMiniaturizable(&self) -> bool; + + #[method(hasCloseBox)] + pub(crate) fn hasCloseBox(&self) -> bool; + + #[method(isMiniaturized)] + pub(crate) fn isMiniaturized(&self) -> bool; + + #[method(isVisible)] + pub(crate) fn isVisible(&self) -> bool; + + #[method(isKeyWindow)] + pub(crate) fn isKeyWindow(&self) -> bool; + + #[method(isZoomed)] + pub(crate) fn isZoomed(&self) -> bool; + + #[method(allowsAutomaticWindowTabbing)] + pub(crate) fn allowsAutomaticWindowTabbing() -> bool; + + #[method(selectNextTab)] + pub(crate) fn selectNextTab(&self); + + #[method_id(tabbingIdentifier)] + pub(crate) fn tabbingIdentifier(&self) -> Id; + + #[method_id(tabGroup)] + pub(crate) fn tabGroup(&self) -> Option>; + + #[method(isDocumentEdited)] + pub(crate) fn isDocumentEdited(&self) -> bool; + + #[method(close)] + pub(crate) fn close(&self); + + #[method(performWindowDragWithEvent:)] + // TODO: Can this actually accept NULL? + pub(crate) fn performWindowDragWithEvent(&self, event: Option<&NSEvent>); + + #[method(invalidateCursorRectsForView:)] + pub(crate) fn invalidateCursorRectsForView(&self, view: &NSView); + + #[method(setDelegate:)] + pub(crate) fn setDelegate(&self, delegate: Option<&NSObject>); + + #[method(sendEvent:)] + pub(crate) unsafe fn sendEvent(&self, event: &NSEvent); + + #[method(addChildWindow:ordered:)] + pub(crate) unsafe fn addChildWindow(&self, child: &NSWindow, ordered: NSWindowOrderingMode); + } +); + +#[allow(dead_code)] +#[repr(isize)] // NSInteger +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum NSWindowTitleVisibility { + #[doc(alias = "NSWindowTitleVisible")] + Visible = 0, + #[doc(alias = "NSWindowTitleHidden")] + Hidden = 1, +} + +unsafe impl Encode for NSWindowTitleVisibility { + const ENCODING: Encoding = NSInteger::ENCODING; +} + +#[allow(dead_code)] +#[repr(usize)] // NSUInteger +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum NSWindowButton { + #[doc(alias = "NSWindowCloseButton")] + Close = 0, + #[doc(alias = "NSWindowMiniaturizeButton")] + Miniaturize = 1, + #[doc(alias = "NSWindowZoomButton")] + Zoom = 2, + #[doc(alias = "NSWindowToolbarButton")] + Toolbar = 3, + #[doc(alias = "NSWindowDocumentIconButton")] + DocumentIcon = 4, + #[doc(alias = "NSWindowDocumentVersionsButton")] + DocumentVersions = 6, + #[doc(alias = "NSWindowFullScreenButton")] + #[deprecated = "Deprecated since macOS 10.12"] + FullScreen = 7, +} + +unsafe impl Encode for NSWindowButton { + const ENCODING: Encoding = NSUInteger::ENCODING; +} + +// CGWindowLevel.h +// +// Note: There are two different things at play in this header: +// `CGWindowLevel` and `CGWindowLevelKey`. +// +// It seems like there was a push towards using "key" values instead of the +// raw window level values, and then you were supposed to use +// `CGWindowLevelForKey` to get the actual level. +// +// But the values that `NSWindowLevel` has are compiled in, and as such has +// to remain ABI compatible, so they're safe for us to hardcode as well. +#[allow(dead_code)] +mod window_level { + const kCGNumReservedWindowLevels: i32 = 16; + const kCGNumReservedBaseWindowLevels: i32 = 5; + + pub const kCGBaseWindowLevel: i32 = i32::MIN; + pub const kCGMinimumWindowLevel: i32 = kCGBaseWindowLevel + kCGNumReservedBaseWindowLevels; + pub const kCGMaximumWindowLevel: i32 = i32::MAX - kCGNumReservedWindowLevels; + + pub const kCGDesktopWindowLevel: i32 = kCGMinimumWindowLevel + 20; + pub const kCGDesktopIconWindowLevel: i32 = kCGDesktopWindowLevel + 20; + pub const kCGBackstopMenuLevel: i32 = -20; + pub const kCGNormalWindowLevel: i32 = 0; + pub const kCGFloatingWindowLevel: i32 = 3; + pub const kCGTornOffMenuWindowLevel: i32 = 3; + pub const kCGModalPanelWindowLevel: i32 = 8; + pub const kCGUtilityWindowLevel: i32 = 19; + pub const kCGDockWindowLevel: i32 = 20; + pub const kCGMainMenuWindowLevel: i32 = 24; + pub const kCGStatusWindowLevel: i32 = 25; + pub const kCGPopUpMenuWindowLevel: i32 = 101; + pub const kCGOverlayWindowLevel: i32 = 102; + pub const kCGHelpWindowLevel: i32 = 200; + pub const kCGDraggingWindowLevel: i32 = 500; + pub const kCGScreenSaverWindowLevel: i32 = 1000; + pub const kCGAssistiveTechHighWindowLevel: i32 = 1500; + pub const kCGCursorWindowLevel: i32 = kCGMaximumWindowLevel - 1; +} +use window_level::*; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct NSWindowLevel(pub NSInteger); + +#[allow(dead_code)] +impl NSWindowLevel { + #[doc(alias = "BelowNormalWindowLevel")] + pub const BELOW_NORMAL: Self = Self((kCGNormalWindowLevel - 1) as _); + #[doc(alias = "NSNormalWindowLevel")] + pub const Normal: Self = Self(kCGNormalWindowLevel as _); + #[doc(alias = "NSFloatingWindowLevel")] + pub const Floating: Self = Self(kCGFloatingWindowLevel as _); + #[doc(alias = "NSTornOffMenuWindowLevel")] + pub const TornOffMenu: Self = Self(kCGTornOffMenuWindowLevel as _); + #[doc(alias = "NSModalPanelWindowLevel")] + pub const ModalPanel: Self = Self(kCGModalPanelWindowLevel as _); + #[doc(alias = "NSMainMenuWindowLevel")] + pub const MainMenu: Self = Self(kCGMainMenuWindowLevel as _); + #[doc(alias = "NSStatusWindowLevel")] + pub const Status: Self = Self(kCGStatusWindowLevel as _); + #[doc(alias = "NSPopUpMenuWindowLevel")] + pub const PopUpMenu: Self = Self(kCGPopUpMenuWindowLevel as _); + #[doc(alias = "NSScreenSaverWindowLevel")] + pub const ScreenSaver: Self = Self(kCGScreenSaverWindowLevel as _); +} + +unsafe impl Encode for NSWindowLevel { + const ENCODING: Encoding = NSInteger::ENCODING; +} + +bitflags! { + #[derive(Clone, Copy)] + pub struct NSWindowOcclusionState: NSUInteger { + const NSWindowOcclusionStateVisible = 1 << 1; + } +} + +unsafe impl Encode for NSWindowOcclusionState { + const ENCODING: Encoding = NSUInteger::ENCODING; +} + +bitflags! { + #[derive(Debug, Clone, Copy)] + pub struct NSWindowStyleMask: NSUInteger { + const NSBorderlessWindowMask = 0; + const NSTitledWindowMask = 1 << 0; + const NSClosableWindowMask = 1 << 1; + const NSMiniaturizableWindowMask = 1 << 2; + const NSResizableWindowMask = 1 << 3; + const NSTexturedBackgroundWindowMask = 1 << 8; + const NSUnifiedTitleAndToolbarWindowMask = 1 << 12; + const NSFullScreenWindowMask = 1 << 14; + const NSFullSizeContentViewWindowMask = 1 << 15; + } +} + +unsafe impl Encode for NSWindowStyleMask { + const ENCODING: Encoding = NSUInteger::ENCODING; +} + +#[allow(dead_code)] +#[repr(usize)] // NSUInteger +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum NSBackingStoreType { + NSBackingStoreRetained = 0, + NSBackingStoreNonretained = 1, + NSBackingStoreBuffered = 2, +} + +unsafe impl Encode for NSBackingStoreType { + const ENCODING: Encoding = NSUInteger::ENCODING; +} + +#[allow(dead_code)] +#[repr(usize)] // NSUInteger +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum NSWindowSharingType { + NSWindowSharingNone = 0, + NSWindowSharingReadOnly = 1, + NSWindowSharingReadWrite = 2, +} + +unsafe impl Encode for NSWindowSharingType { + const ENCODING: Encoding = NSUInteger::ENCODING; +} + +#[allow(dead_code)] +#[repr(isize)] // NSInteger +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum NSWindowOrderingMode { + NSWindowAbove = 1, + NSWindowBelow = -1, + NSWindowOut = 0, +} + +unsafe impl Encode for NSWindowOrderingMode { + const ENCODING: Encoding = NSInteger::ENCODING; +} + +#[allow(dead_code)] +#[repr(isize)] // NSInteger +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum NSWindowTabbingMode { + NSWindowTabbingModeAutomatic = 0, + NSWindowTabbingModeDisallowed = 2, + NSWindowTabbingModePreferred = 1, +} + +unsafe impl Encode for NSWindowTabbingMode { + const ENCODING: Encoding = NSInteger::ENCODING; +} diff --git a/rio-window/src/platform_impl/macos/cursor.rs b/rio-window/src/platform_impl/macos/cursor.rs new file mode 100644 index 00000000..95cec639 --- /dev/null +++ b/rio-window/src/platform_impl/macos/cursor.rs @@ -0,0 +1,233 @@ +use std::ffi::c_uchar; +use std::slice; +use std::sync::OnceLock; + +use objc2::rc::Retained; +use objc2::runtime::Sel; +use objc2::{msg_send_id, sel, ClassType}; +use objc2_app_kit::{NSBitmapImageRep, NSCursor, NSDeviceRGBColorSpace, NSImage}; +use objc2_foundation::{ + ns_string, NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, + NSSize, NSString, +}; + +use crate::cursor::{CursorImage, OnlyCursorImageSource}; +use crate::window::CursorIcon; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct CustomCursor(pub(crate) Retained); + +// SAFETY: NSCursor is immutable and thread-safe +// TODO(madsmtm): Put this logic in objc2-app-kit itself +unsafe impl Send for CustomCursor {} +unsafe impl Sync for CustomCursor {} + +impl CustomCursor { + pub(crate) fn new(cursor: OnlyCursorImageSource) -> CustomCursor { + Self(cursor_from_image(&cursor.0)) + } +} + +pub(crate) fn cursor_from_image(cursor: &CursorImage) -> Retained { + let width = cursor.width; + let height = cursor.height; + + let bitmap = unsafe { + NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel( + NSBitmapImageRep::alloc(), + std::ptr::null_mut::<*mut c_uchar>(), + width as isize, + height as isize, + 8, + 4, + true, + false, + NSDeviceRGBColorSpace, + width as isize * 4, + 32, + ).unwrap() + }; + let bitmap_data = + unsafe { slice::from_raw_parts_mut(bitmap.bitmapData(), cursor.rgba.len()) }; + bitmap_data.copy_from_slice(&cursor.rgba); + + let image = unsafe { + NSImage::initWithSize(NSImage::alloc(), NSSize::new(width.into(), height.into())) + }; + unsafe { image.addRepresentation(&bitmap) }; + + let hotspot = NSPoint::new(cursor.hotspot_x as f64, cursor.hotspot_y as f64); + + NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot) +} + +pub(crate) fn default_cursor() -> Retained { + NSCursor::arrowCursor() +} + +unsafe fn try_cursor_from_selector(sel: Sel) -> Option> { + let cls = NSCursor::class(); + if cls.responds_to(sel) { + let cursor: Retained = + unsafe { msg_send_id![cls, performSelector: sel] }; + Some(cursor) + } else { + tracing::warn!("cursor `{sel}` appears to be invalid"); + None + } +} + +macro_rules! def_undocumented_cursor { + {$( + $(#[$($m:meta)*])* + fn $name:ident(); + )*} => {$( + $(#[$($m)*])* + #[allow(non_snake_case)] + fn $name() -> Retained { + unsafe { try_cursor_from_selector(sel!($name)).unwrap_or_else(|| default_cursor()) } + } + )*}; +} + +def_undocumented_cursor!( + // Undocumented cursors: https://stackoverflow.com/a/46635398/5435443 + fn _helpCursor(); + fn _zoomInCursor(); + fn _zoomOutCursor(); + fn _windowResizeNorthEastCursor(); + fn _windowResizeNorthWestCursor(); + fn _windowResizeSouthEastCursor(); + fn _windowResizeSouthWestCursor(); + fn _windowResizeNorthEastSouthWestCursor(); + fn _windowResizeNorthWestSouthEastCursor(); + + // While these two are available, the former just loads a white arrow, + // and the latter loads an ugly deflated beachball! + // pub fn _moveCursor(); + // pub fn _waitCursor(); + + // An even more undocumented cursor... + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=522349 + fn busyButClickableCursor(); +); + +// Note that loading `busybutclickable` with this code won't animate +// the frames; instead you'll just get them all in a column. +unsafe fn load_webkit_cursor(name: &NSString) -> Retained { + // Snatch a cursor from WebKit; They fit the style of the native + // cursors, and will seem completely standard to macOS users. + // + // https://stackoverflow.com/a/21786835/5435443 + let root = ns_string!( + "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/\ + HIServices.framework/Versions/A/Resources/cursors" + ); + let cursor_path = root.stringByAppendingPathComponent(name); + + let pdf_path = cursor_path.stringByAppendingPathComponent(ns_string!("cursor.pdf")); + let image = NSImage::initByReferencingFile(NSImage::alloc(), &pdf_path).unwrap(); + + // TODO: Handle PLists better + let info_path = cursor_path.stringByAppendingPathComponent(ns_string!("info.plist")); + let info: Retained> = unsafe { + msg_send_id![ + >::class(), + dictionaryWithContentsOfFile: &*info_path, + ] + }; + let mut x = 0.0; + if let Some(n) = info.get(&*ns_string!("hotx")) { + if n.is_kind_of::() { + let ptr: *const NSObject = n; + let ptr: *const NSNumber = ptr.cast(); + x = unsafe { &*ptr }.as_cgfloat() + } + } + let mut y = 0.0; + if let Some(n) = info.get(&*ns_string!("hotx")) { + if n.is_kind_of::() { + let ptr: *const NSObject = n; + let ptr: *const NSNumber = ptr.cast(); + y = unsafe { &*ptr }.as_cgfloat() + } + } + + let hotspot = NSPoint::new(x, y); + NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot) +} + +fn webkit_move() -> Retained { + unsafe { load_webkit_cursor(ns_string!("move")) } +} + +fn webkit_cell() -> Retained { + unsafe { load_webkit_cursor(ns_string!("cell")) } +} + +pub(crate) fn invisible_cursor() -> Retained { + // 16x16 GIF data for invisible cursor + // You can reproduce this via ImageMagick. + // $ convert -size 16x16 xc:none cursor.gif + static CURSOR_BYTES: &[u8] = &[ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x10, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x02, 0x0e, + 0x84, 0x8f, 0xa9, 0xcb, 0xed, 0x0f, 0xa3, 0x9c, 0xb4, 0xda, 0x8b, 0xb3, 0x3e, + 0x05, 0x00, 0x3b, + ]; + + fn new_invisible() -> Retained { + // TODO: Consider using `dataWithBytesNoCopy:` + let data = NSData::with_bytes(CURSOR_BYTES); + let image = NSImage::initWithData(NSImage::alloc(), &data).unwrap(); + let hotspot = NSPoint::new(0.0, 0.0); + NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot) + } + + // Cache this for efficiency + static CURSOR: OnceLock = OnceLock::new(); + CURSOR + .get_or_init(|| CustomCursor(new_invisible())) + .0 + .clone() +} + +pub(crate) fn cursor_from_icon(icon: CursorIcon) -> Retained { + match icon { + CursorIcon::Default => default_cursor(), + CursorIcon::Pointer => NSCursor::pointingHandCursor(), + CursorIcon::Grab => NSCursor::openHandCursor(), + CursorIcon::Grabbing => NSCursor::closedHandCursor(), + CursorIcon::Text => NSCursor::IBeamCursor(), + CursorIcon::VerticalText => NSCursor::IBeamCursorForVerticalLayout(), + CursorIcon::Copy => NSCursor::dragCopyCursor(), + CursorIcon::Alias => NSCursor::dragLinkCursor(), + CursorIcon::NotAllowed | CursorIcon::NoDrop => { + NSCursor::operationNotAllowedCursor() + } + CursorIcon::ContextMenu => NSCursor::contextualMenuCursor(), + CursorIcon::Crosshair => NSCursor::crosshairCursor(), + CursorIcon::EResize => NSCursor::resizeRightCursor(), + CursorIcon::NResize => NSCursor::resizeUpCursor(), + CursorIcon::WResize => NSCursor::resizeLeftCursor(), + CursorIcon::SResize => NSCursor::resizeDownCursor(), + CursorIcon::EwResize | CursorIcon::ColResize => NSCursor::resizeLeftRightCursor(), + CursorIcon::NsResize | CursorIcon::RowResize => NSCursor::resizeUpDownCursor(), + CursorIcon::Help => _helpCursor(), + CursorIcon::ZoomIn => _zoomInCursor(), + CursorIcon::ZoomOut => _zoomOutCursor(), + CursorIcon::NeResize => _windowResizeNorthEastCursor(), + CursorIcon::NwResize => _windowResizeNorthWestCursor(), + CursorIcon::SeResize => _windowResizeSouthEastCursor(), + CursorIcon::SwResize => _windowResizeSouthWestCursor(), + CursorIcon::NeswResize => _windowResizeNorthEastSouthWestCursor(), + CursorIcon::NwseResize => _windowResizeNorthWestSouthEastCursor(), + // This is the wrong semantics for `Wait`, but it's the same as + // what's used in Safari and Chrome. + CursorIcon::Wait | CursorIcon::Progress => busyButClickableCursor(), + CursorIcon::Move | CursorIcon::AllScroll => webkit_move(), + CursorIcon::Cell => webkit_cell(), + _ => default_cursor(), + } +} diff --git a/rio-window/src/platform_impl/macos/event.rs b/rio-window/src/platform_impl/macos/event.rs new file mode 100644 index 00000000..ebe2ae8f --- /dev/null +++ b/rio-window/src/platform_impl/macos/event.rs @@ -0,0 +1,666 @@ +use std::ffi::c_void; + +use core_foundation::base::CFRelease; +use core_foundation::data::{CFDataGetBytePtr, CFDataRef}; +use objc2::rc::Retained; +use objc2_app_kit::{NSEvent, NSEventModifierFlags, NSEventSubtype, NSEventType}; +use objc2_foundation::{run_on_main, NSPoint}; +use smol_str::SmolStr; + +use crate::event::{ElementState, KeyEvent, Modifiers}; +use crate::keyboard::{ + Key, KeyCode, KeyLocation, ModifiersKeys, ModifiersState, NamedKey, NativeKey, + NativeKeyCode, PhysicalKey, +}; +use crate::platform_impl::platform::ffi; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifiers: Option, + pub key_without_modifiers: Key, +} + +/// Ignores ALL modifiers. +pub fn get_modifierless_char(scancode: u16) -> Key { + let mut string = [0; 16]; + let input_source; + let layout; + unsafe { + input_source = ffi::TISCopyCurrentKeyboardLayoutInputSource(); + if input_source.is_null() { + tracing::error!( + "`TISCopyCurrentKeyboardLayoutInputSource` returned null ptr" + ); + return Key::Unidentified(NativeKey::MacOS(scancode)); + } + let layout_data = ffi::TISGetInputSourceProperty( + input_source, + ffi::kTISPropertyUnicodeKeyLayoutData, + ); + if layout_data.is_null() { + CFRelease(input_source as *mut c_void); + tracing::error!("`TISGetInputSourceProperty` returned null ptr"); + return Key::Unidentified(NativeKey::MacOS(scancode)); + } + layout = + CFDataGetBytePtr(layout_data as CFDataRef) as *const ffi::UCKeyboardLayout; + } + let keyboard_type = run_on_main(|_mtm| unsafe { ffi::LMGetKbdType() }); + + let mut result_len = 0; + let mut dead_keys = 0; + let modifiers = 0; + let translate_result = unsafe { + ffi::UCKeyTranslate( + layout, + scancode, + ffi::kUCKeyActionDisplay, + modifiers, + keyboard_type as u32, + ffi::kUCKeyTranslateNoDeadKeysMask, + &mut dead_keys, + string.len() as ffi::UniCharCount, + &mut result_len, + string.as_mut_ptr(), + ) + }; + unsafe { + CFRelease(input_source as *mut c_void); + } + if translate_result != 0 { + tracing::error!( + "`UCKeyTranslate` returned with the non-zero value: {}", + translate_result + ); + return Key::Unidentified(NativeKey::MacOS(scancode)); + } + if result_len == 0 { + // This is fine - not all keys have text representation. + // For instance, users that have mapped the `Fn` key to toggle + // keyboard layouts will hit this code path. + return Key::Unidentified(NativeKey::MacOS(scancode)); + } + let chars = String::from_utf16_lossy(&string[0..result_len as usize]); + Key::Character(SmolStr::new(chars)) +} + +// Ignores all modifiers except for SHIFT (yes, even ALT is ignored). +fn get_logical_key_char(ns_event: &NSEvent, modifierless_chars: &str) -> Key { + let string = unsafe { ns_event.charactersIgnoringModifiers() } + .map(|s| s.to_string()) + .unwrap_or_default(); + if string.is_empty() { + // Probably a dead key + let first_char = modifierless_chars.chars().next(); + return Key::Dead(first_char); + } + Key::Character(SmolStr::new(string)) +} + +/// Create `KeyEvent` for the given `NSEvent`. +/// +/// This function shouldn't be called when the IME input is in process. +pub(crate) fn create_key_event( + ns_event: &NSEvent, + is_press: bool, + is_repeat: bool, + key_override: Option, +) -> KeyEvent { + use ElementState::{Pressed, Released}; + let state = if is_press { Pressed } else { Released }; + + let scancode = unsafe { ns_event.keyCode() }; + let mut physical_key = + key_override.unwrap_or_else(|| scancode_to_physicalkey(scancode as u32)); + + // NOTE: The logical key should heed both SHIFT and ALT if possible. + // For instance: + // * Pressing the A key: logical key should be "a" + // * Pressing SHIFT A: logical key should be "A" + // * Pressing CTRL SHIFT A: logical key should also be "A" + // This is not easy to tease out of `NSEvent`, but we do our best. + + let text_with_all_modifiers: Option = if key_override.is_some() { + None + } else { + let characters = unsafe { ns_event.characters() } + .map(|s| s.to_string()) + .unwrap_or_default(); + if characters.is_empty() { + None + } else { + if matches!(physical_key, PhysicalKey::Unidentified(_)) { + // The key may be one of the funky function keys + physical_key = extra_function_key_to_code(scancode, &characters); + } + Some(SmolStr::new(characters)) + } + }; + + let key_from_code = code_to_key(physical_key, scancode); + let (logical_key, key_without_modifiers) = + if matches!(key_from_code, Key::Unidentified(_)) { + // `get_modifierless_char/key_without_modifiers` ignores ALL modifiers. + let key_without_modifiers = get_modifierless_char(scancode); + + let modifiers = unsafe { ns_event.modifierFlags() }; + let has_ctrl = + modifiers.contains(NSEventModifierFlags::NSEventModifierFlagControl); + let has_cmd = + modifiers.contains(NSEventModifierFlags::NSEventModifierFlagCommand); + + let logical_key = match text_with_all_modifiers.as_ref() { + // Only checking for ctrl and cmd here, not checking for alt because we DO want to + // include its effect in the key. For example if -on the Germay layout- one + // presses alt+8, the logical key should be "{" + // Also not checking if this is a release event because then this issue would + // still affect the key release. + Some(text) if !has_ctrl && !has_cmd => { + // Character heeding both SHIFT and ALT. + Key::Character(text.clone()) + } + + _ => match key_without_modifiers.as_ref() { + // Character heeding just SHIFT, ignoring ALT. + Key::Character(ch) => get_logical_key_char(ns_event, ch), + + // Character ignoring ALL modifiers. + _ => key_without_modifiers.clone(), + }, + }; + + (logical_key, key_without_modifiers) + } else { + (key_from_code.clone(), key_from_code) + }; + + let text = if is_press { + logical_key.to_text().map(SmolStr::new) + } else { + None + }; + + let location = code_to_location(physical_key); + + KeyEvent { + location, + logical_key, + physical_key, + repeat: is_repeat, + state, + text, + platform_specific: KeyEventExtra { + text_with_all_modifiers, + key_without_modifiers, + }, + } +} + +pub fn code_to_key(key: PhysicalKey, scancode: u16) -> Key { + let code = match key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(code) => return Key::Unidentified(code.into()), + }; + + Key::Named(match code { + KeyCode::Enter => NamedKey::Enter, + KeyCode::Tab => NamedKey::Tab, + KeyCode::Space => NamedKey::Space, + KeyCode::Backspace => NamedKey::Backspace, + KeyCode::Escape => NamedKey::Escape, + KeyCode::SuperRight => NamedKey::Super, + KeyCode::SuperLeft => NamedKey::Super, + KeyCode::ShiftLeft => NamedKey::Shift, + KeyCode::AltLeft => NamedKey::Alt, + KeyCode::ControlLeft => NamedKey::Control, + KeyCode::ShiftRight => NamedKey::Shift, + KeyCode::AltRight => NamedKey::Alt, + KeyCode::ControlRight => NamedKey::Control, + + KeyCode::NumLock => NamedKey::NumLock, + KeyCode::AudioVolumeUp => NamedKey::AudioVolumeUp, + KeyCode::AudioVolumeDown => NamedKey::AudioVolumeDown, + + // Other numpad keys all generate text on macOS (if I understand correctly) + KeyCode::NumpadEnter => NamedKey::Enter, + + KeyCode::F1 => NamedKey::F1, + KeyCode::F2 => NamedKey::F2, + KeyCode::F3 => NamedKey::F3, + KeyCode::F4 => NamedKey::F4, + KeyCode::F5 => NamedKey::F5, + KeyCode::F6 => NamedKey::F6, + KeyCode::F7 => NamedKey::F7, + KeyCode::F8 => NamedKey::F8, + KeyCode::F9 => NamedKey::F9, + KeyCode::F10 => NamedKey::F10, + KeyCode::F11 => NamedKey::F11, + KeyCode::F12 => NamedKey::F12, + KeyCode::F13 => NamedKey::F13, + KeyCode::F14 => NamedKey::F14, + KeyCode::F15 => NamedKey::F15, + KeyCode::F16 => NamedKey::F16, + KeyCode::F17 => NamedKey::F17, + KeyCode::F18 => NamedKey::F18, + KeyCode::F19 => NamedKey::F19, + KeyCode::F20 => NamedKey::F20, + + KeyCode::Insert => NamedKey::Insert, + KeyCode::Home => NamedKey::Home, + KeyCode::PageUp => NamedKey::PageUp, + KeyCode::Delete => NamedKey::Delete, + KeyCode::End => NamedKey::End, + KeyCode::PageDown => NamedKey::PageDown, + KeyCode::ArrowLeft => NamedKey::ArrowLeft, + KeyCode::ArrowRight => NamedKey::ArrowRight, + KeyCode::ArrowDown => NamedKey::ArrowDown, + KeyCode::ArrowUp => NamedKey::ArrowUp, + _ => return Key::Unidentified(NativeKey::MacOS(scancode)), + }) +} + +pub fn code_to_location(key: PhysicalKey) -> KeyLocation { + let code = match key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(_) => return KeyLocation::Standard, + }; + + match code { + KeyCode::SuperRight => KeyLocation::Right, + KeyCode::SuperLeft => KeyLocation::Left, + KeyCode::ShiftLeft => KeyLocation::Left, + KeyCode::AltLeft => KeyLocation::Left, + KeyCode::ControlLeft => KeyLocation::Left, + KeyCode::ShiftRight => KeyLocation::Right, + KeyCode::AltRight => KeyLocation::Right, + KeyCode::ControlRight => KeyLocation::Right, + + KeyCode::NumLock => KeyLocation::Numpad, + KeyCode::NumpadDecimal => KeyLocation::Numpad, + KeyCode::NumpadMultiply => KeyLocation::Numpad, + KeyCode::NumpadAdd => KeyLocation::Numpad, + KeyCode::NumpadDivide => KeyLocation::Numpad, + KeyCode::NumpadEnter => KeyLocation::Numpad, + KeyCode::NumpadSubtract => KeyLocation::Numpad, + KeyCode::NumpadEqual => KeyLocation::Numpad, + KeyCode::Numpad0 => KeyLocation::Numpad, + KeyCode::Numpad1 => KeyLocation::Numpad, + KeyCode::Numpad2 => KeyLocation::Numpad, + KeyCode::Numpad3 => KeyLocation::Numpad, + KeyCode::Numpad4 => KeyLocation::Numpad, + KeyCode::Numpad5 => KeyLocation::Numpad, + KeyCode::Numpad6 => KeyLocation::Numpad, + KeyCode::Numpad7 => KeyLocation::Numpad, + KeyCode::Numpad8 => KeyLocation::Numpad, + KeyCode::Numpad9 => KeyLocation::Numpad, + + _ => KeyLocation::Standard, + } +} + +// While F1-F20 have scancodes we can match on, we have to check against UTF-16 +// constants for the rest. +// https://developer.apple.com/documentation/appkit/1535851-function-key_unicodes?preferredLanguage=occ +pub fn extra_function_key_to_code(scancode: u16, string: &str) -> PhysicalKey { + if let Some(ch) = string.encode_utf16().next() { + match ch { + 0xf718 => PhysicalKey::Code(KeyCode::F21), + 0xf719 => PhysicalKey::Code(KeyCode::F22), + 0xf71a => PhysicalKey::Code(KeyCode::F23), + 0xf71b => PhysicalKey::Code(KeyCode::F24), + _ => PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode)), + } + } else { + PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode)) + } +} + +// The values are from the https://github.com/apple-oss-distributions/IOHIDFamily/blob/19666c840a6d896468416ff0007040a10b7b46b8/IOHIDSystem/IOKit/hidsystem/IOLLEvent.h#L258-L259 +const NX_DEVICELCTLKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000001); +const NX_DEVICELSHIFTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000002); +const NX_DEVICERSHIFTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000004); +const NX_DEVICELCMDKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000008); +const NX_DEVICERCMDKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000010); +const NX_DEVICELALTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000020); +const NX_DEVICERALTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000040); +const NX_DEVICERCTLKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00002000); + +pub(super) fn lalt_pressed(event: &NSEvent) -> bool { + unsafe { event.modifierFlags() }.contains(NX_DEVICELALTKEYMASK) +} + +pub(super) fn ralt_pressed(event: &NSEvent) -> bool { + unsafe { event.modifierFlags() }.contains(NX_DEVICERALTKEYMASK) +} + +pub(super) fn event_mods(event: &NSEvent) -> Modifiers { + let flags = unsafe { event.modifierFlags() }; + let mut state = ModifiersState::empty(); + let mut pressed_mods = ModifiersKeys::empty(); + + state.set( + ModifiersState::SHIFT, + flags.contains(NSEventModifierFlags::NSEventModifierFlagShift), + ); + pressed_mods.set( + ModifiersKeys::LSHIFT, + flags.contains(NX_DEVICELSHIFTKEYMASK), + ); + pressed_mods.set( + ModifiersKeys::RSHIFT, + flags.contains(NX_DEVICERSHIFTKEYMASK), + ); + + state.set( + ModifiersState::CONTROL, + flags.contains(NSEventModifierFlags::NSEventModifierFlagControl), + ); + pressed_mods.set( + ModifiersKeys::LCONTROL, + flags.contains(NX_DEVICELCTLKEYMASK), + ); + pressed_mods.set( + ModifiersKeys::RCONTROL, + flags.contains(NX_DEVICERCTLKEYMASK), + ); + + state.set( + ModifiersState::ALT, + flags.contains(NSEventModifierFlags::NSEventModifierFlagOption), + ); + pressed_mods.set(ModifiersKeys::LALT, flags.contains(NX_DEVICELALTKEYMASK)); + pressed_mods.set(ModifiersKeys::RALT, flags.contains(NX_DEVICERALTKEYMASK)); + + state.set( + ModifiersState::SUPER, + flags.contains(NSEventModifierFlags::NSEventModifierFlagCommand), + ); + pressed_mods.set(ModifiersKeys::LSUPER, flags.contains(NX_DEVICELCMDKEYMASK)); + pressed_mods.set(ModifiersKeys::RSUPER, flags.contains(NX_DEVICERCMDKEYMASK)); + + Modifiers { + state, + pressed_mods, + } +} + +pub(super) fn dummy_event() -> Option> { + unsafe { + NSEvent::otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2( + NSEventType::ApplicationDefined, + NSPoint::new(0.0, 0.0), + NSEventModifierFlags(0), + 0.0, + 0, + None, + NSEventSubtype::WindowExposed.0, + 0, + 0, + ) + } +} + +pub(crate) fn physicalkey_to_scancode(physical_key: PhysicalKey) -> Option { + let code = match physical_key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(_) => return None, + }; + + match code { + KeyCode::KeyA => Some(0x00), + KeyCode::KeyS => Some(0x01), + KeyCode::KeyD => Some(0x02), + KeyCode::KeyF => Some(0x03), + KeyCode::KeyH => Some(0x04), + KeyCode::KeyG => Some(0x05), + KeyCode::KeyZ => Some(0x06), + KeyCode::KeyX => Some(0x07), + KeyCode::KeyC => Some(0x08), + KeyCode::KeyV => Some(0x09), + KeyCode::KeyB => Some(0x0b), + KeyCode::KeyQ => Some(0x0c), + KeyCode::KeyW => Some(0x0d), + KeyCode::KeyE => Some(0x0e), + KeyCode::KeyR => Some(0x0f), + KeyCode::KeyY => Some(0x10), + KeyCode::KeyT => Some(0x11), + KeyCode::Digit1 => Some(0x12), + KeyCode::Digit2 => Some(0x13), + KeyCode::Digit3 => Some(0x14), + KeyCode::Digit4 => Some(0x15), + KeyCode::Digit6 => Some(0x16), + KeyCode::Digit5 => Some(0x17), + KeyCode::Equal => Some(0x18), + KeyCode::Digit9 => Some(0x19), + KeyCode::Digit7 => Some(0x1a), + KeyCode::Minus => Some(0x1b), + KeyCode::Digit8 => Some(0x1c), + KeyCode::Digit0 => Some(0x1d), + KeyCode::BracketRight => Some(0x1e), + KeyCode::KeyO => Some(0x1f), + KeyCode::KeyU => Some(0x20), + KeyCode::BracketLeft => Some(0x21), + KeyCode::KeyI => Some(0x22), + KeyCode::KeyP => Some(0x23), + KeyCode::Enter => Some(0x24), + KeyCode::KeyL => Some(0x25), + KeyCode::KeyJ => Some(0x26), + KeyCode::Quote => Some(0x27), + KeyCode::KeyK => Some(0x28), + KeyCode::Semicolon => Some(0x29), + KeyCode::Backslash => Some(0x2a), + KeyCode::Comma => Some(0x2b), + KeyCode::Slash => Some(0x2c), + KeyCode::KeyN => Some(0x2d), + KeyCode::KeyM => Some(0x2e), + KeyCode::Period => Some(0x2f), + KeyCode::Tab => Some(0x30), + KeyCode::Space => Some(0x31), + KeyCode::Backquote => Some(0x32), + KeyCode::Backspace => Some(0x33), + KeyCode::Escape => Some(0x35), + KeyCode::SuperRight => Some(0x36), + KeyCode::SuperLeft => Some(0x37), + KeyCode::ShiftLeft => Some(0x38), + KeyCode::AltLeft => Some(0x3a), + KeyCode::ControlLeft => Some(0x3b), + KeyCode::ShiftRight => Some(0x3c), + KeyCode::AltRight => Some(0x3d), + KeyCode::ControlRight => Some(0x3e), + KeyCode::F17 => Some(0x40), + KeyCode::NumpadDecimal => Some(0x41), + KeyCode::NumpadMultiply => Some(0x43), + KeyCode::NumpadAdd => Some(0x45), + KeyCode::NumLock => Some(0x47), + KeyCode::AudioVolumeUp => Some(0x49), + KeyCode::AudioVolumeDown => Some(0x4a), + KeyCode::NumpadDivide => Some(0x4b), + KeyCode::NumpadEnter => Some(0x4c), + KeyCode::NumpadSubtract => Some(0x4e), + KeyCode::F18 => Some(0x4f), + KeyCode::F19 => Some(0x50), + KeyCode::NumpadEqual => Some(0x51), + KeyCode::Numpad0 => Some(0x52), + KeyCode::Numpad1 => Some(0x53), + KeyCode::Numpad2 => Some(0x54), + KeyCode::Numpad3 => Some(0x55), + KeyCode::Numpad4 => Some(0x56), + KeyCode::Numpad5 => Some(0x57), + KeyCode::Numpad6 => Some(0x58), + KeyCode::Numpad7 => Some(0x59), + KeyCode::F20 => Some(0x5a), + KeyCode::Numpad8 => Some(0x5b), + KeyCode::Numpad9 => Some(0x5c), + KeyCode::IntlYen => Some(0x5d), + KeyCode::F5 => Some(0x60), + KeyCode::F6 => Some(0x61), + KeyCode::F7 => Some(0x62), + KeyCode::F3 => Some(0x63), + KeyCode::F8 => Some(0x64), + KeyCode::F9 => Some(0x65), + KeyCode::F11 => Some(0x67), + KeyCode::F13 => Some(0x69), + KeyCode::F16 => Some(0x6a), + KeyCode::F14 => Some(0x6b), + KeyCode::F10 => Some(0x6d), + KeyCode::F12 => Some(0x6f), + KeyCode::F15 => Some(0x71), + KeyCode::Insert => Some(0x72), + KeyCode::Home => Some(0x73), + KeyCode::PageUp => Some(0x74), + KeyCode::Delete => Some(0x75), + KeyCode::F4 => Some(0x76), + KeyCode::End => Some(0x77), + KeyCode::F2 => Some(0x78), + KeyCode::PageDown => Some(0x79), + KeyCode::F1 => Some(0x7a), + KeyCode::ArrowLeft => Some(0x7b), + KeyCode::ArrowRight => Some(0x7c), + KeyCode::ArrowDown => Some(0x7d), + KeyCode::ArrowUp => Some(0x7e), + _ => None, + } +} + +pub(crate) fn scancode_to_physicalkey(scancode: u32) -> PhysicalKey { + PhysicalKey::Code(match scancode { + 0x00 => KeyCode::KeyA, + 0x01 => KeyCode::KeyS, + 0x02 => KeyCode::KeyD, + 0x03 => KeyCode::KeyF, + 0x04 => KeyCode::KeyH, + 0x05 => KeyCode::KeyG, + 0x06 => KeyCode::KeyZ, + 0x07 => KeyCode::KeyX, + 0x08 => KeyCode::KeyC, + 0x09 => KeyCode::KeyV, + // 0x0a => World 1, + 0x0b => KeyCode::KeyB, + 0x0c => KeyCode::KeyQ, + 0x0d => KeyCode::KeyW, + 0x0e => KeyCode::KeyE, + 0x0f => KeyCode::KeyR, + 0x10 => KeyCode::KeyY, + 0x11 => KeyCode::KeyT, + 0x12 => KeyCode::Digit1, + 0x13 => KeyCode::Digit2, + 0x14 => KeyCode::Digit3, + 0x15 => KeyCode::Digit4, + 0x16 => KeyCode::Digit6, + 0x17 => KeyCode::Digit5, + 0x18 => KeyCode::Equal, + 0x19 => KeyCode::Digit9, + 0x1a => KeyCode::Digit7, + 0x1b => KeyCode::Minus, + 0x1c => KeyCode::Digit8, + 0x1d => KeyCode::Digit0, + 0x1e => KeyCode::BracketRight, + 0x1f => KeyCode::KeyO, + 0x20 => KeyCode::KeyU, + 0x21 => KeyCode::BracketLeft, + 0x22 => KeyCode::KeyI, + 0x23 => KeyCode::KeyP, + 0x24 => KeyCode::Enter, + 0x25 => KeyCode::KeyL, + 0x26 => KeyCode::KeyJ, + 0x27 => KeyCode::Quote, + 0x28 => KeyCode::KeyK, + 0x29 => KeyCode::Semicolon, + 0x2a => KeyCode::Backslash, + 0x2b => KeyCode::Comma, + 0x2c => KeyCode::Slash, + 0x2d => KeyCode::KeyN, + 0x2e => KeyCode::KeyM, + 0x2f => KeyCode::Period, + 0x30 => KeyCode::Tab, + 0x31 => KeyCode::Space, + 0x32 => KeyCode::Backquote, + 0x33 => KeyCode::Backspace, + // 0x34 => unknown, + 0x35 => KeyCode::Escape, + 0x36 => KeyCode::SuperRight, + 0x37 => KeyCode::SuperLeft, + 0x38 => KeyCode::ShiftLeft, + 0x39 => KeyCode::CapsLock, + 0x3a => KeyCode::AltLeft, + 0x3b => KeyCode::ControlLeft, + 0x3c => KeyCode::ShiftRight, + 0x3d => KeyCode::AltRight, + 0x3e => KeyCode::ControlRight, + 0x3f => KeyCode::Fn, + 0x40 => KeyCode::F17, + 0x41 => KeyCode::NumpadDecimal, + // 0x42 -> unknown, + 0x43 => KeyCode::NumpadMultiply, + // 0x44 => unknown, + 0x45 => KeyCode::NumpadAdd, + // 0x46 => unknown, + 0x47 => KeyCode::NumLock, + // 0x48 => KeyCode::NumpadClear, + + // TODO: (Artur) for me, kVK_VolumeUp is 0x48 + // macOS 10.11 + // /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/ + // Versions/A/Headers/Events.h + 0x49 => KeyCode::AudioVolumeUp, + 0x4a => KeyCode::AudioVolumeDown, + 0x4b => KeyCode::NumpadDivide, + 0x4c => KeyCode::NumpadEnter, + // 0x4d => unknown, + 0x4e => KeyCode::NumpadSubtract, + 0x4f => KeyCode::F18, + 0x50 => KeyCode::F19, + 0x51 => KeyCode::NumpadEqual, + 0x52 => KeyCode::Numpad0, + 0x53 => KeyCode::Numpad1, + 0x54 => KeyCode::Numpad2, + 0x55 => KeyCode::Numpad3, + 0x56 => KeyCode::Numpad4, + 0x57 => KeyCode::Numpad5, + 0x58 => KeyCode::Numpad6, + 0x59 => KeyCode::Numpad7, + 0x5a => KeyCode::F20, + 0x5b => KeyCode::Numpad8, + 0x5c => KeyCode::Numpad9, + 0x5d => KeyCode::IntlYen, + // 0x5e => JIS Ro, + // 0x5f => unknown, + 0x60 => KeyCode::F5, + 0x61 => KeyCode::F6, + 0x62 => KeyCode::F7, + 0x63 => KeyCode::F3, + 0x64 => KeyCode::F8, + 0x65 => KeyCode::F9, + // 0x66 => JIS Eisuu (macOS), + 0x67 => KeyCode::F11, + // 0x68 => JIS Kanna (macOS), + 0x69 => KeyCode::F13, + 0x6a => KeyCode::F16, + 0x6b => KeyCode::F14, + // 0x6c => unknown, + 0x6d => KeyCode::F10, + // 0x6e => unknown, + 0x6f => KeyCode::F12, + // 0x70 => unknown, + 0x71 => KeyCode::F15, + 0x72 => KeyCode::Insert, + 0x73 => KeyCode::Home, + 0x74 => KeyCode::PageUp, + 0x75 => KeyCode::Delete, + 0x76 => KeyCode::F4, + 0x77 => KeyCode::End, + 0x78 => KeyCode::F2, + 0x79 => KeyCode::PageDown, + 0x7a => KeyCode::F1, + 0x7b => KeyCode::ArrowLeft, + 0x7c => KeyCode::ArrowRight, + 0x7d => KeyCode::ArrowDown, + 0x7e => KeyCode::ArrowUp, + // 0x7f => unknown, + + // 0xA is the caret (^) an macOS's German QERTZ layout. This key is at the same location as + // backquote (`) on Windows' US layout. + 0xa => KeyCode::Backquote, + _ => return PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode as u16)), + }) +} diff --git a/rio-window/src/platform_impl/macos/event_handler.rs b/rio-window/src/platform_impl/macos/event_handler.rs new file mode 100644 index 00000000..77cc07c4 --- /dev/null +++ b/rio-window/src/platform_impl/macos/event_handler.rs @@ -0,0 +1,142 @@ +use std::cell::RefCell; +use std::{fmt, mem}; + +use super::app_delegate::HandlePendingUserEvents; +use crate::event::Event; +use crate::event_loop::ActiveEventLoop as RootActiveEventLoop; + +struct EventHandlerData { + #[allow(clippy::type_complexity)] + handler: + Box, &RootActiveEventLoop) + 'static>, +} + +impl fmt::Debug for EventHandlerData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("EventHandlerData").finish_non_exhaustive() + } +} + +#[derive(Debug, Default)] +pub(crate) struct EventHandler { + /// This can be in the following states: + /// - Not registered by the event loop (None). + /// - Present (Some(handler)). + /// - Currently executing the handler / in use (RefCell borrowed). + inner: RefCell>, +} + +impl EventHandler { + /// Set the event loop handler for the duration of the given closure. + /// + /// This is similar to using the `scoped-tls` or `scoped-tls-hkt` crates + /// to store the handler in a thread local, such that it can be accessed + /// from within the closure. + pub(crate) fn set<'handler, R>( + &self, + handler: impl FnMut(Event, &RootActiveEventLoop) + 'handler, + closure: impl FnOnce() -> R, + ) -> R { + // SAFETY: We extend the lifetime of the handler here so that we can + // store it in `EventHandler`'s `RefCell`. + // + // This is sound, since we make sure to unset the handler again at the + // end of this function, and as such the lifetime isn't actually + // extended beyond `'handler`. + let handler = unsafe { + mem::transmute::< + Box< + dyn FnMut(Event, &RootActiveEventLoop) + + 'handler, + >, + Box< + dyn FnMut(Event, &RootActiveEventLoop) + + 'static, + >, + >(Box::new(handler)) + }; + + match self.inner.try_borrow_mut().as_deref_mut() { + Ok(Some(_)) => { + unreachable!("tried to set handler while another was already set"); + } + Ok(data @ None) => { + *data = Some(EventHandlerData { handler }); + } + Err(_) => { + unreachable!("tried to set handler that is currently in use"); + } + } + + struct ClearOnDrop<'a>(&'a EventHandler); + + impl Drop for ClearOnDrop<'_> { + fn drop(&mut self) { + match self.0.inner.try_borrow_mut().as_deref_mut() { + Ok(data @ Some(_)) => { + *data = None; + } + Ok(None) => { + tracing::error!("tried to clear handler, but no handler was set"); + } + Err(_) => { + // Note: This is not expected to ever happen, this + // module generally controls the `RefCell`, and + // prevents it from ever being borrowed outside of it. + // + // But if it _does_ happen, it is a serious error, and + // we must abort the process, it'd be unsound if we + // weren't able to unset the handler. + eprintln!("tried to clear handler that is currently in use"); + std::process::abort(); + } + } + } + } + + let _clear_on_drop = ClearOnDrop(self); + + // Note: The RefCell should not be borrowed while executing the + // closure, that'd defeat the whole point. + closure() + + // `_clear_on_drop` will be dropped here, or when unwinding, ensuring + // soundness. + } + + pub(crate) fn in_use(&self) -> bool { + self.inner.try_borrow().is_err() + } + + pub(crate) fn ready(&self) -> bool { + matches!(self.inner.try_borrow().as_deref(), Ok(Some(_))) + } + + pub(crate) fn handle_event( + &self, + event: Event, + event_loop: &RootActiveEventLoop, + ) { + match self.inner.try_borrow_mut().as_deref_mut() { + Ok(Some(EventHandlerData { handler })) => { + // It is important that we keep the reference borrowed here, + // so that `in_use` can properly detect that the handler is + // still in use. + // + // If the handler unwinds, the `RefMut` will ensure that the + // handler is no longer borrowed. + (handler)(event, event_loop); + } + Ok(None) => { + // `NSApplication`, our app delegate and this handler are all + // global state and so it's not impossible that we could get + // an event after the application has exited the `EventLoop`. + tracing::error!("tried to run event handler, but no handler was set"); + } + Err(_) => { + // Prevent re-entrancy. + panic!("tried to handle event while another event is currently being handled"); + } + } + } +} diff --git a/rio-window/src/platform_impl/macos/event_loop.rs b/rio-window/src/platform_impl/macos/event_loop.rs new file mode 100644 index 00000000..66b637f4 --- /dev/null +++ b/rio-window/src/platform_impl/macos/event_loop.rs @@ -0,0 +1,501 @@ +use std::any::Any; +use std::cell::Cell; +use std::collections::VecDeque; +use std::marker::PhantomData; +use std::os::raw::c_void; +use std::panic::{catch_unwind, resume_unwind, RefUnwindSafe, UnwindSafe}; +use std::ptr; +use std::rc::{Rc, Weak}; +use std::sync::mpsc; +use std::time::{Duration, Instant}; + +use core_foundation::base::{CFIndex, CFRelease}; +use core_foundation::runloop::{ + kCFRunLoopCommonModes, CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopSourceContext, + CFRunLoopSourceCreate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp, +}; +use objc2::rc::{autoreleasepool, Retained}; +use objc2::runtime::ProtocolObject; +use objc2::{msg_send_id, ClassType}; +use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSWindow}; +use objc2_foundation::{MainThreadMarker, NSObjectProtocol}; + +use super::app::WinitApplication; +use super::app_delegate::{ApplicationDelegate, HandlePendingUserEvents}; +use super::event::dummy_event; +use super::monitor::{self, MonitorHandle}; +use super::observer::setup_control_flow_observers; +use crate::error::EventLoopError; +use crate::event::Event; +use crate::event_loop::{ + ActiveEventLoop as RootWindowTarget, ControlFlow, DeviceEvents, EventLoopClosed, +}; +use crate::platform::macos::ActivationPolicy; +use crate::platform::pump_events::PumpStatus; +use crate::platform_impl::platform::cursor::CustomCursor; +use crate::window::{CustomCursor as RootCustomCursor, CustomCursorSource}; + +#[derive(Default)] +pub struct PanicInfo { + inner: Cell>>, +} + +// WARNING: +// As long as this struct is used through its `impl`, it is UnwindSafe. +// (If `get_mut` is called on `inner`, unwind safety may get broken.) +impl UnwindSafe for PanicInfo {} +impl RefUnwindSafe for PanicInfo {} +impl PanicInfo { + pub fn is_panicking(&self) -> bool { + let inner = self.inner.take(); + let result = inner.is_some(); + self.inner.set(inner); + result + } + + /// Overwrites the current state if the current state is not panicking + pub fn set_panic(&self, p: Box) { + if !self.is_panicking() { + self.inner.set(Some(p)); + } + } + + pub fn take(&self) -> Option> { + self.inner.take() + } +} + +#[derive(Debug)] +pub struct ActiveEventLoop { + delegate: Retained, + pub(super) mtm: MainThreadMarker, +} + +impl ActiveEventLoop { + pub(super) fn new_root(delegate: Retained) -> RootWindowTarget { + let mtm = MainThreadMarker::from(&*delegate); + let p = Self { delegate, mtm }; + RootWindowTarget { + p, + _marker: PhantomData, + } + } + + pub(super) fn app_delegate(&self) -> &ApplicationDelegate { + &self.delegate + } + + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> RootCustomCursor { + RootCustomCursor { + inner: CustomCursor::new(source.inner), + } + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + monitor::available_monitors() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + let monitor = monitor::primary_monitor(); + Some(monitor) + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::AppKit( + rwh_06::AppKitDisplayHandle::new(), + )) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.delegate.set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.delegate.control_flow() + } + + pub(crate) fn exit(&self) { + self.delegate.exit() + } + + pub(crate) fn clear_exit(&self) { + self.delegate.clear_exit() + } + + pub(crate) fn exiting(&self) -> bool { + self.delegate.exiting() + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } + + pub(crate) fn hide_application(&self) { + NSApplication::sharedApplication(self.mtm).hide(None) + } + + pub(crate) fn hide_other_applications(&self) { + NSApplication::sharedApplication(self.mtm).hideOtherApplications(None) + } + + pub(crate) fn set_allows_automatic_window_tabbing(&self, enabled: bool) { + NSWindow::setAllowsAutomaticWindowTabbing(enabled, self.mtm) + } + + pub(crate) fn allows_automatic_window_tabbing(&self) -> bool { + NSWindow::allowsAutomaticWindowTabbing(self.mtm) + } +} + +fn map_user_event( + mut handler: impl FnMut(Event, &RootWindowTarget), + receiver: Rc>, +) -> impl FnMut(Event, &RootWindowTarget) { + move |event, window_target| match event.map_nonuser_event() { + Ok(event) => (handler)(event, window_target), + Err(_) => { + for event in receiver.try_iter() { + (handler)(Event::UserEvent(event), window_target); + } + } + } +} + +pub struct EventLoop { + /// Store a reference to the application for convenience. + /// + /// We intentionally don't store `WinitApplication` since we want to have + /// the possibility of swapping that out at some point. + app: Retained, + /// The application delegate that we've registered. + /// + /// The delegate is only weakly referenced by NSApplication, so we must + /// keep it around here as well. + delegate: Retained, + + // Event sender and receiver, used for EventLoopProxy. + sender: mpsc::Sender, + receiver: Rc>, + + window_target: RootWindowTarget, + panic_info: Rc, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes { + pub(crate) activation_policy: ActivationPolicy, + pub(crate) default_menu: bool, + pub(crate) activate_ignoring_other_apps: bool, +} + +impl Default for PlatformSpecificEventLoopAttributes { + fn default() -> Self { + Self { + activation_policy: Default::default(), // Regular + default_menu: true, + activate_ignoring_other_apps: true, + } + } +} + +impl EventLoop { + pub(crate) fn new( + attributes: &PlatformSpecificEventLoopAttributes, + ) -> Result { + let mtm = MainThreadMarker::new() + .expect("on macOS, `EventLoop` must be created on the main thread!"); + + let app: Retained = + unsafe { msg_send_id![WinitApplication::class(), sharedApplication] }; + + if !app.is_kind_of::() { + panic!( + "`winit` requires control over the principal class. You must create the event \ + loop before other parts of your application initialize NSApplication" + ); + } + + let activation_policy = match attributes.activation_policy { + ActivationPolicy::Regular => NSApplicationActivationPolicy::Regular, + ActivationPolicy::Accessory => NSApplicationActivationPolicy::Accessory, + ActivationPolicy::Prohibited => NSApplicationActivationPolicy::Prohibited, + }; + let delegate = ApplicationDelegate::new( + mtm, + activation_policy, + attributes.default_menu, + attributes.activate_ignoring_other_apps, + ); + + autoreleasepool(|_| { + app.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); + }); + + let panic_info: Rc = Default::default(); + setup_control_flow_observers(Rc::downgrade(&panic_info)); + + let (sender, receiver) = mpsc::channel(); + Ok(EventLoop { + app, + delegate: delegate.clone(), + sender, + receiver: Rc::new(receiver), + window_target: RootWindowTarget { + p: ActiveEventLoop { delegate, mtm }, + _marker: PhantomData, + }, + panic_info, + }) + } + + pub fn window_target(&self) -> &RootWindowTarget { + &self.window_target + } + + pub fn run(mut self, handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootWindowTarget), + { + self.run_on_demand(handler) + } + + // NB: we don't base this on `pump_events` because for `MacOs` we can't support + // `pump_events` elegantly (we just ask to run the loop for a "short" amount of + // time and so a layered implementation would end up using a lot of CPU due to + // redundant wake ups. + pub fn run_on_demand(&mut self, handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootWindowTarget), + { + let handler = map_user_event(handler, self.receiver.clone()); + + self.delegate.set_event_handler(handler, || { + autoreleasepool(|_| { + // clear / normalize pump_events state + self.delegate.set_wait_timeout(None); + self.delegate.set_stop_before_wait(false); + self.delegate.set_stop_after_wait(false); + self.delegate.set_stop_on_redraw(false); + + if self.delegate.is_launched() { + debug_assert!(!self.delegate.is_running()); + self.delegate.set_is_running(true); + self.delegate.dispatch_init_events(); + } + + // SAFETY: We do not run the application re-entrantly + unsafe { self.app.run() }; + + // While the app is running it's possible that we catch a panic + // to avoid unwinding across an objective-c ffi boundary, which + // will lead to us stopping the `NSApplication` and saving the + // `PanicInfo` so that we can resume the unwind at a controlled, + // safe point in time. + if let Some(panic) = self.panic_info.take() { + resume_unwind(panic); + } + + self.delegate.internal_exit() + }) + }); + + Ok(()) + } + + pub fn pump_events(&mut self, timeout: Option, handler: F) -> PumpStatus + where + F: FnMut(Event, &RootWindowTarget), + { + let handler = map_user_event(handler, self.receiver.clone()); + + self.delegate.set_event_handler(handler, || { + autoreleasepool(|_| { + // As a special case, if the application hasn't been launched yet then we at least + // run the loop until it has fully launched. + if !self.delegate.is_launched() { + debug_assert!(!self.delegate.is_running()); + + self.delegate.set_stop_on_launch(); + // SAFETY: We do not run the application re-entrantly + unsafe { self.app.run() }; + + // Note: we dispatch `NewEvents(Init)` + `Resumed` events after the application + // has launched + } else if !self.delegate.is_running() { + // Even though the application may have been launched, it's possible we aren't + // running if the `EventLoop` was run before and has since + // exited. This indicates that we just starting to re-run + // the same `EventLoop` again. + self.delegate.set_is_running(true); + self.delegate.dispatch_init_events(); + } else { + // Only run for as long as the given `Duration` allows so we don't block the + // external loop. + match timeout { + Some(Duration::ZERO) => { + self.delegate.set_wait_timeout(None); + self.delegate.set_stop_before_wait(true); + } + Some(duration) => { + self.delegate.set_stop_before_wait(false); + let timeout = Instant::now() + duration; + self.delegate.set_wait_timeout(Some(timeout)); + self.delegate.set_stop_after_wait(true); + } + None => { + self.delegate.set_wait_timeout(None); + self.delegate.set_stop_before_wait(false); + self.delegate.set_stop_after_wait(true); + } + } + self.delegate.set_stop_on_redraw(true); + // SAFETY: We do not run the application re-entrantly + unsafe { self.app.run() }; + } + + // While the app is running it's possible that we catch a panic + // to avoid unwinding across an objective-c ffi boundary, which + // will lead to us stopping the application and saving the + // `PanicInfo` so that we can resume the unwind at a controlled, + // safe point in time. + if let Some(panic) = self.panic_info.take() { + resume_unwind(panic); + } + + if self.delegate.exiting() { + self.delegate.internal_exit(); + PumpStatus::Exit(0) + } else { + PumpStatus::Continue + } + }) + }) + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.sender.clone()) + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::AppKitDisplayHandle::new().into()) + } +} + +pub(super) fn stop_app_immediately(app: &NSApplication) { + autoreleasepool(|_| { + app.stop(None); + // To stop event loop immediately, we need to post some event here. + // See: https://stackoverflow.com/questions/48041279/stopping-the-nsapplication-main-event-loop/48064752#48064752 + app.postEvent_atStart(&dummy_event().unwrap(), true); + }); +} + +/// Catches panics that happen inside `f` and when a panic +/// happens, stops the `sharedApplication` +#[inline] +pub fn stop_app_on_panic R + UnwindSafe, R>( + mtm: MainThreadMarker, + panic_info: Weak, + f: F, +) -> Option { + match catch_unwind(f) { + Ok(r) => Some(r), + Err(e) => { + // It's important that we set the panic before requesting a `stop` + // because some callback are still called during the `stop` message + // and we need to know in those callbacks if the application is currently + // panicking + { + let panic_info = panic_info.upgrade().unwrap(); + panic_info.set_panic(e); + } + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + None + } + } +} + +pub struct EventLoopProxy { + sender: mpsc::Sender, + source: CFRunLoopSourceRef, +} + +unsafe impl Send for EventLoopProxy {} +unsafe impl Sync for EventLoopProxy {} + +impl Drop for EventLoopProxy { + fn drop(&mut self) { + unsafe { + CFRelease(self.source as _); + } + } +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy::new(self.sender.clone()) + } +} + +impl EventLoopProxy { + fn new(sender: mpsc::Sender) -> Self { + unsafe { + // just wake up the eventloop + extern "C" fn event_loop_proxy_handler(_: *const c_void) {} + + // adding a Source to the main CFRunLoop lets us wake it up and + // process user events through the normal OS EventLoop mechanisms. + let rl = CFRunLoopGetMain(); + let mut context = CFRunLoopSourceContext { + version: 0, + info: ptr::null_mut(), + retain: None, + release: None, + copyDescription: None, + equal: None, + hash: None, + schedule: None, + cancel: None, + perform: event_loop_proxy_handler, + }; + let source = + CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::MAX - 1, &mut context); + CFRunLoopAddSource(rl, source, kCFRunLoopCommonModes); + CFRunLoopWakeUp(rl); + + EventLoopProxy { sender, source } + } + } + + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.sender + .send(event) + .map_err(|mpsc::SendError(x)| EventLoopClosed(x))?; + unsafe { + // let the main thread know there's a new event + CFRunLoopSourceSignal(self.source); + let rl = CFRunLoopGetMain(); + CFRunLoopWakeUp(rl); + } + Ok(()) + } +} diff --git a/rio-window/src/platform_impl/macos/ffi.rs b/rio-window/src/platform_impl/macos/ffi.rs new file mode 100644 index 00000000..9d25960d --- /dev/null +++ b/rio-window/src/platform_impl/macos/ffi.rs @@ -0,0 +1,257 @@ +// TODO: Upstream these + +#![allow(dead_code, non_snake_case, non_upper_case_globals)] + +use std::ffi::c_void; + +use core_foundation::array::CFArrayRef; +use core_foundation::dictionary::CFDictionaryRef; +use core_foundation::string::CFStringRef; +use core_foundation::uuid::CFUUIDRef; +use core_graphics::base::CGError; +use core_graphics::display::{CGDirectDisplayID, CGDisplayConfigRef}; +use objc2::ffi::NSInteger; +use objc2::runtime::AnyObject; + +pub type CGDisplayFadeInterval = f32; +pub type CGDisplayReservationInterval = f32; +pub type CGDisplayBlendFraction = f32; + +pub const kCGDisplayBlendNormal: f32 = 0.0; +pub const kCGDisplayBlendSolidColor: f32 = 1.0; + +pub type CGDisplayFadeReservationToken = u32; +pub const kCGDisplayFadeReservationInvalidToken: CGDisplayFadeReservationToken = 0; + +pub type Boolean = u8; +pub const FALSE: Boolean = 0; +pub const TRUE: Boolean = 1; + +pub const kCGErrorSuccess: i32 = 0; +pub const kCGErrorFailure: i32 = 1000; +pub const kCGErrorIllegalArgument: i32 = 1001; +pub const kCGErrorInvalidConnection: i32 = 1002; +pub const kCGErrorInvalidContext: i32 = 1003; +pub const kCGErrorCannotComplete: i32 = 1004; +pub const kCGErrorNotImplemented: i32 = 1006; +pub const kCGErrorRangeCheck: i32 = 1007; +pub const kCGErrorTypeCheck: i32 = 1008; +pub const kCGErrorInvalidOperation: i32 = 1010; +pub const kCGErrorNoneAvailable: i32 = 1011; + +pub const IO1BitIndexedPixels: &str = "P"; +pub const IO2BitIndexedPixels: &str = "PP"; +pub const IO4BitIndexedPixels: &str = "PPPP"; +pub const IO8BitIndexedPixels: &str = "PPPPPPPP"; +pub const IO16BitDirectPixels: &str = "-RRRRRGGGGGBBBBB"; +pub const IO32BitDirectPixels: &str = "--------RRRRRRRRGGGGGGGGBBBBBBBB"; + +pub const kIO30BitDirectPixels: &str = "--RRRRRRRRRRGGGGGGGGGGBBBBBBBBBB"; +pub const kIO64BitDirectPixels: &str = "-16R16G16B16"; + +pub const kIO16BitFloatPixels: &str = "-16FR16FG16FB16"; +pub const kIO32BitFloatPixels: &str = "-32FR32FG32FB32"; + +pub const IOYUV422Pixels: &str = "Y4U2V2"; +pub const IO8BitOverlayPixels: &str = "O8"; + +pub type CGWindowLevel = i32; +pub type CGDisplayModeRef = *mut c_void; + +// `CGDisplayCreateUUIDFromDisplayID` comes from the `ColorSync` framework. +// However, that framework was only introduced "publicly" in macOS 10.13. +// +// Since we want to support older versions, we can't link to `ColorSync` +// directly. Fortunately, it has always been available as a subframework of +// `ApplicationServices`, see: +// https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/OSX_Technology_Overview/SystemFrameworks/SystemFrameworks.html#//apple_ref/doc/uid/TP40001067-CH210-BBCFFIEG +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; +} + +#[link(name = "CoreGraphics", kind = "framework")] +extern "C" { + pub fn CGRestorePermanentDisplayConfiguration(); + pub fn CGDisplayCapture(display: CGDirectDisplayID) -> CGError; + pub fn CGDisplayRelease(display: CGDirectDisplayID) -> CGError; + pub fn CGConfigureDisplayFadeEffect( + config: CGDisplayConfigRef, + fadeOutSeconds: CGDisplayFadeInterval, + fadeInSeconds: CGDisplayFadeInterval, + fadeRed: f32, + fadeGreen: f32, + fadeBlue: f32, + ) -> CGError; + pub fn CGAcquireDisplayFadeReservation( + seconds: CGDisplayReservationInterval, + token: *mut CGDisplayFadeReservationToken, + ) -> CGError; + pub fn CGDisplayFade( + token: CGDisplayFadeReservationToken, + duration: CGDisplayFadeInterval, + startBlend: CGDisplayBlendFraction, + endBlend: CGDisplayBlendFraction, + redBlend: f32, + greenBlend: f32, + blueBlend: f32, + synchronous: Boolean, + ) -> CGError; + pub fn CGReleaseDisplayFadeReservation( + token: CGDisplayFadeReservationToken, + ) -> CGError; + pub fn CGShieldingWindowLevel() -> CGWindowLevel; + pub fn CGDisplaySetDisplayMode( + display: CGDirectDisplayID, + mode: CGDisplayModeRef, + options: CFDictionaryRef, + ) -> CGError; + pub fn CGDisplayCopyAllDisplayModes( + display: CGDirectDisplayID, + options: CFDictionaryRef, + ) -> CFArrayRef; + pub fn CGDisplayModeGetPixelWidth(mode: CGDisplayModeRef) -> usize; + pub fn CGDisplayModeGetPixelHeight(mode: CGDisplayModeRef) -> usize; + pub fn CGDisplayModeGetRefreshRate(mode: CGDisplayModeRef) -> f64; + pub fn CGDisplayModeCopyPixelEncoding(mode: CGDisplayModeRef) -> CFStringRef; + pub fn CGDisplayModeRetain(mode: CGDisplayModeRef); + pub fn CGDisplayModeRelease(mode: CGDisplayModeRef); + + // Wildly used private APIs; Apple uses them for their Terminal.app. + pub fn CGSMainConnectionID() -> *mut AnyObject; + pub fn CGSSetWindowBackgroundBlurRadius( + connection_id: *mut AnyObject, + window_id: NSInteger, + radius: i64, + ) -> i32; +} + +mod core_video { + use super::*; + + #[link(name = "CoreVideo", kind = "framework")] + extern "C" {} + + // CVBase.h + + pub type CVTimeFlags = i32; // int32_t + pub const kCVTimeIsIndefinite: CVTimeFlags = 1 << 0; + + #[repr(C)] + #[derive(Debug, Clone)] + pub struct CVTime { + pub time_value: i64, // int64_t + pub time_scale: i32, // int32_t + pub flags: i32, // int32_t + } + + // CVReturn.h + + pub type CVReturn = i32; // int32_t + pub const kCVReturnSuccess: CVReturn = 0; + + // CVDisplayLink.h + + pub type CVDisplayLinkRef = *mut c_void; + + extern "C" { + pub fn CVDisplayLinkCreateWithCGDisplay( + displayID: CGDirectDisplayID, + displayLinkOut: *mut CVDisplayLinkRef, + ) -> CVReturn; + pub fn CVDisplayLinkGetNominalOutputVideoRefreshPeriod( + displayLink: CVDisplayLinkRef, + ) -> CVTime; + pub fn CVDisplayLinkRelease(displayLink: CVDisplayLinkRef); + } +} + +pub use core_video::*; +#[repr(transparent)] +pub struct TISInputSource(std::ffi::c_void); +pub type TISInputSourceRef = *mut TISInputSource; + +#[repr(transparent)] +pub struct UCKeyboardLayout(std::ffi::c_void); + +pub type OptionBits = u32; +pub type UniCharCount = std::os::raw::c_ulong; +pub type UniChar = std::os::raw::c_ushort; +pub type OSStatus = i32; + +#[allow(non_upper_case_globals)] +pub const kUCKeyActionDisplay: u16 = 3; +#[allow(non_upper_case_globals)] +pub const kUCKeyTranslateNoDeadKeysMask: OptionBits = 1; + +#[link(name = "Carbon", kind = "framework")] +extern "C" { + pub static kTISPropertyUnicodeKeyLayoutData: CFStringRef; + + #[allow(non_snake_case)] + pub fn TISGetInputSourceProperty( + inputSource: TISInputSourceRef, + propertyKey: CFStringRef, + ) -> *mut c_void; + + pub fn TISCopyCurrentKeyboardLayoutInputSource() -> TISInputSourceRef; + + pub fn LMGetKbdType() -> u8; + + #[allow(non_snake_case)] + pub fn UCKeyTranslate( + keyLayoutPtr: *const UCKeyboardLayout, + virtualKeyCode: u16, + keyAction: u16, + modifierKeyState: u32, + keyboardType: u32, + keyTranslateOptions: OptionBits, + deadKeyState: *mut u32, + maxStringLength: UniCharCount, + actualStringLength: *mut UniCharCount, + unicodeString: *mut UniChar, + ) -> OSStatus; +} + +// CGWindowLevel.h +// +// Note: There are two different things at play in this header: +// `CGWindowLevel` and `CGWindowLevelKey`. +// +// It seems like there was a push towards using "key" values instead of the +// raw window level values, and then you were supposed to use +// `CGWindowLevelForKey` to get the actual level. +// +// But the values that `NSWindowLevel` has are compiled in, and as such has +// to remain ABI compatible, so they're safe for us to hardcode as well. +#[allow(dead_code, non_upper_case_globals)] +mod window_level { + const kCGNumReservedWindowLevels: i32 = 16; + const kCGNumReservedBaseWindowLevels: i32 = 5; + + pub const kCGBaseWindowLevel: i32 = i32::MIN; + pub const kCGMinimumWindowLevel: i32 = + kCGBaseWindowLevel + kCGNumReservedBaseWindowLevels; + pub const kCGMaximumWindowLevel: i32 = i32::MAX - kCGNumReservedWindowLevels; + + pub const kCGDesktopWindowLevel: i32 = kCGMinimumWindowLevel + 20; + pub const kCGDesktopIconWindowLevel: i32 = kCGDesktopWindowLevel + 20; + pub const kCGBackstopMenuLevel: i32 = -20; + pub const kCGNormalWindowLevel: i32 = 0; + pub const kCGFloatingWindowLevel: i32 = 3; + pub const kCGTornOffMenuWindowLevel: i32 = 3; + pub const kCGModalPanelWindowLevel: i32 = 8; + pub const kCGUtilityWindowLevel: i32 = 19; + pub const kCGDockWindowLevel: i32 = 20; + pub const kCGMainMenuWindowLevel: i32 = 24; + pub const kCGStatusWindowLevel: i32 = 25; + pub const kCGPopUpMenuWindowLevel: i32 = 101; + pub const kCGOverlayWindowLevel: i32 = 102; + pub const kCGHelpWindowLevel: i32 = 200; + pub const kCGDraggingWindowLevel: i32 = 500; + pub const kCGScreenSaverWindowLevel: i32 = 1000; + pub const kCGAssistiveTechHighWindowLevel: i32 = 1500; + pub const kCGCursorWindowLevel: i32 = kCGMaximumWindowLevel - 1; +} + +pub use window_level::*; diff --git a/rio-window/src/platform_impl/macos/menu.rs b/rio-window/src/platform_impl/macos/menu.rs new file mode 100644 index 00000000..9b0f953f --- /dev/null +++ b/rio-window/src/platform_impl/macos/menu.rs @@ -0,0 +1,150 @@ +use objc2::rc::Retained; +use objc2::runtime::Sel; +use objc2::sel; +use objc2_app_kit::{NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem}; +use objc2_foundation::{ns_string, MainThreadMarker, NSProcessInfo, NSString}; + +struct KeyEquivalent<'a> { + key: &'a NSString, + masks: Option, +} + +pub fn initialize(app: &NSApplication) { + let mtm = MainThreadMarker::from(app); + let menubar = NSMenu::new(mtm); + + let app_menu_item = NSMenuItem::new(mtm); + let view_menu_item = NSMenuItem::new(mtm); + let help_menu_item = NSMenuItem::new(mtm); + let window_menu_item = NSMenuItem::new(mtm); + + menubar.addItem(&app_menu_item); + menubar.addItem(&view_menu_item); + menubar.addItem(&window_menu_item); + menubar.addItem(&help_menu_item); + + let app_menu = NSMenu::new(mtm); + let process_name = NSProcessInfo::processInfo().processName(); + + // About menu item + let about_item_title = ns_string!("About ").stringByAppendingString(&process_name); + let about_item = menu_item( + mtm, + &about_item_title, + Some(sel!(orderFrontStandardAboutPanel:)), + None, + ); + + // Services menu item + let services_menu = NSMenu::new(mtm); + let services_item = menu_item(mtm, ns_string!("Services"), None, None); + services_item.setSubmenu(Some(&services_menu)); + + // Separator menu item + let sep_first = NSMenuItem::separatorItem(mtm); + + // let open_config_title = ns_string!("Edit Configuration"); + // let open_config = menu_item( + // mtm, + // &open_config_title, + // Some(sel!(openConfig:)), + // Some(KeyEquivalent { key: ns_string!(","), masks: None }), + // ); + + // Hide application menu item + let hide_item_title = ns_string!("Hide ").stringByAppendingString(&process_name); + let hide_item = menu_item( + mtm, + &hide_item_title, + Some(sel!(hide:)), + Some(KeyEquivalent { + key: ns_string!("h"), + masks: None, + }), + ); + + // Hide other applications menu item + let hide_others_item_title = ns_string!("Hide Others"); + let hide_others_item = menu_item( + mtm, + hide_others_item_title, + Some(sel!(hideOtherApplications:)), + Some(KeyEquivalent { + key: ns_string!("h"), + masks: Some( + NSEventModifierFlags::NSEventModifierFlagOption + | NSEventModifierFlags::NSEventModifierFlagCommand, + ), + }), + ); + + // Show applications menu item + let show_all_item_title = ns_string!("Show All"); + let show_all_item = menu_item( + mtm, + show_all_item_title, + Some(sel!(unhideAllApplications:)), + None, + ); + + // Separator menu item + let sep = NSMenuItem::separatorItem(mtm); + + // Quit application menu item + let quit_item_title = ns_string!("Quit ").stringByAppendingString(&process_name); + let quit_item = menu_item( + mtm, + &quit_item_title, + Some(sel!(terminate:)), + Some(KeyEquivalent { + key: ns_string!("q"), + masks: None, + }), + ); + + let view_menu = unsafe { NSMenu::initWithTitle(mtm.alloc(), ns_string!("View")) }; + + let window_menu = unsafe { NSMenu::initWithTitle(mtm.alloc(), ns_string!("Window")) }; + + let help_menu = unsafe { NSMenu::initWithTitle(mtm.alloc(), ns_string!("Help")) }; + app_menu.addItem(&about_item); + app_menu.addItem(&sep_first); + app_menu.addItem(&services_item); + // app_menu.addItem(&open_config); + app_menu.addItem(&hide_item); + app_menu.addItem(&hide_others_item); + app_menu.addItem(&show_all_item); + app_menu.addItem(&sep); + app_menu.addItem(&quit_item); + app_menu_item.setSubmenu(Some(&app_menu)); + view_menu_item.setSubmenu(Some(&view_menu)); + window_menu_item.setSubmenu(Some(&window_menu)); + help_menu_item.setSubmenu(Some(&help_menu)); + + unsafe { + app.setServicesMenu(Some(&services_menu)); + app.setWindowsMenu(Some(&window_menu)); + app.setHelpMenu(Some(&help_menu)); + }; + app.setMainMenu(Some(&menubar)); +} + +fn menu_item( + mtm: MainThreadMarker, + title: &NSString, + selector: Option, + key_equivalent: Option>, +) -> Retained { + let (key, masks) = match key_equivalent { + Some(ke) => (ke.key, ke.masks), + None => (ns_string!(""), None), + }; + let item = unsafe { + NSMenuItem::initWithTitle_action_keyEquivalent(mtm.alloc(), title, selector, key) + }; + if let Some(masks) = masks { + item.setKeyEquivalentModifierMask(masks) + } + + item +} diff --git a/rio-window/src/platform_impl/macos/mod.rs b/rio-window/src/platform_impl/macos/mod.rs new file mode 100644 index 00000000..fe331025 --- /dev/null +++ b/rio-window/src/platform_impl/macos/mod.rs @@ -0,0 +1,63 @@ +#[macro_use] +mod util; + +mod app; +mod app_delegate; +mod cursor; +mod event; +mod event_handler; +mod event_loop; +mod ffi; +mod menu; +mod monitor; +mod observer; +mod view; +mod window; +mod window_delegate; + +use std::fmt; + +pub(crate) use self::event::{ + physicalkey_to_scancode, scancode_to_physicalkey, KeyEventExtra, +}; +pub(crate) use self::event_loop::{ + ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle, + PlatformSpecificEventLoopAttributes, +}; +pub(crate) use self::monitor::{MonitorHandle, VideoModeHandle}; +pub(crate) use self::window::WindowId; +pub(crate) use self::window_delegate::PlatformSpecificWindowAttributes; +use crate::event::DeviceId as RootDeviceId; + +pub(crate) use self::cursor::CustomCursor as PlatformCustomCursor; +pub(crate) use self::window::Window; +pub(crate) use crate::cursor::OnlyCursorImageSource as PlatformCustomCursorSource; +pub(crate) use crate::icon::NoIcon as PlatformIcon; +pub(crate) use crate::platform_impl::Fullscreen; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId; + +impl DeviceId { + pub const unsafe fn dummy() -> Self { + DeviceId + } +} + +// Constant device ID; to be removed when if backend is updated to report real device IDs. +pub(crate) const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId); + +#[derive(Debug)] +pub enum OsError { + CGError(core_graphics::base::CGError), + CreationError(&'static str), +} + +impl fmt::Display for OsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + OsError::CGError(e) => f.pad(&format!("CGError {e}")), + OsError::CreationError(e) => f.pad(e), + } + } +} diff --git a/rio-window/src/platform_impl/macos/monitor.rs b/rio-window/src/platform_impl/macos/monitor.rs new file mode 100644 index 00000000..0ed04f55 --- /dev/null +++ b/rio-window/src/platform_impl/macos/monitor.rs @@ -0,0 +1,365 @@ +#![allow(clippy::unnecessary_cast)] + +use std::collections::VecDeque; +use std::fmt; + +use core_foundation::array::{CFArrayGetCount, CFArrayGetValueAtIndex}; +use core_foundation::base::{CFRelease, TCFType}; +use core_foundation::string::CFString; +use core_graphics::display::{ + CGDirectDisplayID, CGDisplay, CGDisplayBounds, CGDisplayCopyDisplayMode, +}; +use objc2::rc::Retained; +use objc2::runtime::AnyObject; +use objc2_app_kit::NSScreen; +use objc2_foundation::{ + ns_string, run_on_main, MainThreadMarker, NSNumber, NSPoint, NSRect, +}; + +use super::ffi; +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; + +#[derive(Clone)] +pub struct VideoModeHandle { + size: PhysicalSize, + bit_depth: u16, + refresh_rate_millihertz: u32, + pub(crate) monitor: MonitorHandle, + pub(crate) native_mode: NativeDisplayMode, +} + +impl PartialEq for VideoModeHandle { + fn eq(&self, other: &Self) -> bool { + self.size == other.size + && self.bit_depth == other.bit_depth + && self.refresh_rate_millihertz == other.refresh_rate_millihertz + && self.monitor == other.monitor + } +} + +impl Eq for VideoModeHandle {} + +impl std::hash::Hash for VideoModeHandle { + fn hash(&self, state: &mut H) { + self.size.hash(state); + self.bit_depth.hash(state); + self.refresh_rate_millihertz.hash(state); + self.monitor.hash(state); + } +} + +impl std::fmt::Debug for VideoModeHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VideoModeHandle") + .field("size", &self.size) + .field("bit_depth", &self.bit_depth) + .field("refresh_rate_millihertz", &self.refresh_rate_millihertz) + .field("monitor", &self.monitor) + .finish() + } +} + +pub struct NativeDisplayMode(pub ffi::CGDisplayModeRef); + +unsafe impl Send for NativeDisplayMode {} +unsafe impl Sync for NativeDisplayMode {} + +impl Drop for NativeDisplayMode { + fn drop(&mut self) { + unsafe { + ffi::CGDisplayModeRelease(self.0); + } + } +} + +impl Clone for NativeDisplayMode { + fn clone(&self) -> Self { + unsafe { + ffi::CGDisplayModeRetain(self.0); + } + NativeDisplayMode(self.0) + } +} + +impl VideoModeHandle { + pub fn size(&self) -> PhysicalSize { + self.size + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } +} + +#[derive(Clone)] +pub struct MonitorHandle(CGDirectDisplayID); + +// `CGDirectDisplayID` changes on video mode change, so we cannot rely on that +// for comparisons, but we can use `CGDisplayCreateUUIDFromDisplayID` to get an +// unique identifier that persists even across system reboots +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + unsafe { + ffi::CGDisplayCreateUUIDFromDisplayID(self.0) + == ffi::CGDisplayCreateUUIDFromDisplayID(other.0) + } + } +} + +impl Eq for MonitorHandle {} + +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MonitorHandle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + unsafe { + ffi::CGDisplayCreateUUIDFromDisplayID(self.0) + .cmp(&ffi::CGDisplayCreateUUIDFromDisplayID(other.0)) + } + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + unsafe { + ffi::CGDisplayCreateUUIDFromDisplayID(self.0).hash(state); + } + } +} + +pub fn available_monitors() -> VecDeque { + if let Ok(displays) = CGDisplay::active_displays() { + let mut monitors = VecDeque::with_capacity(displays.len()); + for display in displays { + monitors.push_back(MonitorHandle(display)); + } + monitors + } else { + VecDeque::with_capacity(0) + } +} + +pub fn primary_monitor() -> MonitorHandle { + MonitorHandle(CGDisplay::main().id) +} + +impl fmt::Debug for MonitorHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MonitorHandle") + .field("name", &self.name()) + .field("native_identifier", &self.native_identifier()) + .field("size", &self.size()) + .field("position", &self.position()) + .field("scale_factor", &self.scale_factor()) + .field("refresh_rate_millihertz", &self.refresh_rate_millihertz()) + .finish_non_exhaustive() + } +} + +impl MonitorHandle { + pub fn new(id: CGDirectDisplayID) -> Self { + MonitorHandle(id) + } + + // TODO: Be smarter about this: + // + pub fn name(&self) -> Option { + let MonitorHandle(display_id) = *self; + let screen_num = CGDisplay::new(display_id).model_number(); + Some(format!("Monitor #{screen_num}")) + } + + #[inline] + pub fn native_identifier(&self) -> u32 { + self.0 + } + + pub fn size(&self) -> PhysicalSize { + let MonitorHandle(display_id) = *self; + let display = CGDisplay::new(display_id); + let height = display.pixels_high(); + let width = display.pixels_wide(); + PhysicalSize::from_logical::<_, f64>( + (width as f64, height as f64), + self.scale_factor(), + ) + } + + #[inline] + pub fn position(&self) -> PhysicalPosition { + // This is already in screen coordinates. If we were using `NSScreen`, + // then a conversion would've been needed: + // flip_window_screen_coordinates(self.ns_screen(mtm)?.frame()) + let bounds = unsafe { CGDisplayBounds(self.native_identifier()) }; + let position = LogicalPosition::new(bounds.origin.x, bounds.origin.y); + position.to_physical(self.scale_factor()) + } + + pub fn scale_factor(&self) -> f64 { + run_on_main(|mtm| { + match self.ns_screen(mtm) { + Some(screen) => screen.backingScaleFactor() as f64, + None => 1.0, // default to 1.0 when we can't find the screen + } + }) + } + + pub fn refresh_rate_millihertz(&self) -> Option { + unsafe { + let current_display_mode = + NativeDisplayMode(CGDisplayCopyDisplayMode(self.0) as _); + let refresh_rate = ffi::CGDisplayModeGetRefreshRate(current_display_mode.0); + if refresh_rate > 0.0 { + return Some((refresh_rate * 1000.0).round() as u32); + } + + let mut display_link = std::ptr::null_mut(); + if ffi::CVDisplayLinkCreateWithCGDisplay(self.0, &mut display_link) + != ffi::kCVReturnSuccess + { + return None; + } + let time = ffi::CVDisplayLinkGetNominalOutputVideoRefreshPeriod(display_link); + ffi::CVDisplayLinkRelease(display_link); + + // This value is indefinite if an invalid display link was specified + if time.flags & ffi::kCVTimeIsIndefinite != 0 { + return None; + } + + (time.time_scale as i64) + .checked_div(time.time_value) + .map(|v| (v * 1000) as u32) + } + } + + pub fn video_modes(&self) -> impl Iterator { + let refresh_rate_millihertz = self.refresh_rate_millihertz().unwrap_or(0); + let monitor = self.clone(); + + unsafe { + let modes = { + let array = ffi::CGDisplayCopyAllDisplayModes(self.0, std::ptr::null()); + assert!(!array.is_null(), "failed to get list of display modes"); + let array_count = CFArrayGetCount(array); + let modes: Vec<_> = (0..array_count) + .map(move |i| { + let mode = CFArrayGetValueAtIndex(array, i) as *mut _; + ffi::CGDisplayModeRetain(mode); + mode + }) + .collect(); + CFRelease(array as *const _); + modes + }; + + modes.into_iter().map(move |mode| { + let cg_refresh_rate_hertz = + ffi::CGDisplayModeGetRefreshRate(mode).round() as i64; + + // CGDisplayModeGetRefreshRate returns 0.0 for any display that + // isn't a CRT + let refresh_rate_millihertz = if cg_refresh_rate_hertz > 0 { + (cg_refresh_rate_hertz * 1000) as u32 + } else { + refresh_rate_millihertz + }; + + let pixel_encoding = CFString::wrap_under_create_rule( + ffi::CGDisplayModeCopyPixelEncoding(mode), + ) + .to_string(); + let bit_depth = if pixel_encoding + .eq_ignore_ascii_case(ffi::IO32BitDirectPixels) + { + 32 + } else if pixel_encoding.eq_ignore_ascii_case(ffi::IO16BitDirectPixels) { + 16 + } else if pixel_encoding.eq_ignore_ascii_case(ffi::kIO30BitDirectPixels) { + 30 + } else { + unimplemented!() + }; + + VideoModeHandle { + size: PhysicalSize::new( + ffi::CGDisplayModeGetPixelWidth(mode) as u32, + ffi::CGDisplayModeGetPixelHeight(mode) as u32, + ), + refresh_rate_millihertz, + bit_depth, + monitor: monitor.clone(), + native_mode: NativeDisplayMode(mode), + } + }) + } + } + + pub(crate) fn ns_screen(&self, mtm: MainThreadMarker) -> Option> { + let uuid = unsafe { ffi::CGDisplayCreateUUIDFromDisplayID(self.0) }; + NSScreen::screens(mtm).into_iter().find(|screen| { + let other_native_id = get_display_id(screen); + let other_uuid = unsafe { + ffi::CGDisplayCreateUUIDFromDisplayID( + other_native_id as CGDirectDisplayID, + ) + }; + uuid == other_uuid + }) + } +} + +pub(crate) fn get_display_id(screen: &NSScreen) -> u32 { + let key = ns_string!("NSScreenNumber"); + + objc2::rc::autoreleasepool(|_| { + let device_description = screen.deviceDescription(); + + // Retrieve the CGDirectDisplayID associated with this screen + // + // SAFETY: The value from @"NSScreenNumber" in deviceDescription is guaranteed + // to be an NSNumber. See documentation for `deviceDescription` for details: + // + let obj = device_description + .get(key) + .expect("failed getting screen display id from device description"); + let obj: *const AnyObject = obj; + let obj: *const NSNumber = obj.cast(); + let obj: &NSNumber = unsafe { &*obj }; + + obj.as_u32() + }) +} + +/// Core graphics screen coordinates are relative to the top-left corner of +/// the so-called "main" display, with y increasing downwards - which is +/// exactly what we want in Winit. +/// +/// However, `NSWindow` and `NSScreen` changes these coordinates to: +/// 1. Be relative to the bottom-left corner of the "main" screen. +/// 2. Be relative to the bottom-left corner of the window/screen itself. +/// 3. Have y increasing upwards. +/// +/// This conversion happens to be symmetric, so we only need this one function +/// to convert between the two coordinate systems. +pub(crate) fn flip_window_screen_coordinates(frame: NSRect) -> NSPoint { + // It is intentional that we use `CGMainDisplayID` (as opposed to + // `NSScreen::mainScreen`), because that's what the screen coordinates + // are relative to, no matter which display the window is currently on. + let main_screen_height = CGDisplay::main().bounds().size.height; + + let y = main_screen_height - frame.size.height - frame.origin.y; + NSPoint::new(frame.origin.x, y) +} diff --git a/rio-window/src/platform_impl/macos/observer.rs b/rio-window/src/platform_impl/macos/observer.rs new file mode 100644 index 00000000..7a3e82bc --- /dev/null +++ b/rio-window/src/platform_impl/macos/observer.rs @@ -0,0 +1,235 @@ +use std::ffi::c_void; +use std::panic::{AssertUnwindSafe, UnwindSafe}; +use std::ptr; +use std::rc::Weak; +use std::time::Instant; + +use core_foundation::base::{CFIndex, CFOptionFlags, CFRelease}; +use core_foundation::date::CFAbsoluteTimeGetCurrent; +use core_foundation::runloop::{ + kCFRunLoopAfterWaiting, kCFRunLoopBeforeWaiting, kCFRunLoopCommonModes, + kCFRunLoopExit, CFRunLoopActivity, CFRunLoopAddObserver, CFRunLoopAddTimer, + CFRunLoopGetMain, CFRunLoopObserverCallBack, CFRunLoopObserverContext, + CFRunLoopObserverCreate, CFRunLoopObserverRef, CFRunLoopRef, CFRunLoopTimerCreate, + CFRunLoopTimerInvalidate, CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, + CFRunLoopWakeUp, +}; +use objc2_foundation::MainThreadMarker; + +use super::app_delegate::ApplicationDelegate; +use super::event_loop::{stop_app_on_panic, PanicInfo}; +use super::ffi; + +unsafe fn control_flow_handler(panic_info: *mut c_void, f: F) +where + F: FnOnce(Weak) + UnwindSafe, +{ + let info_from_raw = unsafe { Weak::from_raw(panic_info as *mut PanicInfo) }; + // Asserting unwind safety on this type should be fine because `PanicInfo` is + // `RefUnwindSafe` and `Rc` is `UnwindSafe` if `T` is `RefUnwindSafe`. + let panic_info = AssertUnwindSafe(Weak::clone(&info_from_raw)); + // `from_raw` takes ownership of the data behind the pointer. + // But if this scope takes ownership of the weak pointer, then + // the weak pointer will get free'd at the end of the scope. + // However we want to keep that weak reference around after the function. + std::mem::forget(info_from_raw); + + let mtm = MainThreadMarker::new().unwrap(); + stop_app_on_panic(mtm, Weak::clone(&panic_info), move || { + let _ = &panic_info; + f(panic_info.0) + }); +} + +// begin is queued with the highest priority to ensure it is processed before other observers +extern "C" fn control_flow_begin_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + panic_info: *mut c_void, +) { + unsafe { + control_flow_handler(panic_info, |panic_info| { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopAfterWaiting => { + // trace!("Triggered `CFRunLoopAfterWaiting`"); + ApplicationDelegate::get(MainThreadMarker::new().unwrap()) + .wakeup(panic_info); + // trace!("Completed `CFRunLoopAfterWaiting`"); + } + _ => unreachable!(), + } + }); + } +} + +// end is queued with the lowest priority to ensure it is processed after other observers +// without that, LoopExiting would get sent after AboutToWait +extern "C" fn control_flow_end_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + panic_info: *mut c_void, +) { + unsafe { + control_flow_handler(panic_info, |panic_info| { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => { + // trace!("Triggered `CFRunLoopBeforeWaiting`"); + ApplicationDelegate::get(MainThreadMarker::new().unwrap()) + .cleared(panic_info); + // trace!("Completed `CFRunLoopBeforeWaiting`"); + } + kCFRunLoopExit => (), // unimplemented!(), // not expected to ever happen + _ => unreachable!(), + } + }); + } +} + +pub struct RunLoop(CFRunLoopRef); + +impl RunLoop { + pub unsafe fn get() -> Self { + RunLoop(unsafe { CFRunLoopGetMain() }) + } + + pub fn wakeup(&self) { + unsafe { CFRunLoopWakeUp(self.0) } + } + + unsafe fn add_observer( + &self, + flags: CFOptionFlags, + priority: CFIndex, + handler: CFRunLoopObserverCallBack, + context: *mut CFRunLoopObserverContext, + ) { + let observer = unsafe { + CFRunLoopObserverCreate( + ptr::null_mut(), + flags, + ffi::TRUE, // Indicates we want this to run repeatedly + priority, // The lower the value, the sooner this will run + handler, + context, + ) + }; + unsafe { CFRunLoopAddObserver(self.0, observer, kCFRunLoopCommonModes) }; + } +} + +pub fn setup_control_flow_observers(panic_info: Weak) { + unsafe { + let mut context = CFRunLoopObserverContext { + info: Weak::into_raw(panic_info) as *mut _, + version: 0, + retain: None, + release: None, + copyDescription: None, + }; + let run_loop = RunLoop::get(); + run_loop.add_observer( + kCFRunLoopAfterWaiting, + CFIndex::MIN, + control_flow_begin_handler, + &mut context as *mut _, + ); + run_loop.add_observer( + kCFRunLoopExit | kCFRunLoopBeforeWaiting, + CFIndex::MAX, + control_flow_end_handler, + &mut context as *mut _, + ); + } +} + +#[derive(Debug)] +pub struct EventLoopWaker { + timer: CFRunLoopTimerRef, + + /// An arbitrary instant in the past, that will trigger an immediate wake + /// We save this as the `next_fire_date` for consistency so we can + /// easily check if the next_fire_date needs updating. + start_instant: Instant, + + /// This is what the `NextFireDate` has been set to. + /// `None` corresponds to `waker.stop()` and `start_instant` is used + /// for `waker.start()` + next_fire_date: Option, +} + +impl Drop for EventLoopWaker { + fn drop(&mut self) { + unsafe { + CFRunLoopTimerInvalidate(self.timer); + CFRelease(self.timer as _); + } + } +} + +impl Default for EventLoopWaker { + fn default() -> EventLoopWaker { + extern "C" fn wakeup_main_loop(_timer: CFRunLoopTimerRef, _info: *mut c_void) {} + unsafe { + // Create a timer with a 0.1µs interval (1ns does not work) to mimic polling. + // It is initially setup with a first fire time really far into the + // future, but that gets changed to fire immediately in did_finish_launching + let timer = CFRunLoopTimerCreate( + ptr::null_mut(), + f64::MAX, + 0.000_000_1, + 0, + 0, + wakeup_main_loop, + ptr::null_mut(), + ); + CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes); + EventLoopWaker { + timer, + start_instant: Instant::now(), + next_fire_date: None, + } + } + } +} + +impl EventLoopWaker { + pub fn stop(&mut self) { + if self.next_fire_date.is_some() { + self.next_fire_date = None; + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MAX) } + } + } + + pub fn start(&mut self) { + if self.next_fire_date != Some(self.start_instant) { + self.next_fire_date = Some(self.start_instant); + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MIN) } + } + } + + pub fn start_at(&mut self, instant: Option) { + let now = Instant::now(); + match instant { + Some(instant) if now >= instant => { + self.start(); + } + Some(instant) => { + if self.next_fire_date != Some(instant) { + self.next_fire_date = Some(instant); + unsafe { + let current = CFAbsoluteTimeGetCurrent(); + let duration = instant - now; + let fsecs = duration.subsec_nanos() as f64 / 1_000_000_000.0 + + duration.as_secs() as f64; + CFRunLoopTimerSetNextFireDate(self.timer, current + fsecs) + } + } + } + None => { + self.stop(); + } + } + } +} diff --git a/rio-window/src/platform_impl/macos/util.rs b/rio-window/src/platform_impl/macos/util.rs new file mode 100644 index 00000000..fa692da2 --- /dev/null +++ b/rio-window/src/platform_impl/macos/util.rs @@ -0,0 +1,35 @@ +use tracing::trace; + +macro_rules! trace_scope { + ($s:literal) => { + let _crate = + $crate::platform_impl::platform::util::TraceGuard::new(module_path!(), $s); + }; +} + +pub(crate) struct TraceGuard { + module_path: &'static str, + called_from_fn: &'static str, +} + +impl TraceGuard { + #[inline] + pub(crate) fn new(module_path: &'static str, called_from_fn: &'static str) -> Self { + trace!(target = module_path, "Triggered `{}`", called_from_fn); + Self { + module_path, + called_from_fn, + } + } +} + +impl Drop for TraceGuard { + #[inline] + fn drop(&mut self) { + trace!( + target = self.module_path, + "Completed `{}`", + self.called_from_fn + ); + } +} diff --git a/rio-window/src/platform_impl/macos/view.rs b/rio-window/src/platform_impl/macos/view.rs new file mode 100644 index 00000000..b903fb3b --- /dev/null +++ b/rio-window/src/platform_impl/macos/view.rs @@ -0,0 +1,1139 @@ +#![allow(clippy::unnecessary_cast)] +use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, VecDeque}; +use std::ptr; + +use objc2::rc::{Retained, WeakId}; +use objc2::runtime::{AnyObject, Sel}; +use objc2::{declare_class, msg_send_id, mutability, sel, ClassType, DeclaredClass}; +use objc2_app_kit::{ + NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, + NSTrackingRectTag, NSView, NSViewFrameDidChangeNotification, +}; +use objc2_foundation::{ + MainThreadMarker, NSArray, NSAttributedString, NSAttributedStringKey, NSCopying, + NSMutableAttributedString, NSNotFound, NSNotificationCenter, NSObject, + NSObjectProtocol, NSPoint, NSRange, NSRect, NSSize, NSString, NSUInteger, +}; + +use super::app_delegate::ApplicationDelegate; +use super::cursor::{default_cursor, invisible_cursor}; +use super::event::{ + code_to_key, code_to_location, create_key_event, event_mods, lalt_pressed, + ralt_pressed, scancode_to_physicalkey, +}; +use super::window::WinitWindow; +use super::DEVICE_ID; +use crate::dpi::{LogicalPosition, LogicalSize}; +use crate::event::{ + DeviceEvent, ElementState, Ime, Modifiers, MouseButton, MouseScrollDelta, TouchPhase, + WindowEvent, +}; +use crate::keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NamedKey}; +use crate::platform::macos::OptionAsAlt; + +#[derive(Debug)] +struct CursorState { + visible: bool, + cursor: Retained, +} + +impl Default for CursorState { + fn default() -> Self { + Self { + visible: true, + cursor: default_cursor(), + } + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy, Default)] +enum ImeState { + #[default] + /// The IME events are disabled, so only `ReceivedCharacter` is being sent to the user. + Disabled, + + /// The ground state of enabled IME input. It means that both Preedit and regular keyboard + /// input could be start from it. + Ground, + + /// The IME is in preedit. + Preedit, + + /// The text was just committed, so the next input from the keyboard must be ignored. + Committed, +} + +bitflags::bitflags! { + #[derive(Debug, Clone, Copy, PartialEq)] + struct ModLocationMask: u8 { + const LEFT = 0b0001; + const RIGHT = 0b0010; + } +} +impl ModLocationMask { + fn from_location(loc: KeyLocation) -> ModLocationMask { + match loc { + KeyLocation::Left => ModLocationMask::LEFT, + KeyLocation::Right => ModLocationMask::RIGHT, + _ => unreachable!(), + } + } +} + +fn key_to_modifier(key: &Key) -> Option { + match key { + Key::Named(NamedKey::Alt) => Some(ModifiersState::ALT), + Key::Named(NamedKey::Control) => Some(ModifiersState::CONTROL), + Key::Named(NamedKey::Super) => Some(ModifiersState::SUPER), + Key::Named(NamedKey::Shift) => Some(ModifiersState::SHIFT), + _ => None, + } +} + +fn get_right_modifier_code(key: &Key) -> KeyCode { + match key { + Key::Named(NamedKey::Alt) => KeyCode::AltRight, + Key::Named(NamedKey::Control) => KeyCode::ControlRight, + Key::Named(NamedKey::Shift) => KeyCode::ShiftRight, + Key::Named(NamedKey::Super) => KeyCode::SuperRight, + _ => unreachable!(), + } +} + +fn get_left_modifier_code(key: &Key) -> KeyCode { + match key { + Key::Named(NamedKey::Alt) => KeyCode::AltLeft, + Key::Named(NamedKey::Control) => KeyCode::ControlLeft, + Key::Named(NamedKey::Shift) => KeyCode::ShiftLeft, + Key::Named(NamedKey::Super) => KeyCode::SuperLeft, + _ => unreachable!(), + } +} + +#[derive(Debug)] +pub struct ViewState { + /// Strong reference to the global application state. + app_delegate: Retained, + + cursor_state: RefCell, + ime_position: Cell, + ime_size: Cell, + modifiers: Cell, + phys_modifiers: RefCell>, + tracking_rect: Cell>, + ime_state: Cell, + input_source: RefCell, + + /// True iff the application wants IME events. + /// + /// Can be set using `set_ime_allowed` + ime_allowed: Cell, + + /// True if the current key event should be forwarded + /// to the application, even during IME + forward_key_to_app: Cell, + + marked_text: RefCell>, + accepts_first_mouse: bool, + + // Weak reference because the window keeps a strong reference to the view + _ns_window: WeakId, + + /// The state of the `Option` as `Alt`. + option_as_alt: Cell, +} + +declare_class!( + pub(super) struct WinitView; + + unsafe impl ClassType for WinitView { + #[inherits(NSResponder, NSObject)] + type Super = NSView; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitView"; + } + + impl DeclaredClass for WinitView { + type Ivars = ViewState; + } + + unsafe impl WinitView { + #[method(isFlipped)] + fn is_flipped(&self) -> bool { + // `winit` uses the upper-left corner as the origin. + true + } + + #[method(viewDidMoveToWindow)] + fn view_did_move_to_window(&self) { + trace_scope!("viewDidMoveToWindow"); + if let Some(tracking_rect) = self.ivars().tracking_rect.take() { + self.removeTrackingRect(tracking_rect); + } + + let rect = self.frame(); + let tracking_rect = unsafe { + self.addTrackingRect_owner_userData_assumeInside(rect, self, ptr::null_mut(), false) + }; + assert_ne!(tracking_rect, 0, "failed adding tracking rect"); + self.ivars().tracking_rect.set(Some(tracking_rect)); + } + + #[method(frameDidChange:)] + fn frame_did_change(&self, _event: &NSEvent) { + trace_scope!("frameDidChange:"); + if let Some(tracking_rect) = self.ivars().tracking_rect.take() { + self.removeTrackingRect(tracking_rect); + } + + let rect = self.frame(); + let tracking_rect = unsafe { + self.addTrackingRect_owner_userData_assumeInside(rect, self, ptr::null_mut(), false) + }; + assert_ne!(tracking_rect, 0, "failed adding tracking rect"); + self.ivars().tracking_rect.set(Some(tracking_rect)); + + // Emit resize event here rather than from windowDidResize because: + // 1. When a new window is created as a tab, the frame size may change without a window resize occurring. + // 2. Even when a window resize does occur on a new tabbed window, it contains the wrong size (includes tab height). + let logical_size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64); + let size = logical_size.to_physical::(self.scale_factor()); + self.queue_event(WindowEvent::Resized(size)); + } + + #[method(drawRect:)] + fn draw_rect(&self, _rect: NSRect) { + trace_scope!("drawRect:"); + + // It's a workaround for https://github.com/rust-windowing/winit/issues/2640, don't replace with `self.window_id()`. + if let Some(window) = self.ivars()._ns_window.load() { + self.ivars().app_delegate.handle_redraw(window.id()); + } + + // This is a direct subclass of NSView, no need to call superclass' drawRect: + } + + #[method(acceptsFirstResponder)] + fn accepts_first_responder(&self) -> bool { + trace_scope!("acceptsFirstResponder"); + true + } + + // This is necessary to prevent a beefy terminal error on MacBook Pros: + // IMKInputSession [0x7fc573576ff0 presentFunctionRowItemTextInputViewWithEndpoint:completionHandler:] : [self textInputContext]=0x7fc573558e10 *NO* NSRemoteViewController to client, NSError=Error Domain=NSCocoaErrorDomain Code=4099 "The connection from pid 0 was invalidated from this process." UserInfo={NSDebugDescription=The connection from pid 0 was invalidated from this process.}, com.apple.inputmethod.EmojiFunctionRowItem + // TODO: Add an API extension for using `NSTouchBar` + #[method_id(touchBar)] + fn touch_bar(&self) -> Option> { + trace_scope!("touchBar"); + None + } + + #[method(resetCursorRects)] + fn reset_cursor_rects(&self) { + trace_scope!("resetCursorRects"); + let bounds = self.bounds(); + let cursor_state = self.ivars().cursor_state.borrow(); + // We correctly invoke `addCursorRect` only from inside `resetCursorRects` + if cursor_state.visible { + self.addCursorRect_cursor(bounds, &cursor_state.cursor); + } else { + self.addCursorRect_cursor(bounds, &invisible_cursor()); + } + } + } + + unsafe impl NSTextInputClient for WinitView { + #[method(hasMarkedText)] + fn has_marked_text(&self) -> bool { + trace_scope!("hasMarkedText"); + self.ivars().marked_text.borrow().length() > 0 + } + + #[method(markedRange)] + fn marked_range(&self) -> NSRange { + trace_scope!("markedRange"); + let length = self.ivars().marked_text.borrow().length(); + if length > 0 { + NSRange::new(0, length) + } else { + // Documented to return `{NSNotFound, 0}` if there is no marked range. + NSRange::new(NSNotFound as NSUInteger, 0) + } + } + + #[method(selectedRange)] + fn selected_range(&self) -> NSRange { + trace_scope!("selectedRange"); + // Documented to return `{NSNotFound, 0}` if there is no selection. + NSRange::new(NSNotFound as NSUInteger, 0) + } + + #[method(setMarkedText:selectedRange:replacementRange:)] + fn set_marked_text( + &self, + string: &NSObject, + selected_range: NSRange, + _replacement_range: NSRange, + ) { + trace_scope!("setMarkedText:selectedRange:replacementRange:"); + + // SAFETY: This method is guaranteed to get either a `NSString` or a `NSAttributedString`. + let (marked_text, string) = if string.is_kind_of::() { + let string: *const NSObject = string; + let string: *const NSAttributedString = string.cast(); + let string = unsafe { &*string }; + ( + NSMutableAttributedString::from_attributed_nsstring(string), + string.string(), + ) + } else { + let string: *const NSObject = string; + let string: *const NSString = string.cast(); + let string = unsafe { &*string }; + ( + NSMutableAttributedString::from_nsstring(string), + string.copy(), + ) + }; + + // Update marked text. + *self.ivars().marked_text.borrow_mut() = marked_text; + + // Notify IME is active if application still doesn't know it. + if self.ivars().ime_state.get() == ImeState::Disabled { + *self.ivars().input_source.borrow_mut() = self.current_input_source(); + self.queue_event(WindowEvent::Ime(Ime::Enabled)); + } + + if unsafe { self.hasMarkedText() } { + self.ivars().ime_state.set(ImeState::Preedit); + } else { + // In case the preedit was cleared, set IME into the Ground state. + self.ivars().ime_state.set(ImeState::Ground); + } + + let cursor_range = if string.is_empty() { + // An empty string basically means that there's no preedit, so indicate that by + // sending a `None` cursor range. + None + } else { + // Convert the selected range from UTF-16 indices to UTF-8 indices. + let sub_string_a = unsafe { string.substringToIndex(selected_range.location) }; + let sub_string_b = unsafe { string.substringToIndex(selected_range.end()) }; + let lowerbound_utf8 = sub_string_a.len(); + let upperbound_utf8 = sub_string_b.len(); + Some((lowerbound_utf8, upperbound_utf8)) + }; + + // Send WindowEvent for updating marked text + self.queue_event(WindowEvent::Ime(Ime::Preedit(string.to_string(), cursor_range))); + } + + #[method(unmarkText)] + fn unmark_text(&self) { + trace_scope!("unmarkText"); + *self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new(); + + let input_context = self.inputContext().expect("input context"); + input_context.discardMarkedText(); + + self.queue_event(WindowEvent::Ime(Ime::Preedit(String::new(), None))); + if self.is_ime_enabled() { + // Leave the Preedit self.ivars() + self.ivars().ime_state.set(ImeState::Ground); + } else { + tracing::warn!("Expected to have IME enabled when receiving unmarkText"); + } + } + + #[method_id(validAttributesForMarkedText)] + fn valid_attributes_for_marked_text(&self) -> Retained> { + trace_scope!("validAttributesForMarkedText"); + NSArray::new() + } + + #[method_id(attributedSubstringForProposedRange:actualRange:)] + fn attributed_substring_for_proposed_range( + &self, + _range: NSRange, + _actual_range: *mut NSRange, + ) -> Option> { + trace_scope!("attributedSubstringForProposedRange:actualRange:"); + None + } + + #[method(characterIndexForPoint:)] + fn character_index_for_point(&self, _point: NSPoint) -> NSUInteger { + trace_scope!("characterIndexForPoint:"); + 0 + } + + #[method(firstRectForCharacterRange:actualRange:)] + fn first_rect_for_character_range( + &self, + _range: NSRange, + _actual_range: *mut NSRange, + ) -> NSRect { + trace_scope!("firstRectForCharacterRange:actualRange:"); + let rect = NSRect::new( + self.ivars().ime_position.get(), + self.ivars().ime_size.get() + ); + // Return value is expected to be in screen coordinates, so we need a conversion here + self.window() + .convertRectToScreen(self.convertRect_toView(rect, None)) + } + + #[method(insertText:replacementRange:)] + fn insert_text(&self, string: &NSObject, _replacement_range: NSRange) { + trace_scope!("insertText:replacementRange:"); + + // SAFETY: This method is guaranteed to get either a `NSString` or a `NSAttributedString`. + let string = if string.is_kind_of::() { + let string: *const NSObject = string; + let string: *const NSAttributedString = string.cast(); + unsafe { &*string }.string().to_string() + } else { + let string: *const NSObject = string; + let string: *const NSString = string.cast(); + unsafe { &*string }.to_string() + }; + + let is_control = string.chars().next().map_or(false, |c| c.is_control()); + + // Commit only if we have marked text. + if unsafe { self.hasMarkedText() } && self.is_ime_enabled() && !is_control { + self.queue_event(WindowEvent::Ime(Ime::Preedit(String::new(), None))); + self.queue_event(WindowEvent::Ime(Ime::Commit(string))); + self.ivars().ime_state.set(ImeState::Committed); + } + } + + // Basically, we're sent this message whenever a keyboard event that doesn't generate a "human + // readable" character happens, i.e. newlines, tabs, and Ctrl+C. + #[method(doCommandBySelector:)] + fn do_command_by_selector(&self, _command: Sel) { + trace_scope!("doCommandBySelector:"); + // We shouldn't forward any character from just committed text, since we'll end up sending + // it twice with some IMEs like Korean one. We'll also always send `Enter` in that case, + // which is not desired given it was used to confirm IME input. + if self.ivars().ime_state.get() == ImeState::Committed { + return; + } + + self.ivars().forward_key_to_app.set(true); + + if unsafe { self.hasMarkedText() } && self.ivars().ime_state.get() == ImeState::Preedit + { + // Leave preedit so that we also report the key-up for this key. + self.ivars().ime_state.set(ImeState::Ground); + } + } + } + + unsafe impl WinitView { + #[method(keyDown:)] + fn key_down(&self, event: &NSEvent) { + trace_scope!("keyDown:"); + { + let mut prev_input_source = self.ivars().input_source.borrow_mut(); + let current_input_source = self.current_input_source(); + if *prev_input_source != current_input_source && self.is_ime_enabled() { + *prev_input_source = current_input_source; + drop(prev_input_source); + self.ivars().ime_state.set(ImeState::Disabled); + self.queue_event(WindowEvent::Ime(Ime::Disabled)); + } + } + + // Get the characters from the event. + let old_ime_state = self.ivars().ime_state.get(); + self.ivars().forward_key_to_app.set(false); + let event = replace_event(event, self.option_as_alt()); + + // The `interpretKeyEvents` function might call + // `setMarkedText`, `insertText`, and `doCommandBySelector`. + // It's important that we call this before queuing the KeyboardInput, because + // we must send the `KeyboardInput` event during IME if it triggered + // `doCommandBySelector`. (doCommandBySelector means that the keyboard input + // is not handled by IME and should be handled by the application) + if self.ivars().ime_allowed.get() { + let events_for_nsview = NSArray::from_slice(&[&*event]); + unsafe { self.interpretKeyEvents(&events_for_nsview) }; + + // If the text was committed we must treat the next keyboard event as IME related. + if self.ivars().ime_state.get() == ImeState::Committed { + // Remove any marked text, so normal input can continue. + *self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new(); + } + } + + self.update_modifiers(&event, false); + + let had_ime_input = match self.ivars().ime_state.get() { + ImeState::Committed => { + // Allow normal input after the commit. + self.ivars().ime_state.set(ImeState::Ground); + true + } + ImeState::Preedit => true, + // `key_down` could result in preedit clear, so compare old and current state. + _ => old_ime_state != self.ivars().ime_state.get(), + }; + + if !had_ime_input || self.ivars().forward_key_to_app.get() { + let key_event = create_key_event(&event, true, unsafe { event.isARepeat() }, None); + self.queue_event(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event: key_event, + is_synthetic: false, + }); + } + } + + #[method(keyUp:)] + fn key_up(&self, event: &NSEvent) { + trace_scope!("keyUp:"); + + let event = replace_event(event, self.option_as_alt()); + self.update_modifiers(&event, false); + + // We want to send keyboard input when we are currently in the ground state. + if matches!( + self.ivars().ime_state.get(), + ImeState::Ground | ImeState::Disabled + ) { + self.queue_event(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event: create_key_event(&event, false, false, None), + is_synthetic: false, + }); + } + } + + #[method(flagsChanged:)] + fn flags_changed(&self, event: &NSEvent) { + trace_scope!("flagsChanged:"); + + self.update_modifiers(event, true); + } + + #[method(insertTab:)] + fn insert_tab(&self, _sender: Option<&AnyObject>) { + trace_scope!("insertTab:"); + let window = self.window(); + if let Some(first_responder) = window.firstResponder() { + if *first_responder == ***self { + window.selectNextKeyView(Some(self)) + } + } + } + + #[method(insertBackTab:)] + fn insert_back_tab(&self, _sender: Option<&AnyObject>) { + trace_scope!("insertBackTab:"); + let window = self.window(); + if let Some(first_responder) = window.firstResponder() { + if *first_responder == ***self { + window.selectPreviousKeyView(Some(self)) + } + } + } + + // Allows us to receive Cmd-. (the shortcut for closing a dialog) + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=300620#c6 + #[method(cancelOperation:)] + fn cancel_operation(&self, _sender: Option<&AnyObject>) { + let mtm = MainThreadMarker::from(self); + trace_scope!("cancelOperation:"); + + let event = NSApplication::sharedApplication(mtm) + .currentEvent() + .expect("could not find current event"); + + self.update_modifiers(&event, false); + let event = create_key_event(&event, true, unsafe { event.isARepeat() }, None); + + self.queue_event(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + } + + // In the past (?), `mouseMoved:` events were not generated when the + // user hovered over a window from a separate window, and as such the + // application might not know the location of the mouse in the event. + // + // To fix this, we emit `mouse_motion` inside of mouse click, mouse + // scroll, magnify and other gesture event handlers, to ensure that + // the application's state of where the mouse click was located is up + // to date. + // + // See https://github.com/rust-windowing/winit/pull/1490 for history. + + #[method(mouseDown:)] + fn mouse_down(&self, event: &NSEvent) { + trace_scope!("mouseDown:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Pressed); + } + + #[method(mouseUp:)] + fn mouse_up(&self, event: &NSEvent) { + trace_scope!("mouseUp:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Released); + } + + #[method(rightMouseDown:)] + fn right_mouse_down(&self, event: &NSEvent) { + trace_scope!("rightMouseDown:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Pressed); + } + + #[method(rightMouseUp:)] + fn right_mouse_up(&self, event: &NSEvent) { + trace_scope!("rightMouseUp:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Released); + } + + #[method(otherMouseDown:)] + fn other_mouse_down(&self, event: &NSEvent) { + trace_scope!("otherMouseDown:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Pressed); + } + + #[method(otherMouseUp:)] + fn other_mouse_up(&self, event: &NSEvent) { + trace_scope!("otherMouseUp:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Released); + } + + // No tracing on these because that would be overly verbose + + #[method(mouseMoved:)] + fn mouse_moved(&self, event: &NSEvent) { + self.mouse_motion(event); + } + + #[method(mouseDragged:)] + fn mouse_dragged(&self, event: &NSEvent) { + self.mouse_motion(event); + } + + #[method(rightMouseDragged:)] + fn right_mouse_dragged(&self, event: &NSEvent) { + self.mouse_motion(event); + } + + #[method(otherMouseDragged:)] + fn other_mouse_dragged(&self, event: &NSEvent) { + self.mouse_motion(event); + } + + #[method(mouseEntered:)] + fn mouse_entered(&self, _event: &NSEvent) { + trace_scope!("mouseEntered:"); + self.queue_event(WindowEvent::CursorEntered { + device_id: DEVICE_ID, + }); + } + + #[method(mouseExited:)] + fn mouse_exited(&self, _event: &NSEvent) { + trace_scope!("mouseExited:"); + + self.queue_event(WindowEvent::CursorLeft { + device_id: DEVICE_ID, + }); + } + + #[method(scrollWheel:)] + fn scroll_wheel(&self, event: &NSEvent) { + trace_scope!("scrollWheel:"); + + self.mouse_motion(event); + + let delta = { + let (x, y) = unsafe { (event.scrollingDeltaX(), event.scrollingDeltaY()) }; + if unsafe { event.hasPreciseScrollingDeltas() } { + let delta = LogicalPosition::new(x, y).to_physical(self.scale_factor()); + MouseScrollDelta::PixelDelta(delta) + } else { + MouseScrollDelta::LineDelta(x as f32, y as f32) + } + }; + + // The "momentum phase," if any, has higher priority than touch phase (the two should + // be mutually exclusive anyhow, which is why the API is rather incoherent). If no momentum + // phase is recorded (or rather, the started/ended cases of the momentum phase) then we + // report the touch phase. + #[allow(non_upper_case_globals)] + let phase = match unsafe { event.momentumPhase() } { + NSEventPhase::MayBegin | NSEventPhase::Began => TouchPhase::Started, + NSEventPhase::Ended | NSEventPhase::Cancelled => TouchPhase::Ended, + _ => match unsafe { event.phase() } { + NSEventPhase::MayBegin | NSEventPhase::Began => TouchPhase::Started, + NSEventPhase::Ended | NSEventPhase::Cancelled => TouchPhase::Ended, + _ => TouchPhase::Moved, + }, + }; + + self.update_modifiers(event, false); + + self.queue_device_event(DeviceEvent::MouseWheel { delta }); + self.queue_event(WindowEvent::MouseWheel { + device_id: DEVICE_ID, + delta, + phase, + }); + } + + #[method(magnifyWithEvent:)] + fn magnify_with_event(&self, event: &NSEvent) { + trace_scope!("magnifyWithEvent:"); + + self.mouse_motion(event); + + #[allow(non_upper_case_globals)] + let phase = match unsafe { event.phase() } { + NSEventPhase::Began => TouchPhase::Started, + NSEventPhase::Changed => TouchPhase::Moved, + NSEventPhase::Cancelled => TouchPhase::Cancelled, + NSEventPhase::Ended => TouchPhase::Ended, + _ => return, + }; + + self.queue_event(WindowEvent::PinchGesture { + device_id: DEVICE_ID, + delta: unsafe { event.magnification() }, + phase, + }); + } + + #[method(smartMagnifyWithEvent:)] + fn smart_magnify_with_event(&self, event: &NSEvent) { + trace_scope!("smartMagnifyWithEvent:"); + + self.mouse_motion(event); + + self.queue_event(WindowEvent::DoubleTapGesture { + device_id: DEVICE_ID, + }); + } + + #[method(rotateWithEvent:)] + fn rotate_with_event(&self, event: &NSEvent) { + trace_scope!("rotateWithEvent:"); + + self.mouse_motion(event); + + #[allow(non_upper_case_globals)] + let phase = match unsafe { event.phase() } { + NSEventPhase::Began => TouchPhase::Started, + NSEventPhase::Changed => TouchPhase::Moved, + NSEventPhase::Cancelled => TouchPhase::Cancelled, + NSEventPhase::Ended => TouchPhase::Ended, + _ => return, + }; + + self.queue_event(WindowEvent::RotationGesture { + device_id: DEVICE_ID, + delta: unsafe { event.rotation() }, + phase, + }); + } + + #[method(pressureChangeWithEvent:)] + fn pressure_change_with_event(&self, event: &NSEvent) { + trace_scope!("pressureChangeWithEvent:"); + + self.queue_event(WindowEvent::TouchpadPressure { + device_id: DEVICE_ID, + pressure: unsafe { event.pressure() }, + stage: unsafe { event.stage() } as i64, + }); + } + + // Allows us to receive Ctrl-Tab and Ctrl-Esc. + // Note that this *doesn't* help with any missing Cmd inputs. + // https://github.com/chromium/chromium/blob/a86a8a6bcfa438fa3ac2eba6f02b3ad1f8e0756f/ui/views/cocoa/bridged_content_view.mm#L816 + #[method(_wantsKeyDownForEvent:)] + fn wants_key_down_for_event(&self, _event: &NSEvent) -> bool { + trace_scope!("_wantsKeyDownForEvent:"); + true + } + + #[method(acceptsFirstMouse:)] + fn accepts_first_mouse(&self, _event: &NSEvent) -> bool { + trace_scope!("acceptsFirstMouse:"); + self.ivars().accepts_first_mouse + } + } +); + +impl WinitView { + pub(super) fn new( + app_delegate: &ApplicationDelegate, + window: &WinitWindow, + accepts_first_mouse: bool, + option_as_alt: OptionAsAlt, + ) -> Retained { + let mtm = MainThreadMarker::from(window); + let this = mtm.alloc().set_ivars(ViewState { + app_delegate: app_delegate.retain(), + cursor_state: Default::default(), + ime_position: Default::default(), + ime_size: Default::default(), + modifiers: Default::default(), + phys_modifiers: Default::default(), + tracking_rect: Default::default(), + ime_state: Default::default(), + input_source: Default::default(), + ime_allowed: Default::default(), + forward_key_to_app: Default::default(), + marked_text: Default::default(), + accepts_first_mouse, + _ns_window: WeakId::new(&window.retain()), + option_as_alt: Cell::new(option_as_alt), + }); + let this: Retained = unsafe { msg_send_id![super(this), init] }; + + this.setPostsFrameChangedNotifications(true); + let notification_center = unsafe { NSNotificationCenter::defaultCenter() }; + unsafe { + notification_center.addObserver_selector_name_object( + &this, + sel!(frameDidChange:), + Some(NSViewFrameDidChangeNotification), + Some(&this), + ) + } + + *this.ivars().input_source.borrow_mut() = this.current_input_source(); + + this + } + + fn window(&self) -> Retained { + // TODO: Simply use `window` property on `NSView`. + // That only returns a window _after_ the view has been attached though! + // (which is incompatible with `frameDidChange:`) + // + // unsafe { msg_send_id![self, window] } + self.ivars() + ._ns_window + .load() + .expect("view to have a window") + } + + fn queue_event(&self, event: WindowEvent) { + self.ivars() + .app_delegate + .queue_window_event(self.window().id(), event); + } + + fn queue_device_event(&self, event: DeviceEvent) { + self.ivars().app_delegate.queue_device_event(event); + } + + fn scale_factor(&self) -> f64 { + self.window().backingScaleFactor() as f64 + } + + fn is_ime_enabled(&self) -> bool { + !matches!(self.ivars().ime_state.get(), ImeState::Disabled) + } + + fn current_input_source(&self) -> String { + self.inputContext() + .expect("input context") + .selectedKeyboardInputSource() + .map(|input_source| input_source.to_string()) + .unwrap_or_default() + } + + pub(super) fn cursor_icon(&self) -> Retained { + self.ivars().cursor_state.borrow().cursor.clone() + } + + pub(super) fn set_cursor_icon(&self, icon: Retained) { + let mut cursor_state = self.ivars().cursor_state.borrow_mut(); + cursor_state.cursor = icon; + } + + /// Set whether the cursor should be visible or not. + /// + /// Returns whether the state changed. + pub(super) fn set_cursor_visible(&self, visible: bool) -> bool { + let mut cursor_state = self.ivars().cursor_state.borrow_mut(); + if visible != cursor_state.visible { + cursor_state.visible = visible; + true + } else { + false + } + } + + pub(super) fn set_ime_allowed(&self, ime_allowed: bool) { + if self.ivars().ime_allowed.get() == ime_allowed { + return; + } + self.ivars().ime_allowed.set(ime_allowed); + if self.ivars().ime_allowed.get() { + return; + } + + // Clear markedText + *self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new(); + + if self.ivars().ime_state.get() != ImeState::Disabled { + self.ivars().ime_state.set(ImeState::Disabled); + self.queue_event(WindowEvent::Ime(Ime::Disabled)); + } + } + + pub(super) fn set_ime_cursor_area(&self, position: NSPoint, size: NSSize) { + self.ivars().ime_position.set(position); + self.ivars().ime_size.set(size); + let input_context = self.inputContext().expect("input context"); + input_context.invalidateCharacterCoordinates(); + } + + /// Reset modifiers and emit a synthetic ModifiersChanged event if deemed necessary. + pub(super) fn reset_modifiers(&self) { + if !self.ivars().modifiers.get().state().is_empty() { + self.ivars().modifiers.set(Modifiers::default()); + self.queue_event(WindowEvent::ModifiersChanged(self.ivars().modifiers.get())); + } + } + + pub(super) fn set_option_as_alt(&self, value: OptionAsAlt) { + self.ivars().option_as_alt.set(value) + } + + pub(super) fn option_as_alt(&self) -> OptionAsAlt { + self.ivars().option_as_alt.get() + } + + /// Update modifiers if `event` has something different + fn update_modifiers(&self, ns_event: &NSEvent, is_flags_changed_event: bool) { + use ElementState::{Pressed, Released}; + + let current_modifiers = event_mods(ns_event); + let prev_modifiers = self.ivars().modifiers.get(); + self.ivars().modifiers.set(current_modifiers); + + // This function was called form the flagsChanged event, which is triggered + // when the user presses/releases a modifier even if the same kind of modifier + // has already been pressed. + // + // When flags changed event has key code of zero it means that event doesn't carry any key + // event, thus we can't generate regular presses based on that. The `ModifiersChanged` + // later will work though, since the flags are attached to the event and contain valid + // information. + 'send_event: { + if is_flags_changed_event && unsafe { ns_event.keyCode() } != 0 { + let scancode = unsafe { ns_event.keyCode() }; + let physical_key = scancode_to_physicalkey(scancode as u32); + + // We'll correct the `is_press` later. + let mut event = + create_key_event(ns_event, false, false, Some(physical_key)); + + let key = code_to_key(physical_key, scancode); + // Ignore processing of unknown modifiers because we can't determine whether + // it was pressed or release reliably. + let Some(event_modifier) = key_to_modifier(&key) else { + break 'send_event; + }; + event.physical_key = physical_key; + event.logical_key = key.clone(); + event.location = code_to_location(physical_key); + let location_mask = ModLocationMask::from_location(event.location); + + let mut phys_mod_state = self.ivars().phys_modifiers.borrow_mut(); + let phys_mod = phys_mod_state + .entry(key) + .or_insert(ModLocationMask::empty()); + + let is_active = current_modifiers.state().contains(event_modifier); + let mut events = VecDeque::with_capacity(2); + + // There is no API for getting whether the button was pressed or released + // during this event. For this reason we have to do a bit of magic below + // to come up with a good guess whether this key was pressed or released. + // (This is not trivial because there are multiple buttons that may affect + // the same modifier) + if !is_active { + event.state = Released; + if phys_mod.contains(ModLocationMask::LEFT) { + let mut event = event.clone(); + event.location = KeyLocation::Left; + event.physical_key = + get_left_modifier_code(&event.logical_key).into(); + events.push_back(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + } + if phys_mod.contains(ModLocationMask::RIGHT) { + event.location = KeyLocation::Right; + event.physical_key = + get_right_modifier_code(&event.logical_key).into(); + events.push_back(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + } + *phys_mod = ModLocationMask::empty(); + } else { + if *phys_mod == location_mask { + // Here we hit a contradiction: + // The modifier state was "changed" to active, + // yet the only pressed modifier key was the one that we + // just got a change event for. + // This seemingly means that the only pressed modifier is now released, + // but at the same time the modifier became active. + // + // But this scenario is possible if we released modifiers + // while the application was not in focus. (Because we don't + // get informed of modifier key events while the application + // is not focused) + + // In this case we prioritize the information + // about the current modifier state which means + // that the button was pressed. + event.state = Pressed; + } else { + phys_mod.toggle(location_mask); + let is_pressed = phys_mod.contains(location_mask); + event.state = if is_pressed { Pressed } else { Released }; + } + + events.push_back(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + } + + drop(phys_mod_state); + + for event in events { + self.queue_event(event); + } + } + } + + if prev_modifiers == current_modifiers { + return; + } + + self.queue_event(WindowEvent::ModifiersChanged(self.ivars().modifiers.get())); + } + + fn mouse_click(&self, event: &NSEvent, button_state: ElementState) { + let button = mouse_button(event); + + self.update_modifiers(event, false); + + self.queue_event(WindowEvent::MouseInput { + device_id: DEVICE_ID, + state: button_state, + button, + }); + } + + fn mouse_motion(&self, event: &NSEvent) { + let window_point = unsafe { event.locationInWindow() }; + let view_point = self.convertPoint_fromView(window_point, None); + let frame = self.frame(); + + if view_point.x.is_sign_negative() + || view_point.y.is_sign_negative() + || view_point.x > frame.size.width + || view_point.y > frame.size.height + { + let mouse_buttons_down = unsafe { NSEvent::pressedMouseButtons() }; + if mouse_buttons_down == 0 { + // Point is outside of the client area (view) and no buttons are pressed + return; + } + } + + let view_point = LogicalPosition::new(view_point.x, view_point.y); + + self.update_modifiers(event, false); + + self.queue_event(WindowEvent::CursorMoved { + device_id: DEVICE_ID, + position: view_point.to_physical(self.scale_factor()), + }); + } +} + +/// Get the mouse button from the NSEvent. +fn mouse_button(event: &NSEvent) -> MouseButton { + // The buttonNumber property only makes sense for the mouse events: + // NSLeftMouse.../NSRightMouse.../NSOtherMouse... + // For the other events, it's always set to 0. + // MacOS only defines the left, right and middle buttons, 3..=31 are left as generic buttons, + // but 3 and 4 are very commonly used as Back and Forward by hardware vendors and applications. + match unsafe { event.buttonNumber() } { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + 2 => MouseButton::Middle, + 3 => MouseButton::Back, + 4 => MouseButton::Forward, + n => MouseButton::Other(n as u16), + } +} + +// NOTE: to get option as alt working we need to rewrite events +// we're getting from the operating system, which makes it +// impossible to provide such events as extra in `KeyEvent`. +fn replace_event(event: &NSEvent, option_as_alt: OptionAsAlt) -> Retained { + let ev_mods = event_mods(event).state; + let ignore_alt_characters = match option_as_alt { + OptionAsAlt::OnlyLeft if lalt_pressed(event) => true, + OptionAsAlt::OnlyRight if ralt_pressed(event) => true, + OptionAsAlt::Both if ev_mods.alt_key() => true, + _ => false, + } && !ev_mods.control_key() + && !ev_mods.super_key(); + + if ignore_alt_characters { + let ns_chars = unsafe { + event + .charactersIgnoringModifiers() + .expect("expected characters to be non-null") + }; + + unsafe { + NSEvent::keyEventWithType_location_modifierFlags_timestamp_windowNumber_context_characters_charactersIgnoringModifiers_isARepeat_keyCode( + event.r#type(), + event.locationInWindow(), + event.modifierFlags(), + event.timestamp(), + event.windowNumber(), + None, + &ns_chars, + &ns_chars, + event.isARepeat(), + event.keyCode(), + ) + .unwrap() + } + } else { + event.copy() + } +} diff --git a/rio-window/src/platform_impl/macos/window.rs b/rio-window/src/platform_impl/macos/window.rs new file mode 100644 index 00000000..e090159e --- /dev/null +++ b/rio-window/src/platform_impl/macos/window.rs @@ -0,0 +1,132 @@ +#![allow(clippy::unnecessary_cast)] + +use objc2::rc::{autoreleasepool, Retained}; +use objc2::{declare_class, mutability, ClassType, DeclaredClass}; +use objc2_app_kit::{NSResponder, NSWindow}; +use objc2_foundation::{MainThreadBound, MainThreadMarker, NSObject}; + +use super::event_loop::ActiveEventLoop; +use super::window_delegate::WindowDelegate; +use crate::error::OsError as RootOsError; +use crate::window::WindowAttributes; + +pub(crate) struct Window { + window: MainThreadBound>, + /// The window only keeps a weak reference to this, so we must keep it around here. + delegate: MainThreadBound>, +} + +impl Drop for Window { + fn drop(&mut self) { + self.window + .get_on_main(|window| autoreleasepool(|_| window.close())) + } +} + +impl Window { + pub(crate) fn new( + window_target: &ActiveEventLoop, + attributes: WindowAttributes, + ) -> Result { + let mtm = window_target.mtm; + let delegate = autoreleasepool(|_| { + WindowDelegate::new(window_target.app_delegate(), attributes, mtm) + })?; + Ok(Window { + window: MainThreadBound::new(delegate.window().retain(), mtm), + delegate: MainThreadBound::new(delegate, mtm), + }) + } + + pub(crate) fn maybe_queue_on_main( + &self, + f: impl FnOnce(&WindowDelegate) + Send + 'static, + ) { + // For now, don't actually do queuing, since it may be less predictable + self.maybe_wait_on_main(f) + } + + pub(crate) fn maybe_wait_on_main( + &self, + f: impl FnOnce(&WindowDelegate) -> R + Send, + ) -> R { + self.delegate.get_on_main(|delegate| f(delegate)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_window_handle_rwh_06( + &self, + ) -> Result { + if let Some(mtm) = MainThreadMarker::new() { + Ok(self.delegate.get(mtm).raw_window_handle_rwh_06()) + } else { + Err(rwh_06::HandleError::Unavailable) + } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::AppKit( + rwh_06::AppKitDisplayHandle::new(), + )) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(pub usize); + +impl WindowId { + pub const unsafe fn dummy() -> Self { + Self(0) + } +} + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 as u64 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id as usize) + } +} + +declare_class!( + #[derive(Debug)] + pub struct WinitWindow; + + unsafe impl ClassType for WinitWindow { + #[inherits(NSResponder, NSObject)] + type Super = NSWindow; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitWindow"; + } + + impl DeclaredClass for WinitWindow {} + + unsafe impl WinitWindow { + #[method(canBecomeMainWindow)] + fn can_become_main_window(&self) -> bool { + trace_scope!("canBecomeMainWindow"); + true + } + + #[method(canBecomeKeyWindow)] + fn can_become_key_window(&self) -> bool { + trace_scope!("canBecomeKeyWindow"); + true + } + } +); + +impl WinitWindow { + pub(super) fn id(&self) -> WindowId { + WindowId(self as *const Self as usize) + } +} diff --git a/rio-window/src/platform_impl/macos/window_delegate.rs b/rio-window/src/platform_impl/macos/window_delegate.rs new file mode 100644 index 00000000..25e6db2e --- /dev/null +++ b/rio-window/src/platform_impl/macos/window_delegate.rs @@ -0,0 +1,1861 @@ +#![allow(clippy::unnecessary_cast)] +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; + +use core_graphics::display::{CGDisplay, CGPoint}; +use monitor::VideoModeHandle; +use objc2::rc::{autoreleasepool, Retained}; +use objc2::runtime::{AnyObject, ProtocolObject}; +use objc2::{declare_class, msg_send_id, mutability, sel, ClassType, DeclaredClass}; +use objc2_app_kit::{ + NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSApplication, + NSApplicationPresentationOptions, NSBackingStoreType, NSColor, NSDraggingDestination, + NSFilenamesPboardType, NSPasteboard, NSRequestUserAttentionType, NSScreen, NSView, + NSWindowButton, NSWindowDelegate, NSWindowFullScreenButton, NSWindowLevel, + NSWindowOcclusionState, NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, + NSWindowTabbingMode, NSWindowTitleVisibility, +}; +use objc2_foundation::{ + ns_string, CGFloat, MainThreadMarker, NSArray, NSCopying, + NSDistributedNotificationCenter, NSObject, NSObjectNSDelayedPerforming, + NSObjectNSThreadPerformAdditions, NSObjectProtocol, NSPoint, NSRect, NSSize, + NSString, +}; + +use super::app_delegate::ApplicationDelegate; +use super::cursor::cursor_from_icon; +use super::monitor::{self, flip_window_screen_coordinates, get_display_id}; +use super::view::WinitView; +use super::window::WinitWindow; +use super::{ffi, Fullscreen, MonitorHandle, OsError, WindowId}; +use crate::dpi::{ + LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size, +}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event::WindowEvent; +use crate::platform::macos::{OptionAsAlt, WindowExtMacOS}; +use crate::window::{ + Cursor, CursorGrabMode, Icon, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowLevel, +}; + +#[derive(Clone, Debug)] +pub struct PlatformSpecificWindowAttributes { + pub movable_by_window_background: bool, + pub titlebar_transparent: bool, + pub title_hidden: bool, + pub titlebar_hidden: bool, + pub titlebar_buttons_hidden: bool, + pub fullsize_content_view: bool, + pub disallow_hidpi: bool, + pub has_shadow: bool, + pub accepts_first_mouse: bool, + pub tabbing_identifier: Option, + pub option_as_alt: OptionAsAlt, +} + +impl Default for PlatformSpecificWindowAttributes { + #[inline] + fn default() -> Self { + Self { + movable_by_window_background: false, + titlebar_transparent: false, + title_hidden: false, + titlebar_hidden: false, + titlebar_buttons_hidden: false, + fullsize_content_view: false, + disallow_hidpi: false, + has_shadow: true, + accepts_first_mouse: true, + tabbing_identifier: None, + option_as_alt: Default::default(), + } + } +} + +#[derive(Debug)] +pub(crate) struct State { + /// Strong reference to the global application state. + app_delegate: Retained, + + window: Retained, + + current_theme: Cell>, + + // During `windowDidResize`, we use this to only send Moved if the position changed. + // + // This is expressed in native screen coordinates. + previous_position: Cell>, + + // Used to prevent redundant events. + previous_scale_factor: Cell, + + /// The current resize increments for the window content. + resize_increments: Cell, + /// Whether the window is showing decorations. + decorations: Cell, + resizable: Cell, + maximized: Cell, + + /// Presentation options saved before entering `set_simple_fullscreen`, and + /// restored upon exiting it. Also used when transitioning from Borderless to + /// Exclusive fullscreen in `set_fullscreen` because we need to disable the menu + /// bar in exclusive fullscreen but want to restore the original options when + /// transitioning back to borderless fullscreen. + save_presentation_opts: Cell>, + // This is set when WindowAttributes::with_fullscreen was set, + // see comments of `window_did_fail_to_enter_fullscreen` + initial_fullscreen: Cell, + /// This field tracks the current fullscreen state of the window + /// (as seen by `WindowDelegate`). + fullscreen: RefCell>, + // If it is attempted to toggle fullscreen when in_fullscreen_transition is true, + // Set target_fullscreen and do after fullscreen transition is end. + target_fullscreen: RefCell>>, + // This is true between windowWillEnterFullScreen and windowDidEnterFullScreen + // or windowWillExitFullScreen and windowDidExitFullScreen. + // We must not toggle fullscreen when this is true. + in_fullscreen_transition: Cell, + standard_frame: Cell>, + is_simple_fullscreen: Cell, + saved_style: Cell>, +} + +declare_class!( + pub(crate) struct WindowDelegate; + + unsafe impl ClassType for WindowDelegate { + type Super = NSObject; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitWindowDelegate"; + } + + impl DeclaredClass for WindowDelegate { + type Ivars = State; + } + + unsafe impl NSObjectProtocol for WindowDelegate {} + + unsafe impl NSWindowDelegate for WindowDelegate { + #[method(windowShouldClose:)] + fn window_should_close(&self, _: Option<&AnyObject>) -> bool { + trace_scope!("windowShouldClose:"); + self.queue_event(WindowEvent::CloseRequested); + false + } + + #[method(windowWillClose:)] + fn window_will_close(&self, _: Option<&AnyObject>) { + trace_scope!("windowWillClose:"); + // `setDelegate:` retains the previous value and then autoreleases it + autoreleasepool(|_| { + // Since El Capitan, we need to be careful that delegate methods can't + // be called after the window closes. + self.window().setDelegate(None); + }); + self.queue_event(WindowEvent::Destroyed); + } + + #[method(windowDidResize:)] + fn window_did_resize(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidResize:"); + // NOTE: WindowEvent::Resized is reported in frameDidChange. + self.emit_move_event(); + } + + #[method(windowWillStartLiveResize:)] + fn window_will_start_live_resize(&self, _: Option<&AnyObject>) { + trace_scope!("windowWillStartLiveResize:"); + + let increments = self.ivars().resize_increments.get(); + self.set_resize_increments_inner(increments); + } + + #[method(windowDidEndLiveResize:)] + fn window_did_end_live_resize(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidEndLiveResize:"); + self.set_resize_increments_inner(NSSize::new(1., 1.)); + } + + // This won't be triggered if the move was part of a resize. + #[method(windowDidMove:)] + fn window_did_move(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidMove:"); + self.emit_move_event(); + } + + #[method(windowDidChangeBackingProperties:)] + fn window_did_change_backing_properties(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidChangeBackingProperties:"); + self.queue_static_scale_factor_changed_event(); + } + + #[method(windowDidBecomeKey:)] + fn window_did_become_key(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidBecomeKey:"); + // TODO: center the cursor if the window had mouse grab when it + // lost focus + self.queue_event(WindowEvent::Focused(true)); + } + + #[method(windowDidResignKey:)] + fn window_did_resign_key(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidResignKey:"); + // It happens rather often, e.g. when the user is Cmd+Tabbing, that the + // NSWindowDelegate will receive a didResignKey event despite no event + // being received when the modifiers are released. This is because + // flagsChanged events are received by the NSView instead of the + // NSWindowDelegate, and as a result a tracked modifiers state can quite + // easily fall out of synchrony with reality. This requires us to emit + // a synthetic ModifiersChanged event when we lose focus. + self.view().reset_modifiers(); + + self.queue_event(WindowEvent::Focused(false)); + } + + /// Invoked when before enter fullscreen + #[method(windowWillEnterFullScreen:)] + fn window_will_enter_fullscreen(&self, _: Option<&AnyObject>) { + trace_scope!("windowWillEnterFullScreen:"); + + self.ivars().maximized.set(self.is_zoomed()); + let mut fullscreen = self.ivars().fullscreen.borrow_mut(); + match &*fullscreen { + // Exclusive mode sets the state in `set_fullscreen` as the user + // can't enter exclusive mode by other means (like the + // fullscreen button on the window decorations) + Some(Fullscreen::Exclusive(_)) => (), + // `window_will_enter_fullscreen` was triggered and we're already + // in fullscreen, so we must've reached here by `set_fullscreen` + // as it updates the state + Some(Fullscreen::Borderless(_)) => (), + // Otherwise, we must've reached fullscreen by the user clicking + // on the green fullscreen button. Update state! + None => { + let current_monitor = self.current_monitor_inner(); + *fullscreen = Some(Fullscreen::Borderless(current_monitor)); + }, + } + self.ivars().in_fullscreen_transition.set(true); + } + + /// Invoked when before exit fullscreen + #[method(windowWillExitFullScreen:)] + fn window_will_exit_fullscreen(&self, _: Option<&AnyObject>) { + trace_scope!("windowWillExitFullScreen:"); + + self.ivars().in_fullscreen_transition.set(true); + } + + #[method(window:willUseFullScreenPresentationOptions:)] + fn window_will_use_fullscreen_presentation_options( + &self, + _: Option<&AnyObject>, + proposed_options: NSApplicationPresentationOptions, + ) -> NSApplicationPresentationOptions { + trace_scope!("window:willUseFullScreenPresentationOptions:"); + // Generally, games will want to disable the menu bar and the dock. Ideally, + // this would be configurable by the user. Unfortunately because of our + // `CGShieldingWindowLevel() + 1` hack (see `set_fullscreen`), our window is + // placed on top of the menu bar in exclusive fullscreen mode. This looks + // broken so we always disable the menu bar in exclusive fullscreen. We may + // still want to make this configurable for borderless fullscreen. Right now + // we don't, for consistency. If we do, it should be documented that the + // user-provided options are ignored in exclusive fullscreen. + let mut options = proposed_options; + let fullscreen = self.ivars().fullscreen.borrow(); + if let Some(Fullscreen::Exclusive(_)) = &*fullscreen { + options = NSApplicationPresentationOptions::NSApplicationPresentationFullScreen + | NSApplicationPresentationOptions::NSApplicationPresentationHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar; + } + + options + } + + /// Invoked when entered fullscreen + #[method(windowDidEnterFullScreen:)] + fn window_did_enter_fullscreen(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidEnterFullScreen:"); + self.ivars().initial_fullscreen.set(false); + self.ivars().in_fullscreen_transition.set(false); + if let Some(target_fullscreen) = self.ivars().target_fullscreen.take() { + self.set_fullscreen(target_fullscreen); + } + } + + /// Invoked when exited fullscreen + #[method(windowDidExitFullScreen:)] + fn window_did_exit_fullscreen(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidExitFullScreen:"); + + self.restore_state_from_fullscreen(); + self.ivars().in_fullscreen_transition.set(false); + if let Some(target_fullscreen) = self.ivars().target_fullscreen.take() { + self.set_fullscreen(target_fullscreen); + } + } + + /// Invoked when fail to enter fullscreen + /// + /// When this window launch from a fullscreen app (e.g. launch from VS Code + /// terminal), it creates a new virtual desktop and a transition animation. + /// This animation takes one second and cannot be disable without + /// elevated privileges. In this animation time, all toggleFullscreen events + /// will be failed. In this implementation, we will try again by using + /// performSelector:withObject:afterDelay: until window_did_enter_fullscreen. + /// It should be fine as we only do this at initialization (i.e with_fullscreen + /// was set). + /// + /// From Apple doc: + /// In some cases, the transition to enter full-screen mode can fail, + /// due to being in the midst of handling some other animation or user gesture. + /// This method indicates that there was an error, and you should clean up any + /// work you may have done to prepare to enter full-screen mode. + #[method(windowDidFailToEnterFullScreen:)] + fn window_did_fail_to_enter_fullscreen(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidFailToEnterFullScreen:"); + self.ivars().in_fullscreen_transition.set(false); + self.ivars().target_fullscreen.replace(None); + if self.ivars().initial_fullscreen.get() { + unsafe { + self.window().performSelector_withObject_afterDelay( + sel!(toggleFullScreen:), + None, + 0.5, + ) + }; + } else { + self.restore_state_from_fullscreen(); + } + } + + // Invoked when the occlusion state of the window changes + #[method(windowDidChangeOcclusionState:)] + fn window_did_change_occlusion_state(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidChangeOcclusionState:"); + let visible = self.window().occlusionState().contains(NSWindowOcclusionState::Visible); + self.queue_event(WindowEvent::Occluded(!visible)); + } + + #[method(windowDidChangeScreen:)] + fn window_did_change_screen(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidChangeScreen:"); + let is_simple_fullscreen = self.ivars().is_simple_fullscreen.get(); + if is_simple_fullscreen { + if let Some(screen) = self.window().screen() { + self.window().setFrame_display(screen.frame(), true); + } + } + } + } + + unsafe impl NSDraggingDestination for WindowDelegate { + /// Invoked when the dragged image enters destination bounds or frame + #[method(draggingEntered:)] + fn dragging_entered(&self, sender: &NSObject) -> bool { + trace_scope!("draggingEntered:"); + + use std::path::PathBuf; + + let pb: Retained = unsafe { msg_send_id![sender, draggingPasteboard] }; + let filenames = pb.propertyListForType(unsafe { NSFilenamesPboardType }).unwrap(); + let filenames: Retained> = unsafe { Retained::cast(filenames) }; + + filenames.into_iter().for_each(|file| { + let path = PathBuf::from(file.to_string()); + self.queue_event(WindowEvent::HoveredFile(path)); + }); + + true + } + + /// Invoked when the image is released + #[method(prepareForDragOperation:)] + fn prepare_for_drag_operation(&self, _sender: &NSObject) -> bool { + trace_scope!("prepareForDragOperation:"); + true + } + + /// Invoked after the released image has been removed from the screen + #[method(performDragOperation:)] + fn perform_drag_operation(&self, sender: &NSObject) -> bool { + trace_scope!("performDragOperation:"); + + use std::path::PathBuf; + + let pb: Retained = unsafe { msg_send_id![sender, draggingPasteboard] }; + let filenames = pb.propertyListForType(unsafe { NSFilenamesPboardType }).unwrap(); + let filenames: Retained> = unsafe { Retained::cast(filenames) }; + + filenames.into_iter().for_each(|file| { + let path = PathBuf::from(file.to_string()); + self.queue_event(WindowEvent::DroppedFile(path)); + }); + + true + } + + /// Invoked when the dragging operation is complete + #[method(concludeDragOperation:)] + fn conclude_drag_operation(&self, _sender: Option<&NSObject>) { + trace_scope!("concludeDragOperation:"); + } + + /// Invoked when the dragging operation is cancelled + #[method(draggingExited:)] + fn dragging_exited(&self, _sender: Option<&NSObject>) { + trace_scope!("draggingExited:"); + self.queue_event(WindowEvent::HoveredFileCancelled); + } + } + + unsafe impl WindowDelegate { + // Observe theme change + #[method(effectiveAppearanceDidChange:)] + fn effective_appearance_did_change(&self, sender: Option<&AnyObject>) { + trace_scope!("effectiveAppearanceDidChange:"); + unsafe { + self.performSelectorOnMainThread_withObject_waitUntilDone( + sel!(effectiveAppearanceDidChangedOnMainThread:), + sender, + false, + ) + }; + } + + #[method(effectiveAppearanceDidChangedOnMainThread:)] + fn effective_appearance_did_changed_on_main_thread(&self, _: Option<&AnyObject>) { + let mtm = MainThreadMarker::from(self); + let theme = get_ns_theme(mtm); + let old_theme = self.ivars().current_theme.replace(Some(theme)); + if old_theme != Some(theme) { + self.queue_event(WindowEvent::ThemeChanged(theme)); + } + } + } +); + +fn new_window( + app_delegate: &ApplicationDelegate, + attrs: &WindowAttributes, + mtm: MainThreadMarker, +) -> Option> { + autoreleasepool(|_| { + let screen = match attrs.fullscreen.clone().map(Into::into) { + Some(Fullscreen::Borderless(Some(monitor))) + | Some(Fullscreen::Exclusive(VideoModeHandle { monitor, .. })) => { + monitor.ns_screen(mtm).or_else(|| NSScreen::mainScreen(mtm)) + } + Some(Fullscreen::Borderless(None)) => NSScreen::mainScreen(mtm), + None => None, + }; + let frame = match &screen { + Some(screen) => screen.frame(), + None => { + let scale_factor = NSScreen::mainScreen(mtm) + .map(|screen| screen.backingScaleFactor() as f64) + .unwrap_or(1.0); + let size = match attrs.inner_size { + Some(size) => { + let size = size.to_logical(scale_factor); + NSSize::new(size.width, size.height) + } + None => NSSize::new(800.0, 600.0), + }; + let position = match attrs.position { + Some(position) => { + let position = position.to_logical(scale_factor); + flip_window_screen_coordinates(NSRect::new( + NSPoint::new(position.x, position.y), + size, + )) + } + // This value is ignored by calling win.center() below + None => NSPoint::new(0.0, 0.0), + }; + NSRect::new(position, size) + } + }; + + let mut masks = if (!attrs.decorations && screen.is_none()) + || attrs.platform_specific.titlebar_hidden + { + // Resizable without a titlebar or borders + // if decorations is set to false, ignore pl_attrs + // + // if the titlebar is hidden, ignore other pl_attrs + NSWindowStyleMask::Borderless + | NSWindowStyleMask::Resizable + | NSWindowStyleMask::Miniaturizable + } else { + // default case, resizable window with titlebar and titlebar buttons + NSWindowStyleMask::Closable + | NSWindowStyleMask::Miniaturizable + | NSWindowStyleMask::Resizable + | NSWindowStyleMask::Titled + }; + + if !attrs.resizable { + masks &= !NSWindowStyleMask::Resizable; + } + + if !attrs.enabled_buttons.contains(WindowButtons::MINIMIZE) { + masks &= !NSWindowStyleMask::Miniaturizable; + } + + if !attrs.enabled_buttons.contains(WindowButtons::CLOSE) { + masks &= !NSWindowStyleMask::Closable; + } + + if attrs.platform_specific.fullsize_content_view { + masks |= NSWindowStyleMask::FullSizeContentView; + } + + let window: Option> = unsafe { + msg_send_id![ + super(mtm.alloc().set_ivars(())), + initWithContentRect: frame, + styleMask: masks, + backing: NSBackingStoreType::NSBackingStoreBuffered, + defer: false, + ] + }; + let window = window?; + + // It is very important for correct memory management that we + // disable the extra release that would otherwise happen when + // calling `close` on the window. + unsafe { window.setReleasedWhenClosed(false) }; + + window.setTitle(&NSString::from_str(&attrs.title)); + window.setAcceptsMouseMovedEvents(true); + + if let Some(identifier) = &attrs.platform_specific.tabbing_identifier { + window.setTabbingIdentifier(&NSString::from_str(identifier)); + window.setTabbingMode(NSWindowTabbingMode::Preferred); + } + + if attrs.content_protected { + window.setSharingType(NSWindowSharingType::NSWindowSharingNone); + } + + if attrs.platform_specific.titlebar_transparent { + window.setTitlebarAppearsTransparent(true); + } + if attrs.platform_specific.title_hidden { + window.setTitleVisibility(NSWindowTitleVisibility::NSWindowTitleHidden); + } + if attrs.platform_specific.titlebar_buttons_hidden { + for titlebar_button in &[ + #[allow(deprecated)] + NSWindowFullScreenButton, + NSWindowButton::NSWindowMiniaturizeButton, + NSWindowButton::NSWindowCloseButton, + NSWindowButton::NSWindowZoomButton, + ] { + if let Some(button) = window.standardWindowButton(*titlebar_button) { + button.setHidden(true); + } + } + } + if attrs.platform_specific.movable_by_window_background { + window.setMovableByWindowBackground(true); + } + + if !attrs.enabled_buttons.contains(WindowButtons::MAXIMIZE) { + if let Some(button) = + window.standardWindowButton(NSWindowButton::NSWindowZoomButton) + { + button.setEnabled(false); + } + } + + if !attrs.platform_specific.has_shadow { + window.setHasShadow(false); + } + if attrs.position.is_none() { + window.center(); + } + + let view = WinitView::new( + app_delegate, + &window, + attrs.platform_specific.accepts_first_mouse, + attrs.platform_specific.option_as_alt, + ); + + // The default value of `setWantsBestResolutionOpenGLSurface:` was `false` until + // macos 10.14 and `true` after 10.15, we should set it to `YES` or `NO` to avoid + // always the default system value in favour of the user's code + #[allow(deprecated)] + view.setWantsBestResolutionOpenGLSurface(!attrs.platform_specific.disallow_hidpi); + + // On Mojave, views automatically become layer-backed shortly after being added to + // a window. Changing the layer-backedness of a view breaks the association between + // the view and its associated OpenGL context. To work around this, on Mojave we + // explicitly make the view layer-backed up front so that AppKit doesn't do it + // itself and break the association with its context. + if unsafe { NSAppKitVersionNumber }.floor() > NSAppKitVersionNumber10_12 { + view.setWantsLayer(true); + } + + // Configure the new view as the "key view" for the window + window.setContentView(Some(&view)); + window.setInitialFirstResponder(Some(&view)); + + if attrs.transparent { + window.setOpaque(false); + } + + // register for drag and drop operations. + window.registerForDraggedTypes(&NSArray::from_id_slice(&[unsafe { + NSFilenamesPboardType + } + .copy()])); + + Some(window) + }) +} + +impl WindowDelegate { + pub(super) fn new( + app_delegate: &ApplicationDelegate, + attrs: WindowAttributes, + mtm: MainThreadMarker, + ) -> Result, RootOsError> { + let window = new_window(app_delegate, &attrs, mtm).ok_or_else(|| { + os_error!(OsError::CreationError("couldn't create `NSWindow`")) + })?; + + #[cfg(feature = "rwh_06")] + match attrs.parent_window.map(|handle| handle.0) { + Some(rwh_06::RawWindowHandle::AppKit(handle)) => { + // SAFETY: Caller ensures the pointer is valid or NULL + // Unwrap is fine, since the pointer comes from `NonNull`. + let parent_view: Retained = + unsafe { Retained::retain(handle.ns_view.as_ptr().cast()) }.unwrap(); + let parent = parent_view.window().ok_or_else(|| { + os_error!(OsError::CreationError( + "parent view should be installed in a window" + )) + })?; + + // SAFETY: We know that there are no parent -> child -> parent cycles since the only + // place in `winit` where we allow making a window a child window is + // right here, just after it's been created. + unsafe { + parent.addChildWindow_ordered( + &window, + NSWindowOrderingMode::NSWindowAbove, + ) + }; + } + Some(raw) => panic!("invalid raw window handle {raw:?} on macOS"), + None => (), + } + + let resize_increments = match attrs + .resize_increments + .map(|i| i.to_logical(window.backingScaleFactor() as _)) + { + Some(LogicalSize { width, height }) if width >= 1. && height >= 1. => { + NSSize::new(width, height) + } + _ => NSSize::new(1., 1.), + }; + + let scale_factor = window.backingScaleFactor() as _; + + let current_theme = match attrs.preferred_theme { + Some(theme) => Some(theme), + None => Some(get_ns_theme(mtm)), + }; + + let delegate = mtm.alloc().set_ivars(State { + app_delegate: app_delegate.retain(), + window: window.retain(), + current_theme: Cell::new(current_theme), + previous_position: Cell::new(None), + previous_scale_factor: Cell::new(scale_factor), + resize_increments: Cell::new(resize_increments), + decorations: Cell::new(attrs.decorations), + resizable: Cell::new(attrs.resizable), + maximized: Cell::new(attrs.maximized), + save_presentation_opts: Cell::new(None), + initial_fullscreen: Cell::new(attrs.fullscreen.is_some()), + fullscreen: RefCell::new(None), + target_fullscreen: RefCell::new(None), + in_fullscreen_transition: Cell::new(false), + standard_frame: Cell::new(None), + is_simple_fullscreen: Cell::new(false), + saved_style: Cell::new(None), + }); + let delegate: Retained = + unsafe { msg_send_id![super(delegate), init] }; + + if scale_factor != 1.0 { + delegate.queue_static_scale_factor_changed_event(); + } + window.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); + + // Enable theme change event + let notification_center = + unsafe { NSDistributedNotificationCenter::defaultCenter() }; + unsafe { + notification_center.addObserver_selector_name_object( + &delegate, + sel!(effectiveAppearanceDidChange:), + Some(ns_string!("AppleInterfaceThemeChangedNotification")), + None, + ) + }; + + if attrs.blur { + delegate.set_blur(attrs.blur); + } + + if let Some(dim) = attrs.min_inner_size { + delegate.set_min_inner_size(Some(dim)); + } + if let Some(dim) = attrs.max_inner_size { + delegate.set_max_inner_size(Some(dim)); + } + + delegate.set_window_level(attrs.window_level); + + delegate.set_cursor(attrs.cursor); + + // XXX Send `Focused(false)` right after creating the window delegate, so we won't + // obscure the real focused events on the startup. + delegate.queue_event(WindowEvent::Focused(false)); + + // Set fullscreen mode after we setup everything + delegate.set_fullscreen(attrs.fullscreen.map(Into::into)); + + // Setting the window as key has to happen *after* we set the fullscreen + // state, since otherwise we'll briefly see the window at normal size + // before it transitions. + if attrs.visible { + if attrs.active { + // Tightly linked with `app_state::window_activation_hack` + window.makeKeyAndOrderFront(None); + } else { + window.orderFront(None); + } + } + + if attrs.maximized { + delegate.set_maximized(attrs.maximized); + } + + Ok(delegate) + } + + #[track_caller] + pub(super) fn view(&self) -> Retained { + // SAFETY: The view inside WinitWindow is always `WinitView` + unsafe { Retained::cast(self.window().contentView().unwrap()) } + } + + #[track_caller] + pub(super) fn window(&self) -> &WinitWindow { + &self.ivars().window + } + + #[track_caller] + pub(crate) fn id(&self) -> WindowId { + self.window().id() + } + + pub(crate) fn queue_event(&self, event: WindowEvent) { + self.ivars() + .app_delegate + .queue_window_event(self.window().id(), event); + } + + fn queue_static_scale_factor_changed_event(&self) { + let scale_factor = self.scale_factor(); + if scale_factor == self.ivars().previous_scale_factor.get() { + return; + }; + + self.ivars().previous_scale_factor.set(scale_factor); + let content_size = self + .window() + .contentRectForFrameRect(self.window().frame()) + .size; + let content_size = LogicalSize::new(content_size.width, content_size.height); + + self.ivars() + .app_delegate + .queue_static_scale_factor_changed_event( + self.window().retain(), + content_size.to_physical(scale_factor), + scale_factor, + ); + } + + fn emit_move_event(&self) { + let frame = self.window().frame(); + if self.ivars().previous_position.get() == Some(frame.origin) { + return; + } + self.ivars().previous_position.set(Some(frame.origin)); + + let position = flip_window_screen_coordinates(frame); + let position = + LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor()); + self.queue_event(WindowEvent::Moved(position)); + } + + fn set_style_mask(&self, mask: NSWindowStyleMask) { + self.window().setStyleMask(mask); + // If we don't do this, key handling will break + // (at least until the window is clicked again/etc.) + let _ = self.window().makeFirstResponder(Some(&self.view())); + } + + pub fn set_title(&self, title: &str) { + self.window().setTitle(&NSString::from_str(title)) + } + + pub fn set_subtitle(&self, subtitle: &str) { + unsafe { + self.window().setSubtitle(&NSString::from_str(subtitle)); + } + } + + pub fn set_transparent(&self, transparent: bool) { + self.window().setOpaque(!transparent); + + if transparent { + unsafe { + self.window() + .setBackgroundColor(Some(&NSColor::clearColor())); + } + } else { + unsafe { + self.window() + .setBackgroundColor(Some(&NSColor::blackColor())); + } + } + } + + pub fn set_blur(&self, blur: bool) { + // NOTE: in general we want to specify the blur radius, but the choice of 80 + // should be a reasonable default. + let radius = if blur { 80 } else { 0 }; + let window_number = unsafe { self.window().windowNumber() }; + unsafe { + ffi::CGSSetWindowBackgroundBlurRadius( + ffi::CGSMainConnectionID(), + window_number, + radius, + ); + } + } + + pub fn set_visible(&self, visible: bool) { + match visible { + true => self.window().makeKeyAndOrderFront(None), + false => self.window().orderOut(None), + } + } + + #[inline] + pub fn is_visible(&self) -> Option { + Some(self.window().isVisible()) + } + + pub fn request_redraw(&self) { + self.ivars().app_delegate.queue_redraw(self.window().id()); + } + + #[inline] + pub fn pre_present_notify(&self) {} + + pub fn outer_position(&self) -> Result, NotSupportedError> { + let position = flip_window_screen_coordinates(self.window().frame()); + Ok(LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor())) + } + + pub fn inner_position(&self) -> Result, NotSupportedError> { + let content_rect = self.window().contentRectForFrameRect(self.window().frame()); + let position = flip_window_screen_coordinates(content_rect); + Ok(LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor())) + } + + pub fn set_outer_position(&self, position: Position) { + let position = position.to_logical(self.scale_factor()); + let point = flip_window_screen_coordinates(NSRect::new( + NSPoint::new(position.x, position.y), + self.window().frame().size, + )); + unsafe { self.window().setFrameOrigin(point) }; + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let content_rect = self.window().contentRectForFrameRect(self.window().frame()); + let logical = LogicalSize::new(content_rect.size.width, content_rect.size.height); + logical.to_physical(self.scale_factor()) + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + let frame = self.window().frame(); + let logical = LogicalSize::new(frame.size.width, frame.size.height); + logical.to_physical(self.scale_factor()) + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let scale_factor = self.scale_factor(); + let size = size.to_logical(scale_factor); + self.window() + .setContentSize(NSSize::new(size.width, size.height)); + None + } + + pub fn set_min_inner_size(&self, dimensions: Option) { + let dimensions = dimensions.unwrap_or(Size::Logical(LogicalSize { + width: 0.0, + height: 0.0, + })); + let min_size = dimensions.to_logical::(self.scale_factor()); + + let min_size = NSSize::new(min_size.width, min_size.height); + unsafe { self.window().setContentMinSize(min_size) }; + + // If necessary, resize the window to match constraint + let mut current_size = self + .window() + .contentRectForFrameRect(self.window().frame()) + .size; + if current_size.width < min_size.width { + current_size.width = min_size.width; + } + if current_size.height < min_size.height { + current_size.height = min_size.height; + } + self.window().setContentSize(current_size); + } + + pub fn set_max_inner_size(&self, dimensions: Option) { + let dimensions = dimensions.unwrap_or(Size::Logical(LogicalSize { + width: f32::MAX as f64, + height: f32::MAX as f64, + })); + let scale_factor = self.scale_factor(); + let max_size = dimensions.to_logical::(scale_factor); + + let max_size = NSSize::new(max_size.width, max_size.height); + unsafe { self.window().setContentMaxSize(max_size) }; + + // If necessary, resize the window to match constraint + let mut current_size = self + .window() + .contentRectForFrameRect(self.window().frame()) + .size; + if max_size.width < current_size.width { + current_size.width = max_size.width; + } + if max_size.height < current_size.height { + current_size.height = max_size.height; + } + self.window().setContentSize(current_size); + } + + pub fn resize_increments(&self) -> Option> { + let increments = self.ivars().resize_increments.get(); + let (w, h) = (increments.width, increments.height); + if w > 1.0 || h > 1.0 { + Some(LogicalSize::new(w, h).to_physical(self.scale_factor())) + } else { + None + } + } + + pub fn set_resize_increments(&self, increments: Option) { + // XXX the resize increments are only used during live resizes. + self.ivars().resize_increments.set( + increments + .map(|increments| { + let logical = increments.to_logical::(self.scale_factor()); + NSSize::new(logical.width.max(1.0), logical.height.max(1.0)) + }) + .unwrap_or_else(|| NSSize::new(1.0, 1.0)), + ); + } + + pub(crate) fn set_resize_increments_inner(&self, size: NSSize) { + // It was concluded (#2411) that there is never a use-case for + // "outer" resize increments, hence we set "inner" ones here. + // ("outer" in macOS being just resizeIncrements, and "inner" - contentResizeIncrements) + // This is consistent with X11 size hints behavior + self.window().setContentResizeIncrements(size); + } + + #[inline] + pub fn set_resizable(&self, resizable: bool) { + self.ivars().resizable.set(resizable); + let fullscreen = self.ivars().fullscreen.borrow().is_some(); + if !fullscreen { + let mut mask = self.window().styleMask(); + if resizable { + mask |= NSWindowStyleMask::Resizable; + } else { + mask &= !NSWindowStyleMask::Resizable; + } + self.set_style_mask(mask); + } + // Otherwise, we don't change the mask until we exit fullscreen. + } + + #[inline] + pub fn is_resizable(&self) -> bool { + self.window().isResizable() + } + + #[inline] + pub fn set_enabled_buttons(&self, buttons: WindowButtons) { + let mut mask = self.window().styleMask(); + + if buttons.contains(WindowButtons::CLOSE) { + mask |= NSWindowStyleMask::Closable; + } else { + mask &= !NSWindowStyleMask::Closable; + } + + if buttons.contains(WindowButtons::MINIMIZE) { + mask |= NSWindowStyleMask::Miniaturizable; + } else { + mask &= !NSWindowStyleMask::Miniaturizable; + } + + // This must happen before the button's "enabled" status has been set, + // hence we do it synchronously. + self.set_style_mask(mask); + + // We edit the button directly instead of using `NSResizableWindowMask`, + // since that mask also affect the resizability of the window (which is + // controllable by other means in `winit`). + if let Some(button) = self + .window() + .standardWindowButton(NSWindowButton::NSWindowZoomButton) + { + button.setEnabled(buttons.contains(WindowButtons::MAXIMIZE)); + } + } + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + let mut buttons = WindowButtons::empty(); + if self.window().isMiniaturizable() { + buttons |= WindowButtons::MINIMIZE; + } + if self + .window() + .standardWindowButton(NSWindowButton::NSWindowZoomButton) + .map(|b| b.isEnabled()) + .unwrap_or(true) + { + buttons |= WindowButtons::MAXIMIZE; + } + if self.window().hasCloseBox() { + buttons |= WindowButtons::CLOSE; + } + buttons + } + + pub fn set_cursor(&self, cursor: Cursor) { + let view = self.view(); + + let cursor = match cursor { + Cursor::Icon(icon) => cursor_from_icon(icon), + Cursor::Custom(cursor) => cursor.inner.0, + }; + + if view.cursor_icon() == cursor { + return; + } + + view.set_cursor_icon(cursor); + self.window().invalidateCursorRectsForView(&view); + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let associate_mouse_cursor = match mode { + CursorGrabMode::Locked => false, + CursorGrabMode::None => true, + CursorGrabMode::Confined => { + return Err(ExternalError::NotSupported(NotSupportedError::new())) + } + }; + + // TODO: Do this for real https://stackoverflow.com/a/40922095/5435443 + CGDisplay::associate_mouse_and_mouse_cursor_position(associate_mouse_cursor) + .map_err(|status| ExternalError::Os(os_error!(OsError::CGError(status)))) + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + let view = self.view(); + let state_changed = view.set_cursor_visible(visible); + if state_changed { + self.window().invalidateCursorRectsForView(&view); + } + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.window().backingScaleFactor() as _ + } + + #[inline] + pub fn set_cursor_position( + &self, + cursor_position: Position, + ) -> Result<(), ExternalError> { + let physical_window_position = self.inner_position().unwrap(); + let scale_factor = self.scale_factor(); + let window_position = + physical_window_position.to_logical::(scale_factor); + let logical_cursor_position = cursor_position.to_logical::(scale_factor); + let point = CGPoint { + x: logical_cursor_position.x + window_position.x, + y: logical_cursor_position.y + window_position.y, + }; + CGDisplay::warp_mouse_cursor_position(point) + .map_err(|e| ExternalError::Os(os_error!(OsError::CGError(e))))?; + CGDisplay::associate_mouse_and_mouse_cursor_position(true) + .map_err(|e| ExternalError::Os(os_error!(OsError::CGError(e))))?; + + Ok(()) + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + let mtm = MainThreadMarker::from(self); + let event = NSApplication::sharedApplication(mtm) + .currentEvent() + .unwrap(); + self.window().performWindowDragWithEvent(&event); + Ok(()) + } + + #[inline] + pub fn drag_resize_window( + &self, + _direction: ResizeDirection, + ) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + self.window().setIgnoresMouseEvents(!hittest); + Ok(()) + } + + pub(crate) fn is_zoomed(&self) -> bool { + // because `isZoomed` doesn't work if the window's borderless, + // we make it resizable temporarily. + let curr_mask = self.window().styleMask(); + + let required = NSWindowStyleMask::Titled | NSWindowStyleMask::Resizable; + let needs_temp_mask = !curr_mask.contains(required); + if needs_temp_mask { + self.set_style_mask(required); + } + + let is_zoomed = self.window().isZoomed(); + + // Roll back temp styles + if needs_temp_mask { + self.set_style_mask(curr_mask); + } + + is_zoomed + } + + fn saved_style(&self) -> NSWindowStyleMask { + let base_mask = self + .ivars() + .saved_style + .take() + .unwrap_or_else(|| self.window().styleMask()); + if self.ivars().resizable.get() { + base_mask | NSWindowStyleMask::Resizable + } else { + base_mask & !NSWindowStyleMask::Resizable + } + } + + /// This is called when the window is exiting fullscreen, whether by the + /// user clicking on the green fullscreen button or programmatically by + /// `toggleFullScreen:` + pub(crate) fn restore_state_from_fullscreen(&self) { + self.ivars().fullscreen.replace(None); + + let maximized = self.ivars().maximized.get(); + let mask = self.saved_style(); + + self.set_style_mask(mask); + self.set_maximized(maximized); + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + let is_minimized = self.window().isMiniaturized(); + if is_minimized == minimized { + return; + } + + if minimized { + self.window().miniaturize(Some(self)); + } else { + unsafe { self.window().deminiaturize(Some(self)) }; + } + } + + #[inline] + pub fn is_minimized(&self) -> Option { + Some(self.window().isMiniaturized()) + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + let mtm = MainThreadMarker::from(self); + let is_zoomed = self.is_zoomed(); + if is_zoomed == maximized { + return; + }; + + // Save the standard frame sized if it is not zoomed + if !is_zoomed { + self.ivars().standard_frame.set(Some(self.window().frame())); + } + + self.ivars().maximized.set(maximized); + + if self.ivars().fullscreen.borrow().is_some() { + // Handle it in window_did_exit_fullscreen + return; + } + + if self + .window() + .styleMask() + .contains(NSWindowStyleMask::Resizable) + { + // Just use the native zoom if resizable + self.window().zoom(None); + } else { + // if it's not resizable, we set the frame directly + let new_rect = if maximized { + let screen = NSScreen::mainScreen(mtm).expect("no screen found"); + screen.visibleFrame() + } else { + self.ivars() + .standard_frame + .get() + .unwrap_or(DEFAULT_STANDARD_FRAME) + }; + self.window().setFrame_display(new_rect, false); + } + } + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + self.ivars().fullscreen.borrow().clone() + } + + #[inline] + pub fn is_maximized(&self) -> bool { + self.is_zoomed() + } + + #[inline] + pub(crate) fn set_fullscreen(&self, fullscreen: Option) { + let mtm = MainThreadMarker::from(self); + let app = NSApplication::sharedApplication(mtm); + + if self.ivars().is_simple_fullscreen.get() { + return; + } + if self.ivars().in_fullscreen_transition.get() { + // We can't set fullscreen here. + // Set fullscreen after transition. + self.ivars().target_fullscreen.replace(Some(fullscreen)); + return; + } + let old_fullscreen = self.ivars().fullscreen.borrow().clone(); + if fullscreen == old_fullscreen { + return; + } + + // If the fullscreen is on a different monitor, we must move the window + // to that monitor before we toggle fullscreen (as `toggleFullScreen` + // does not take a screen parameter, but uses the current screen) + if let Some(ref fullscreen) = fullscreen { + let new_screen = match fullscreen { + Fullscreen::Borderless(Some(monitor)) => monitor.clone(), + Fullscreen::Borderless(None) => { + if let Some(monitor) = self.current_monitor_inner() { + monitor + } else { + return; + } + } + Fullscreen::Exclusive(video_mode) => video_mode.monitor(), + } + .ns_screen(mtm) + .unwrap(); + + let old_screen = self.window().screen().unwrap(); + if old_screen != new_screen { + unsafe { self.window().setFrameOrigin(new_screen.frame().origin) }; + } + } + + if let Some(Fullscreen::Exclusive(ref video_mode)) = fullscreen { + // Note: `enterFullScreenMode:withOptions:` seems to do the exact + // same thing as we're doing here (captures the display, sets the + // video mode, and hides the menu bar and dock), with the exception + // of that I couldn't figure out how to set the display mode with + // it. I think `enterFullScreenMode:withOptions:` is still using the + // older display mode API where display modes were of the type + // `CFDictionary`, but this has changed, so we can't obtain the + // correct parameter for this any longer. Apple's code samples for + // this function seem to just pass in "YES" for the display mode + // parameter, which is not consistent with the docs saying that it + // takes a `NSDictionary`.. + + let display_id = video_mode.monitor().native_identifier(); + + let mut fade_token = ffi::kCGDisplayFadeReservationInvalidToken; + + if matches!(old_fullscreen, Some(Fullscreen::Borderless(_))) { + self.ivars() + .save_presentation_opts + .replace(Some(app.presentationOptions())); + } + + unsafe { + // Fade to black (and wait for the fade to complete) to hide the + // flicker from capturing the display and switching display mode + if ffi::CGAcquireDisplayFadeReservation(5.0, &mut fade_token) + == ffi::kCGErrorSuccess + { + ffi::CGDisplayFade( + fade_token, + 0.3, + ffi::kCGDisplayBlendNormal, + ffi::kCGDisplayBlendSolidColor, + 0.0, + 0.0, + 0.0, + ffi::TRUE, + ); + } + + assert_eq!(ffi::CGDisplayCapture(display_id), ffi::kCGErrorSuccess); + } + + unsafe { + let result = ffi::CGDisplaySetDisplayMode( + display_id, + video_mode.native_mode.0, + std::ptr::null(), + ); + assert!(result == ffi::kCGErrorSuccess, "failed to set video mode"); + + // After the display has been configured, fade back in + // asynchronously + if fade_token != ffi::kCGDisplayFadeReservationInvalidToken { + ffi::CGDisplayFade( + fade_token, + 0.6, + ffi::kCGDisplayBlendSolidColor, + ffi::kCGDisplayBlendNormal, + 0.0, + 0.0, + 0.0, + ffi::FALSE, + ); + ffi::CGReleaseDisplayFadeReservation(fade_token); + } + } + } + + self.ivars().fullscreen.replace(fullscreen.clone()); + + fn toggle_fullscreen(window: &WinitWindow) { + // Window level must be restored from `CGShieldingWindowLevel() + // + 1` back to normal in order for `toggleFullScreen` to do + // anything + window.setLevel(ffi::kCGNormalWindowLevel as NSWindowLevel); + window.toggleFullScreen(None); + } + + match (old_fullscreen, fullscreen) { + (None, Some(_)) => { + // `toggleFullScreen` doesn't work if the `StyleMask` is none, so we + // set a normal style temporarily. The previous state will be + // restored in `WindowDelegate::window_did_exit_fullscreen`. + let curr_mask = self.window().styleMask(); + let required = NSWindowStyleMask::Titled | NSWindowStyleMask::Resizable; + if !curr_mask.contains(required) { + self.set_style_mask(required); + self.ivars().saved_style.set(Some(curr_mask)); + } + toggle_fullscreen(self.window()); + } + (Some(Fullscreen::Borderless(_)), None) => { + // State is restored by `window_did_exit_fullscreen` + toggle_fullscreen(self.window()); + } + (Some(Fullscreen::Exclusive(ref video_mode)), None) => { + restore_and_release_display(&video_mode.monitor()); + toggle_fullscreen(self.window()); + } + (Some(Fullscreen::Borderless(_)), Some(Fullscreen::Exclusive(_))) => { + // If we're already in fullscreen mode, calling + // `CGDisplayCapture` will place the shielding window on top of + // our window, which results in a black display and is not what + // we want. So, we must place our window on top of the shielding + // window. Unfortunately, this also makes our window be on top + // of the menu bar, and this looks broken, so we must make sure + // that the menu bar is disabled. This is done in the window + // delegate in `window:willUseFullScreenPresentationOptions:`. + self.ivars() + .save_presentation_opts + .set(Some(app.presentationOptions())); + + let presentation_options = + NSApplicationPresentationOptions::NSApplicationPresentationFullScreen + | NSApplicationPresentationOptions::NSApplicationPresentationHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar; + app.setPresentationOptions(presentation_options); + + let window_level = + unsafe { ffi::CGShieldingWindowLevel() } as NSWindowLevel + 1; + self.window().setLevel(window_level); + } + ( + Some(Fullscreen::Exclusive(ref video_mode)), + Some(Fullscreen::Borderless(_)), + ) => { + let presentation_options = self.ivars().save_presentation_opts.get().unwrap_or( + NSApplicationPresentationOptions::NSApplicationPresentationFullScreen + | NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar + ); + app.setPresentationOptions(presentation_options); + + restore_and_release_display(&video_mode.monitor()); + + // Restore the normal window level following the Borderless fullscreen + // `CGShieldingWindowLevel() + 1` hack. + self.window() + .setLevel(ffi::kCGNormalWindowLevel as NSWindowLevel); + } + _ => {} + }; + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + if decorations == self.ivars().decorations.get() { + return; + } + + self.ivars().decorations.set(decorations); + + let fullscreen = self.ivars().fullscreen.borrow().is_some(); + let resizable = self.ivars().resizable.get(); + + // If we're in fullscreen mode, we wait to apply decoration changes + // until we're in `window_did_exit_fullscreen`. + if fullscreen { + return; + } + + let new_mask = { + let mut new_mask = if decorations { + NSWindowStyleMask::Closable + | NSWindowStyleMask::Miniaturizable + | NSWindowStyleMask::Resizable + | NSWindowStyleMask::Titled + } else { + NSWindowStyleMask::Borderless | NSWindowStyleMask::Resizable + }; + if !resizable { + new_mask &= !NSWindowStyleMask::Resizable; + } + new_mask + }; + self.set_style_mask(new_mask); + } + + #[inline] + pub fn is_decorated(&self) -> bool { + self.ivars().decorations.get() + } + + #[inline] + pub fn set_window_level(&self, level: WindowLevel) { + let level = match level { + WindowLevel::AlwaysOnTop => ffi::kCGFloatingWindowLevel as NSWindowLevel, + WindowLevel::AlwaysOnBottom => { + (ffi::kCGNormalWindowLevel - 1) as NSWindowLevel + } + WindowLevel::Normal => ffi::kCGNormalWindowLevel as NSWindowLevel, + }; + self.window().setLevel(level); + } + + #[inline] + pub fn set_window_icon(&self, _icon: Option) { + // macOS doesn't have window icons. Though, there is + // `setRepresentedFilename`, but that's semantically distinct and should + // only be used when the window is in some way representing a specific + // file/directory. For instance, Terminal.app uses this for the CWD. + // Anyway, that should eventually be implemented as + // `WindowAttributesExt::with_represented_file` or something, and doesn't + // have anything to do with `set_window_icon`. + // https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/WinPanel/Tasks/SettingWindowTitle.html + } + + #[inline] + pub fn set_ime_cursor_area(&self, spot: Position, size: Size) { + let scale_factor = self.scale_factor(); + let logical_spot = spot.to_logical(scale_factor); + let logical_spot = NSPoint::new(logical_spot.x, logical_spot.y); + + let size = size.to_logical(scale_factor); + let size = NSSize::new(size.width, size.height); + + self.view().set_ime_cursor_area(logical_spot, size); + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + self.view().set_ime_allowed(allowed); + } + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + + #[inline] + pub fn focus_window(&self) { + let mtm = MainThreadMarker::from(self); + let is_minimized = self.window().isMiniaturized(); + let is_visible = self.window().isVisible(); + + if !is_minimized && is_visible { + #[allow(deprecated)] + NSApplication::sharedApplication(mtm).activateIgnoringOtherApps(true); + self.window().makeKeyAndOrderFront(None); + } + } + + #[inline] + pub fn request_user_attention(&self, request_type: Option) { + let mtm = MainThreadMarker::from(self); + let ns_request_type = request_type.map(|ty| match ty { + UserAttentionType::Critical => NSRequestUserAttentionType::NSCriticalRequest, + UserAttentionType::Informational => { + NSRequestUserAttentionType::NSInformationalRequest + } + }); + if let Some(ty) = ns_request_type { + NSApplication::sharedApplication(mtm).requestUserAttention(ty); + } + } + + #[inline] + // Allow directly accessing the current monitor internally without unwrapping. + pub(crate) fn current_monitor_inner(&self) -> Option { + let display_id = get_display_id(&*self.window().screen()?); + Some(MonitorHandle::new(display_id)) + } + + #[inline] + pub fn current_monitor(&self) -> Option { + self.current_monitor_inner() + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + monitor::available_monitors() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + let monitor = monitor::primary_monitor(); + Some(monitor) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> rwh_06::RawWindowHandle { + let window_handle = rwh_06::AppKitWindowHandle::new({ + let ptr = Retained::as_ptr(&self.view()) as *mut _; + std::ptr::NonNull::new(ptr).expect("Retained should never be null") + }); + rwh_06::RawWindowHandle::AppKit(window_handle) + } + + fn toggle_style_mask(&self, mask: NSWindowStyleMask, on: bool) { + let current_style_mask = self.window().styleMask(); + if on { + self.set_style_mask(current_style_mask | mask); + } else { + self.set_style_mask(current_style_mask & !mask); + } + } + + #[inline] + pub fn theme(&self) -> Option { + self.ivars().current_theme.get() + } + + #[inline] + pub fn has_focus(&self) -> bool { + self.window().isKeyWindow() + } + + pub fn set_theme(&self, theme: Option) { + let mtm = MainThreadMarker::from(self); + set_ns_theme(theme, mtm); + self.ivars() + .current_theme + .set(theme.or_else(|| Some(get_ns_theme(mtm)))); + } + + #[inline] + pub fn set_content_protected(&self, protected: bool) { + self.window().setSharingType(if protected { + NSWindowSharingType::NSWindowSharingNone + } else { + NSWindowSharingType::NSWindowSharingReadOnly + }) + } + + pub fn title(&self) -> String { + self.window().title().to_string() + } + + pub fn reset_dead_keys(&self) { + // (Artur) I couldn't find a way to implement this. + } +} + +fn restore_and_release_display(monitor: &MonitorHandle) { + let available_monitors = monitor::available_monitors(); + if available_monitors.contains(monitor) { + unsafe { + ffi::CGRestorePermanentDisplayConfiguration(); + assert_eq!( + ffi::CGDisplayRelease(monitor.native_identifier()), + ffi::kCGErrorSuccess + ); + }; + } else { + tracing::warn!( + monitor = monitor.name(), + "Tried to restore exclusive fullscreen on a monitor that is no longer available" + ); + } +} + +impl WindowExtMacOS for WindowDelegate { + #[inline] + fn simple_fullscreen(&self) -> bool { + self.ivars().is_simple_fullscreen.get() + } + + #[inline] + fn set_simple_fullscreen(&self, fullscreen: bool) -> bool { + let mtm = MainThreadMarker::from(self); + + let app = NSApplication::sharedApplication(mtm); + let is_native_fullscreen = self.ivars().fullscreen.borrow().is_some(); + let is_simple_fullscreen = self.ivars().is_simple_fullscreen.get(); + + // Do nothing if native fullscreen is active. + if is_native_fullscreen + || (fullscreen && is_simple_fullscreen) + || (!fullscreen && !is_simple_fullscreen) + { + return false; + } + + if fullscreen { + // Remember the original window's settings + // Exclude title bar + self.ivars().standard_frame.set(Some( + self.window().contentRectForFrameRect(self.window().frame()), + )); + self.ivars() + .saved_style + .set(Some(self.window().styleMask())); + self.ivars() + .save_presentation_opts + .set(Some(app.presentationOptions())); + + // Tell our window's state that we're in fullscreen + self.ivars().is_simple_fullscreen.set(true); + + // Simulate pre-Lion fullscreen by hiding the dock and menu bar + let presentation_options = + NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar; + app.setPresentationOptions(presentation_options); + + // Hide the titlebar + self.toggle_style_mask(NSWindowStyleMask::Titled, false); + + // Set the window frame to the screen frame size + let screen = self + .window() + .screen() + .expect("expected screen to be available"); + self.window().setFrame_display(screen.frame(), true); + + // Fullscreen windows can't be resized, minimized, or moved + self.toggle_style_mask(NSWindowStyleMask::Miniaturizable, false); + self.toggle_style_mask(NSWindowStyleMask::Resizable, false); + self.window().setMovable(false); + + true + } else { + let new_mask = self.saved_style(); + self.set_style_mask(new_mask); + self.ivars().is_simple_fullscreen.set(false); + + let save_presentation_opts = self.ivars().save_presentation_opts.get(); + let frame = self + .ivars() + .standard_frame + .get() + .unwrap_or(DEFAULT_STANDARD_FRAME); + + if let Some(presentation_opts) = save_presentation_opts { + app.setPresentationOptions(presentation_opts); + } + + self.window().setFrame_display(frame, true); + self.window().setMovable(true); + + true + } + } + + #[inline] + fn has_shadow(&self) -> bool { + self.window().hasShadow() + } + + #[inline] + fn set_has_shadow(&self, has_shadow: bool) { + self.window().setHasShadow(has_shadow) + } + + #[inline] + fn set_tabbing_identifier(&self, identifier: &str) { + self.window() + .setTabbingIdentifier(&NSString::from_str(identifier)) + } + + #[inline] + fn tabbing_identifier(&self) -> String { + self.window().tabbingIdentifier().to_string() + } + + #[inline] + fn select_next_tab(&self) { + self.window().selectNextTab(None) + } + + #[inline] + fn select_previous_tab(&self) { + unsafe { self.window().selectPreviousTab(None) } + } + + #[inline] + fn select_tab_at_index(&self, index: usize) { + if let Some(group) = self.window().tabGroup() { + if let Some(windows) = unsafe { self.window().tabbedWindows() } { + if index < windows.len() { + group.setSelectedWindow(Some(&windows[index])); + } + } + } + } + + #[inline] + fn num_tabs(&self) -> usize { + unsafe { self.window().tabbedWindows() } + .map(|windows| windows.len()) + .unwrap_or(1) + } + + fn is_document_edited(&self) -> bool { + self.window().isDocumentEdited() + } + + fn set_document_edited(&self, edited: bool) { + self.window().setDocumentEdited(edited) + } + + fn set_option_as_alt(&self, option_as_alt: OptionAsAlt) { + self.view().set_option_as_alt(option_as_alt); + } + + fn option_as_alt(&self) -> OptionAsAlt { + self.view().option_as_alt() + } +} + +const DEFAULT_STANDARD_FRAME: NSRect = + NSRect::new(NSPoint::new(50.0, 50.0), NSSize::new(800.0, 600.0)); + +pub(super) fn get_ns_theme(mtm: MainThreadMarker) -> Theme { + let app = NSApplication::sharedApplication(mtm); + if !app.respondsToSelector(sel!(effectiveAppearance)) { + return Theme::Light; + } + let appearance = app.effectiveAppearance(); + let name = appearance + .bestMatchFromAppearancesWithNames(&NSArray::from_id_slice(&[ + NSString::from_str("NSAppearanceNameAqua"), + NSString::from_str("NSAppearanceNameDarkAqua"), + ])) + .unwrap(); + match &*name.to_string() { + "NSAppearanceNameDarkAqua" => Theme::Dark, + _ => Theme::Light, + } +} + +fn set_ns_theme(theme: Option, mtm: MainThreadMarker) { + let app = NSApplication::sharedApplication(mtm); + if app.respondsToSelector(sel!(effectiveAppearance)) { + let appearance = theme.map(|t| { + let name = match t { + Theme::Dark => NSString::from_str("NSAppearanceNameDarkAqua"), + Theme::Light => NSString::from_str("NSAppearanceNameAqua"), + }; + NSAppearance::appearanceNamed(&name).unwrap() + }); + app.setAppearance(appearance.as_ref().map(|a| a.as_ref())); + } +} diff --git a/rio-window/src/platform_impl/mod.rs b/rio-window/src/platform_impl/mod.rs new file mode 100644 index 00000000..c9369913 --- /dev/null +++ b/rio-window/src/platform_impl/mod.rs @@ -0,0 +1,70 @@ +use crate::monitor::{ + MonitorHandle as RootMonitorHandle, VideoModeHandle as RootVideoModeHandle, +}; +use crate::window::Fullscreen as RootFullscreen; + +#[cfg(windows_platform)] +#[path = "windows/mod.rs"] +mod platform; +#[cfg(any(x11_platform, wayland_platform))] +#[path = "linux/mod.rs"] +mod platform; +#[cfg(macos_platform)] +#[path = "macos/mod.rs"] +mod platform; +#[cfg(ios_platform)] +#[path = "ios/mod.rs"] +mod platform; +#[cfg(web_platform)] +#[path = "web/mod.rs"] +mod platform; +#[cfg(orbital_platform)] +#[path = "orbital/mod.rs"] +mod platform; + +pub use self::platform::*; + +/// Helper for converting between platform-specific and generic +/// [`VideoModeHandle`]/[`MonitorHandle`] +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum Fullscreen { + Exclusive(VideoModeHandle), + Borderless(Option), +} + +impl From for Fullscreen { + fn from(f: RootFullscreen) -> Self { + match f { + RootFullscreen::Exclusive(mode) => Self::Exclusive(mode.video_mode), + RootFullscreen::Borderless(Some(handle)) => { + Self::Borderless(Some(handle.inner)) + } + RootFullscreen::Borderless(None) => Self::Borderless(None), + } + } +} + +impl From for RootFullscreen { + fn from(f: Fullscreen) -> Self { + match f { + Fullscreen::Exclusive(video_mode) => { + Self::Exclusive(RootVideoModeHandle { video_mode }) + } + Fullscreen::Borderless(Some(inner)) => { + Self::Borderless(Some(RootMonitorHandle { inner })) + } + Fullscreen::Borderless(None) => Self::Borderless(None), + } + } +} + +#[cfg(all( + not(ios_platform), + not(windows_platform), + not(macos_platform), + not(x11_platform), + not(wayland_platform), + not(web_platform), + not(orbital_platform), +))] +compile_error!("The platform you're compiling for is not supported by winit"); diff --git a/rio-window/src/platform_impl/orbital/event_loop.rs b/rio-window/src/platform_impl/orbital/event_loop.rs new file mode 100644 index 00000000..a47f5483 --- /dev/null +++ b/rio-window/src/platform_impl/orbital/event_loop.rs @@ -0,0 +1,894 @@ +use std::cell::Cell; +use std::collections::VecDeque; +use std::marker::PhantomData; +use std::sync::{mpsc, Arc, Mutex}; +use std::time::Instant; +use std::{mem, slice}; + +use bitflags::bitflags; +use orbclient::{ + ButtonEvent, EventOption, FocusEvent, HoverEvent, KeyEvent, MouseEvent, + MouseRelativeEvent, MoveEvent, QuitEvent, ResizeEvent, ScrollEvent, TextInputEvent, +}; +use smol_str::SmolStr; + +use crate::error::EventLoopError; +use crate::event::{self, Ime, Modifiers, StartCause}; +use crate::event_loop::{self, ControlFlow, DeviceEvents}; +use crate::keyboard::{ + Key, KeyCode, KeyLocation, ModifiersKeys, ModifiersState, NamedKey, NativeKey, + NativeKeyCode, PhysicalKey, +}; +use crate::window::{ + CustomCursor as RootCustomCursor, CustomCursorSource, WindowId as RootWindowId, +}; + +use super::{ + DeviceId, KeyEventExtra, MonitorHandle, OsError, PlatformSpecificEventLoopAttributes, + RedoxSocket, TimeSocket, WindowId, WindowProperties, +}; + +fn convert_scancode(scancode: u8) -> (PhysicalKey, Option) { + // Key constants from https://docs.rs/orbclient/latest/orbclient/event/index.html + let (key_code, named_key_opt) = match scancode { + orbclient::K_A => (KeyCode::KeyA, None), + orbclient::K_B => (KeyCode::KeyB, None), + orbclient::K_C => (KeyCode::KeyC, None), + orbclient::K_D => (KeyCode::KeyD, None), + orbclient::K_E => (KeyCode::KeyE, None), + orbclient::K_F => (KeyCode::KeyF, None), + orbclient::K_G => (KeyCode::KeyG, None), + orbclient::K_H => (KeyCode::KeyH, None), + orbclient::K_I => (KeyCode::KeyI, None), + orbclient::K_J => (KeyCode::KeyJ, None), + orbclient::K_K => (KeyCode::KeyK, None), + orbclient::K_L => (KeyCode::KeyL, None), + orbclient::K_M => (KeyCode::KeyM, None), + orbclient::K_N => (KeyCode::KeyN, None), + orbclient::K_O => (KeyCode::KeyO, None), + orbclient::K_P => (KeyCode::KeyP, None), + orbclient::K_Q => (KeyCode::KeyQ, None), + orbclient::K_R => (KeyCode::KeyR, None), + orbclient::K_S => (KeyCode::KeyS, None), + orbclient::K_T => (KeyCode::KeyT, None), + orbclient::K_U => (KeyCode::KeyU, None), + orbclient::K_V => (KeyCode::KeyV, None), + orbclient::K_W => (KeyCode::KeyW, None), + orbclient::K_X => (KeyCode::KeyX, None), + orbclient::K_Y => (KeyCode::KeyY, None), + orbclient::K_Z => (KeyCode::KeyZ, None), + orbclient::K_0 => (KeyCode::Digit0, None), + orbclient::K_1 => (KeyCode::Digit1, None), + orbclient::K_2 => (KeyCode::Digit2, None), + orbclient::K_3 => (KeyCode::Digit3, None), + orbclient::K_4 => (KeyCode::Digit4, None), + orbclient::K_5 => (KeyCode::Digit5, None), + orbclient::K_6 => (KeyCode::Digit6, None), + orbclient::K_7 => (KeyCode::Digit7, None), + orbclient::K_8 => (KeyCode::Digit8, None), + orbclient::K_9 => (KeyCode::Digit9, None), + + orbclient::K_ALT => (KeyCode::AltLeft, Some(NamedKey::Alt)), + orbclient::K_ALT_GR => (KeyCode::AltRight, Some(NamedKey::AltGraph)), + orbclient::K_BACKSLASH => (KeyCode::Backslash, None), + orbclient::K_BKSP => (KeyCode::Backspace, Some(NamedKey::Backspace)), + orbclient::K_BRACE_CLOSE => (KeyCode::BracketRight, None), + orbclient::K_BRACE_OPEN => (KeyCode::BracketLeft, None), + orbclient::K_CAPS => (KeyCode::CapsLock, Some(NamedKey::CapsLock)), + orbclient::K_COMMA => (KeyCode::Comma, None), + orbclient::K_CTRL => (KeyCode::ControlLeft, Some(NamedKey::Control)), + orbclient::K_DEL => (KeyCode::Delete, Some(NamedKey::Delete)), + orbclient::K_DOWN => (KeyCode::ArrowDown, Some(NamedKey::ArrowDown)), + orbclient::K_END => (KeyCode::End, Some(NamedKey::End)), + orbclient::K_ENTER => (KeyCode::Enter, Some(NamedKey::Enter)), + orbclient::K_EQUALS => (KeyCode::Equal, None), + orbclient::K_ESC => (KeyCode::Escape, Some(NamedKey::Escape)), + orbclient::K_F1 => (KeyCode::F1, Some(NamedKey::F1)), + orbclient::K_F2 => (KeyCode::F2, Some(NamedKey::F2)), + orbclient::K_F3 => (KeyCode::F3, Some(NamedKey::F3)), + orbclient::K_F4 => (KeyCode::F4, Some(NamedKey::F4)), + orbclient::K_F5 => (KeyCode::F5, Some(NamedKey::F5)), + orbclient::K_F6 => (KeyCode::F6, Some(NamedKey::F6)), + orbclient::K_F7 => (KeyCode::F7, Some(NamedKey::F7)), + orbclient::K_F8 => (KeyCode::F8, Some(NamedKey::F8)), + orbclient::K_F9 => (KeyCode::F9, Some(NamedKey::F9)), + orbclient::K_F10 => (KeyCode::F10, Some(NamedKey::F10)), + orbclient::K_F11 => (KeyCode::F11, Some(NamedKey::F11)), + orbclient::K_F12 => (KeyCode::F12, Some(NamedKey::F12)), + orbclient::K_HOME => (KeyCode::Home, Some(NamedKey::Home)), + orbclient::K_LEFT => (KeyCode::ArrowLeft, Some(NamedKey::ArrowLeft)), + orbclient::K_LEFT_SHIFT => (KeyCode::ShiftLeft, Some(NamedKey::Shift)), + orbclient::K_MINUS => (KeyCode::Minus, None), + orbclient::K_NUM_0 => (KeyCode::Numpad0, None), + orbclient::K_NUM_1 => (KeyCode::Numpad1, None), + orbclient::K_NUM_2 => (KeyCode::Numpad2, None), + orbclient::K_NUM_3 => (KeyCode::Numpad3, None), + orbclient::K_NUM_4 => (KeyCode::Numpad4, None), + orbclient::K_NUM_5 => (KeyCode::Numpad5, None), + orbclient::K_NUM_6 => (KeyCode::Numpad6, None), + orbclient::K_NUM_7 => (KeyCode::Numpad7, None), + orbclient::K_NUM_8 => (KeyCode::Numpad8, None), + orbclient::K_NUM_9 => (KeyCode::Numpad9, None), + orbclient::K_PERIOD => (KeyCode::Period, None), + orbclient::K_PGDN => (KeyCode::PageDown, Some(NamedKey::PageDown)), + orbclient::K_PGUP => (KeyCode::PageUp, Some(NamedKey::PageUp)), + orbclient::K_QUOTE => (KeyCode::Quote, None), + orbclient::K_RIGHT => (KeyCode::ArrowRight, Some(NamedKey::ArrowRight)), + orbclient::K_RIGHT_SHIFT => (KeyCode::ShiftRight, Some(NamedKey::Shift)), + orbclient::K_SEMICOLON => (KeyCode::Semicolon, None), + orbclient::K_SLASH => (KeyCode::Slash, None), + orbclient::K_SPACE => (KeyCode::Space, Some(NamedKey::Space)), + orbclient::K_SUPER => (KeyCode::SuperLeft, Some(NamedKey::Super)), + orbclient::K_TAB => (KeyCode::Tab, Some(NamedKey::Tab)), + orbclient::K_TICK => (KeyCode::Backquote, None), + orbclient::K_UP => (KeyCode::ArrowUp, Some(NamedKey::ArrowUp)), + orbclient::K_VOLUME_DOWN => { + (KeyCode::AudioVolumeDown, Some(NamedKey::AudioVolumeDown)) + } + orbclient::K_VOLUME_TOGGLE => { + (KeyCode::AudioVolumeMute, Some(NamedKey::AudioVolumeMute)) + } + orbclient::K_VOLUME_UP => (KeyCode::AudioVolumeUp, Some(NamedKey::AudioVolumeUp)), + + _ => return (PhysicalKey::Unidentified(NativeKeyCode::Unidentified), None), + }; + (PhysicalKey::Code(key_code), named_key_opt) +} + +fn element_state(pressed: bool) -> event::ElementState { + if pressed { + event::ElementState::Pressed + } else { + event::ElementState::Released + } +} + +bitflags! { + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + struct KeyboardModifierState: u8 { + const LSHIFT = 1 << 0; + const RSHIFT = 1 << 1; + const LCTRL = 1 << 2; + const RCTRL = 1 << 3; + const LALT = 1 << 4; + const RALT = 1 << 5; + const LSUPER = 1 << 6; + const RSUPER = 1 << 7; + } +} + +bitflags! { + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + struct MouseButtonState: u8 { + const LEFT = 1 << 0; + const MIDDLE = 1 << 1; + const RIGHT = 1 << 2; + } +} + +#[derive(Default)] +struct EventState { + keyboard: KeyboardModifierState, + mouse: MouseButtonState, + resize_opt: Option<(u32, u32)>, +} + +impl EventState { + fn character_all_modifiers(&self, character: char) -> char { + // Modify character if Ctrl is pressed + #[allow(clippy::collapsible_if)] + if self.keyboard.contains(KeyboardModifierState::LCTRL) + || self.keyboard.contains(KeyboardModifierState::RCTRL) + { + if character.is_ascii_lowercase() { + return ((character as u8 - b'a') + 1) as char; + } + // TODO: more control key variants? + } + + // Return character as-is if no special handling required + character + } + + fn key(&mut self, key: PhysicalKey, pressed: bool) { + let code = match key { + PhysicalKey::Code(code) => code, + _ => return, + }; + + match code { + KeyCode::ShiftLeft => { + self.keyboard.set(KeyboardModifierState::LSHIFT, pressed) + } + KeyCode::ShiftRight => { + self.keyboard.set(KeyboardModifierState::RSHIFT, pressed) + } + KeyCode::ControlLeft => { + self.keyboard.set(KeyboardModifierState::LCTRL, pressed) + } + KeyCode::ControlRight => { + self.keyboard.set(KeyboardModifierState::RCTRL, pressed) + } + KeyCode::AltLeft => self.keyboard.set(KeyboardModifierState::LALT, pressed), + KeyCode::AltRight => self.keyboard.set(KeyboardModifierState::RALT, pressed), + KeyCode::SuperLeft => { + self.keyboard.set(KeyboardModifierState::LSUPER, pressed) + } + KeyCode::SuperRight => { + self.keyboard.set(KeyboardModifierState::RSUPER, pressed) + } + _ => (), + } + } + + fn mouse( + &mut self, + left: bool, + middle: bool, + right: bool, + ) -> Option<(event::MouseButton, event::ElementState)> { + if self.mouse.contains(MouseButtonState::LEFT) != left { + self.mouse.set(MouseButtonState::LEFT, left); + return Some((event::MouseButton::Left, element_state(left))); + } + + if self.mouse.contains(MouseButtonState::MIDDLE) != middle { + self.mouse.set(MouseButtonState::MIDDLE, middle); + return Some((event::MouseButton::Middle, element_state(middle))); + } + + if self.mouse.contains(MouseButtonState::RIGHT) != right { + self.mouse.set(MouseButtonState::RIGHT, right); + return Some((event::MouseButton::Right, element_state(right))); + } + + None + } + + fn modifiers(&self) -> Modifiers { + let mut state = ModifiersState::empty(); + let mut pressed_mods = ModifiersKeys::empty(); + + if self + .keyboard + .intersects(KeyboardModifierState::LSHIFT | KeyboardModifierState::RSHIFT) + { + state |= ModifiersState::SHIFT; + } + + pressed_mods.set( + ModifiersKeys::LSHIFT, + self.keyboard.contains(KeyboardModifierState::LSHIFT), + ); + pressed_mods.set( + ModifiersKeys::RSHIFT, + self.keyboard.contains(KeyboardModifierState::RSHIFT), + ); + + if self + .keyboard + .intersects(KeyboardModifierState::LCTRL | KeyboardModifierState::RCTRL) + { + state |= ModifiersState::CONTROL; + } + + pressed_mods.set( + ModifiersKeys::LCONTROL, + self.keyboard.contains(KeyboardModifierState::LCTRL), + ); + pressed_mods.set( + ModifiersKeys::RCONTROL, + self.keyboard.contains(KeyboardModifierState::RCTRL), + ); + + if self + .keyboard + .intersects(KeyboardModifierState::LALT | KeyboardModifierState::RALT) + { + state |= ModifiersState::ALT; + } + + pressed_mods.set( + ModifiersKeys::LALT, + self.keyboard.contains(KeyboardModifierState::LALT), + ); + pressed_mods.set( + ModifiersKeys::RALT, + self.keyboard.contains(KeyboardModifierState::RALT), + ); + + if self + .keyboard + .intersects(KeyboardModifierState::LSUPER | KeyboardModifierState::RSUPER) + { + state |= ModifiersState::SUPER + } + + pressed_mods.set( + ModifiersKeys::LSUPER, + self.keyboard.contains(KeyboardModifierState::LSUPER), + ); + pressed_mods.set( + ModifiersKeys::RSUPER, + self.keyboard.contains(KeyboardModifierState::RSUPER), + ); + + Modifiers { + state, + pressed_mods, + } + } +} + +pub struct EventLoop { + windows: Vec<(Arc, EventState)>, + window_target: event_loop::ActiveEventLoop, + user_events_sender: mpsc::Sender, + user_events_receiver: mpsc::Receiver, +} + +impl EventLoop { + pub(crate) fn new( + _: &PlatformSpecificEventLoopAttributes, + ) -> Result { + let (user_events_sender, user_events_receiver) = mpsc::channel(); + + let event_socket = Arc::new( + RedoxSocket::event() + .map_err(OsError::new) + .map_err(|error| EventLoopError::Os(os_error!(error)))?, + ); + + let wake_socket = Arc::new( + TimeSocket::open() + .map_err(OsError::new) + .map_err(|error| EventLoopError::Os(os_error!(error)))?, + ); + + event_socket + .write(&syscall::Event { + id: wake_socket.0.fd, + flags: syscall::EventFlags::EVENT_READ, + data: wake_socket.0.fd, + }) + .map_err(OsError::new) + .map_err(|error| EventLoopError::Os(os_error!(error)))?; + + Ok(Self { + windows: Vec::new(), + window_target: event_loop::ActiveEventLoop { + p: ActiveEventLoop { + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(false), + creates: Mutex::new(VecDeque::new()), + redraws: Arc::new(Mutex::new(VecDeque::new())), + destroys: Arc::new(Mutex::new(VecDeque::new())), + event_socket, + wake_socket, + }, + _marker: PhantomData, + }, + user_events_sender, + user_events_receiver, + }) + } + + fn process_event( + window_id: WindowId, + event_option: EventOption, + event_state: &mut EventState, + mut event_handler: F, + ) where + F: FnMut(event::Event), + { + match event_option { + EventOption::Key(KeyEvent { + character, + scancode, + pressed, + }) => { + // Convert scancode + let (physical_key, named_key_opt) = convert_scancode(scancode); + + // Get previous modifiers and update modifiers based on physical key + let modifiers_before = event_state.keyboard; + event_state.key(physical_key, pressed); + + // Default to unidentified key with no text + let mut logical_key = Key::Unidentified(NativeKey::Unidentified); + let mut key_without_modifiers = logical_key.clone(); + let mut text = None; + let mut text_with_all_modifiers = None; + + // Set key and text based on character + if character != '\0' { + let mut tmp = [0u8; 4]; + let character_str = character.encode_utf8(&mut tmp); + // The key with Shift and Caps Lock applied (but not Ctrl) + logical_key = Key::Character(character_str.into()); + // The key without Shift or Caps Lock applied + key_without_modifiers = + Key::Character(SmolStr::from_iter(character.to_lowercase())); + if pressed { + // The key with Shift and Caps Lock applied (but not Ctrl) + text = Some(character_str.into()); + // The key with Shift, Caps Lock, and Ctrl applied + let character_all_modifiers = + event_state.character_all_modifiers(character); + text_with_all_modifiers = + Some(character_all_modifiers.encode_utf8(&mut tmp).into()) + } + }; + + // Override key if a named key was found (this is to allow Enter to replace '\n') + if let Some(named_key) = named_key_opt { + logical_key = Key::Named(named_key); + key_without_modifiers = logical_key.clone(); + } + + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::KeyboardInput { + device_id: event::DeviceId(DeviceId), + event: event::KeyEvent { + logical_key, + physical_key, + location: KeyLocation::Standard, + state: element_state(pressed), + repeat: false, + text, + platform_specific: KeyEventExtra { + key_without_modifiers, + text_with_all_modifiers, + }, + }, + is_synthetic: false, + }, + }); + + // If the state of the modifiers has changed, send the event. + if modifiers_before != event_state.keyboard { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::ModifiersChanged( + event_state.modifiers(), + ), + }) + } + } + EventOption::TextInput(TextInputEvent { character }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Ime(Ime::Preedit("".into(), None)), + }); + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Ime(Ime::Commit(character.into())), + }); + } + EventOption::Mouse(MouseEvent { x, y }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::CursorMoved { + device_id: event::DeviceId(DeviceId), + position: (x, y).into(), + }, + }); + } + EventOption::MouseRelative(MouseRelativeEvent { dx, dy }) => { + event_handler(event::Event::DeviceEvent { + device_id: event::DeviceId(DeviceId), + event: event::DeviceEvent::MouseMotion { + delta: (dx as f64, dy as f64), + }, + }); + } + EventOption::Button(ButtonEvent { + left, + middle, + right, + }) => { + while let Some((button, state)) = event_state.mouse(left, middle, right) { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::MouseInput { + device_id: event::DeviceId(DeviceId), + state, + button, + }, + }); + } + } + EventOption::Scroll(ScrollEvent { x, y }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::MouseWheel { + device_id: event::DeviceId(DeviceId), + delta: event::MouseScrollDelta::LineDelta(x as f32, y as f32), + phase: event::TouchPhase::Moved, + }, + }); + } + EventOption::Quit(QuitEvent {}) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::CloseRequested, + }); + } + EventOption::Focus(FocusEvent { focused }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Focused(focused), + }); + } + EventOption::Move(MoveEvent { x, y }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Moved((x, y).into()), + }); + } + EventOption::Resize(ResizeEvent { width, height }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Resized((width, height).into()), + }); + + // Acknowledge resize after event loop. + event_state.resize_opt = Some((width, height)); + } + // TODO: Screen, Clipboard, Drop + EventOption::Hover(HoverEvent { entered }) => { + if entered { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::CursorEntered { + device_id: event::DeviceId(DeviceId), + }, + }); + } else { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::CursorLeft { + device_id: event::DeviceId(DeviceId), + }, + }); + } + } + other => { + tracing::warn!("unhandled event: {:?}", other); + } + } + } + + pub fn run(mut self, mut event_handler_inner: F) -> Result<(), EventLoopError> + where + F: FnMut(event::Event, &event_loop::ActiveEventLoop), + { + let mut event_handler = + move |event: event::Event, window_target: &event_loop::ActiveEventLoop| { + event_handler_inner(event, window_target); + }; + + let mut start_cause = StartCause::Init; + + loop { + event_handler(event::Event::NewEvents(start_cause), &self.window_target); + + if start_cause == StartCause::Init { + event_handler(event::Event::Resumed, &self.window_target); + } + + // Handle window creates. + while let Some(window) = { + let mut creates = self.window_target.p.creates.lock().unwrap(); + creates.pop_front() + } { + let window_id = WindowId { + fd: window.fd as u64, + }; + + let mut buf: [u8; 4096] = [0; 4096]; + let path = window.fpath(&mut buf).expect("failed to read properties"); + let properties = WindowProperties::new(path); + + self.windows.push((window, EventState::default())); + + // Send resize event on create to indicate first size. + event_handler( + event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Resized( + (properties.w, properties.h).into(), + ), + }, + &self.window_target, + ); + + // Send resize event on create to indicate first position. + event_handler( + event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Moved( + (properties.x, properties.y).into(), + ), + }, + &self.window_target, + ); + } + + // Handle window destroys. + while let Some(destroy_id) = { + let mut destroys = self.window_target.p.destroys.lock().unwrap(); + destroys.pop_front() + } { + event_handler( + event::Event::WindowEvent { + window_id: RootWindowId(destroy_id), + event: event::WindowEvent::Destroyed, + }, + &self.window_target, + ); + + self.windows + .retain(|(window, _event_state)| window.fd as u64 != destroy_id.fd); + } + + // Handle window events. + let mut i = 0; + // While loop is used here because the same window may be processed more than once. + while let Some((window, event_state)) = self.windows.get_mut(i) { + let window_id = WindowId { + fd: window.fd as u64, + }; + + let mut event_buf = [0u8; 16 * mem::size_of::()]; + let count = syscall::read(window.fd, &mut event_buf) + .expect("failed to read window events"); + // Safety: orbclient::Event is a packed struct designed to be transferred over a + // socket. + let events = unsafe { + slice::from_raw_parts( + event_buf.as_ptr() as *const orbclient::Event, + count / mem::size_of::(), + ) + }; + + for orbital_event in events { + Self::process_event( + window_id, + orbital_event.to_option(), + event_state, + |event| event_handler(event, &self.window_target), + ); + } + + if count == event_buf.len() { + // If event buf was full, process same window again to ensure all events are + // drained. + continue; + } + + // Acknowledge the latest resize event. + if let Some((w, h)) = event_state.resize_opt.take() { + window + .write(format!("S,{w},{h}").as_bytes()) + .expect("failed to acknowledge resize"); + + // Require redraw after resize. + let mut redraws = self.window_target.p.redraws.lock().unwrap(); + if !redraws.contains(&window_id) { + redraws.push_back(window_id); + } + } + + // Move to next window. + i += 1; + } + + while let Ok(event) = self.user_events_receiver.try_recv() { + event_handler(event::Event::UserEvent(event), &self.window_target); + } + + // To avoid deadlocks the redraws lock is not held during event processing. + while let Some(window_id) = { + let mut redraws = self.window_target.p.redraws.lock().unwrap(); + redraws.pop_front() + } { + event_handler( + event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::RedrawRequested, + }, + &self.window_target, + ); + } + + event_handler(event::Event::AboutToWait, &self.window_target); + + if self.window_target.p.exiting() { + break; + } + + let requested_resume = match self.window_target.p.control_flow() { + ControlFlow::Poll => { + start_cause = StartCause::Poll; + continue; + } + ControlFlow::Wait => None, + ControlFlow::WaitUntil(instant) => Some(instant), + }; + + // Re-using wake socket caused extra wake events before because there were leftover + // timeouts, and then new timeouts were added each time a spurious timeout expired. + let timeout_socket = TimeSocket::open().unwrap(); + + self.window_target + .p + .event_socket + .write(&syscall::Event { + id: timeout_socket.0.fd, + flags: syscall::EventFlags::EVENT_READ, + data: 0, + }) + .unwrap(); + + let start = Instant::now(); + if let Some(instant) = requested_resume { + let mut time = timeout_socket.current_time().unwrap(); + + if let Some(duration) = instant.checked_duration_since(start) { + time.tv_sec += duration.as_secs() as i64; + time.tv_nsec += duration.subsec_nanos() as i32; + // Normalize timespec so tv_nsec is not greater than one second. + while time.tv_nsec >= 1_000_000_000 { + time.tv_sec += 1; + time.tv_nsec -= 1_000_000_000; + } + } + + timeout_socket.timeout(&time).unwrap(); + } + + // Wait for event if needed. + let mut event = syscall::Event::default(); + self.window_target.p.event_socket.read(&mut event).unwrap(); + + // TODO: handle spurious wakeups (redraw caused wakeup but redraw already handled) + match requested_resume { + Some(requested_resume) if event.id == timeout_socket.0.fd => { + // If the event is from the special timeout socket, report that resume + // time was reached. + start_cause = StartCause::ResumeTimeReached { + start, + requested_resume, + }; + } + _ => { + // Normal window event or spurious timeout. + start_cause = StartCause::WaitCancelled { + start, + requested_resume, + }; + } + } + } + + event_handler(event::Event::LoopExiting, &self.window_target); + + Ok(()) + } + + pub fn window_target(&self) -> &event_loop::ActiveEventLoop { + &self.window_target + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + user_events_sender: self.user_events_sender.clone(), + wake_socket: self.window_target.p.wake_socket.clone(), + } + } +} + +pub struct EventLoopProxy { + user_events_sender: mpsc::Sender, + wake_socket: Arc, +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), event_loop::EventLoopClosed> { + self.user_events_sender + .send(event) + .map_err(|mpsc::SendError(x)| event_loop::EventLoopClosed(x))?; + + self.wake_socket.wake().unwrap(); + + Ok(()) + } +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + Self { + user_events_sender: self.user_events_sender.clone(), + wake_socket: self.wake_socket.clone(), + } + } +} + +impl Unpin for EventLoopProxy {} + +pub struct ActiveEventLoop { + control_flow: Cell, + exit: Cell, + pub(super) creates: Mutex>>, + pub(super) redraws: Arc>>, + pub(super) destroys: Arc>>, + pub(super) event_socket: Arc, + pub(super) wake_socket: Arc, +} + +impl ActiveEventLoop { + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> RootCustomCursor { + let _ = source.inner; + RootCustomCursor { + inner: super::PlatformCustomCursor, + } + } + + pub fn primary_monitor(&self) -> Option { + Some(MonitorHandle) + } + + pub fn available_monitors(&self) -> VecDeque { + let mut v = VecDeque::with_capacity(1); + v.push_back(MonitorHandle); + v + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Orbital( + rwh_06::OrbitalDisplayHandle::new(), + )) + } + + pub fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub(crate) fn exit(&self) { + self.exit.set(true); + } + + pub(crate) fn exiting(&self) -> bool { + self.exit.get() + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::OrbitalDisplayHandle::new().into()) + } +} diff --git a/rio-window/src/platform_impl/orbital/mod.rs b/rio-window/src/platform_impl/orbital/mod.rs new file mode 100644 index 00000000..fb0d829f --- /dev/null +++ b/rio-window/src/platform_impl/orbital/mod.rs @@ -0,0 +1,274 @@ +#![cfg(target_os = "redox")] + +use std::fmt::{self, Display, Formatter}; +use std::str; +use std::sync::Arc; + +use smol_str::SmolStr; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::keyboard::Key; + +pub(crate) use self::event_loop::{ + ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle, +}; +mod event_loop; + +pub use self::window::Window; +mod window; + +struct RedoxSocket { + fd: usize, +} + +impl RedoxSocket { + fn event() -> syscall::Result { + Self::open_raw("event:") + } + + fn orbital(properties: &WindowProperties<'_>) -> syscall::Result { + Self::open_raw(&format!("{properties}")) + } + + // Paths should be checked to ensure they are actually sockets and not normal files. If a + // non-socket path is used, it could cause read and write to not function as expected. For + // example, the seek would change in a potentially unpredictable way if either read or write + // were called at the same time by multiple threads. + fn open_raw(path: &str) -> syscall::Result { + let fd = syscall::open(path, syscall::O_RDWR | syscall::O_CLOEXEC)?; + Ok(Self { fd }) + } + + fn read(&self, buf: &mut [u8]) -> syscall::Result<()> { + let count = syscall::read(self.fd, buf)?; + if count == buf.len() { + Ok(()) + } else { + Err(syscall::Error::new(syscall::EINVAL)) + } + } + + fn write(&self, buf: &[u8]) -> syscall::Result<()> { + let count = syscall::write(self.fd, buf)?; + if count == buf.len() { + Ok(()) + } else { + Err(syscall::Error::new(syscall::EINVAL)) + } + } + + fn fpath<'a>(&self, buf: &'a mut [u8]) -> syscall::Result<&'a str> { + let count = syscall::fpath(self.fd, buf)?; + str::from_utf8(&buf[..count]).map_err(|_err| syscall::Error::new(syscall::EINVAL)) + } +} + +impl Drop for RedoxSocket { + fn drop(&mut self) { + let _ = syscall::close(self.fd); + } +} + +pub struct TimeSocket(RedoxSocket); + +impl TimeSocket { + fn open() -> syscall::Result { + RedoxSocket::open_raw("time:4").map(Self) + } + + // Read current time. + fn current_time(&self) -> syscall::Result { + let mut timespec = syscall::TimeSpec::default(); + self.0.read(&mut timespec)?; + Ok(timespec) + } + + // Write a timeout. + fn timeout(&self, timespec: &syscall::TimeSpec) -> syscall::Result<()> { + self.0.write(timespec) + } + + // Wake immediately. + fn wake(&self) -> syscall::Result<()> { + // Writing a default TimeSpec will always trigger a time event. + self.timeout(&syscall::TimeSpec::default()) + } +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes {} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct WindowId { + fd: u64, +} + +impl WindowId { + pub const fn dummy() -> Self { + WindowId { + fd: u64::max_value(), + } + } +} + +impl From for u64 { + fn from(id: WindowId) -> Self { + id.fd + } +} + +impl From for WindowId { + fn from(fd: u64) -> Self { + Self { fd } + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct DeviceId; + +impl DeviceId { + pub const fn dummy() -> Self { + DeviceId + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct PlatformSpecificWindowAttributes; + +struct WindowProperties<'a> { + flags: &'a str, + x: i32, + y: i32, + w: u32, + h: u32, + title: &'a str, +} + +impl<'a> WindowProperties<'a> { + fn new(path: &'a str) -> Self { + // orbital:flags/x/y/w/h/t + let mut parts = path.splitn(6, '/'); + let flags = parts.next().unwrap_or(""); + let x = parts + .next() + .map_or(0, |part| part.parse::().unwrap_or(0)); + let y = parts + .next() + .map_or(0, |part| part.parse::().unwrap_or(0)); + let w = parts + .next() + .map_or(0, |part| part.parse::().unwrap_or(0)); + let h = parts + .next() + .map_or(0, |part| part.parse::().unwrap_or(0)); + let title = parts.next().unwrap_or(""); + Self { + flags, + x, + y, + w, + h, + title, + } + } +} + +impl<'a> fmt::Display for WindowProperties<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "orbital:{}/{}/{}/{}/{}/{}", + self.flags, self.x, self.y, self.w, self.h, self.title + ) + } +} + +#[derive(Clone, Debug)] +pub struct OsError(Arc); + +impl OsError { + fn new(error: syscall::Error) -> Self { + Self(Arc::new(error)) + } +} + +impl Display for OsError { + fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> { + self.0.fmt(fmt) + } +} + +pub(crate) use crate::cursor::{ + NoCustomCursor as PlatformCustomCursor, NoCustomCursor as PlatformCustomCursorSource, +}; +pub(crate) use crate::icon::NoIcon as PlatformIcon; + +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct MonitorHandle; + +impl MonitorHandle { + pub fn name(&self) -> Option { + Some("Redox Device".to_owned()) + } + + pub fn size(&self) -> PhysicalSize { + PhysicalSize::new(0, 0) // TODO + } + + pub fn position(&self) -> PhysicalPosition { + (0, 0).into() + } + + pub fn scale_factor(&self) -> f64 { + 1.0 // TODO + } + + pub fn refresh_rate_millihertz(&self) -> Option { + // FIXME no way to get real refresh rate for now. + None + } + + pub fn video_modes(&self) -> impl Iterator { + let size = self.size().into(); + // FIXME this is not the real refresh rate + // (it is guaranteed to support 32 bit color though) + std::iter::once(VideoModeHandle { + size, + bit_depth: 32, + refresh_rate_millihertz: 60000, + monitor: self.clone(), + }) + } +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct VideoModeHandle { + size: (u32, u32), + bit_depth: u16, + refresh_rate_millihertz: u32, + monitor: MonitorHandle, +} + +impl VideoModeHandle { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra { + pub key_without_modifiers: Key, + pub text_with_all_modifiers: Option, +} diff --git a/rio-window/src/platform_impl/orbital/window.rs b/rio-window/src/platform_impl/orbital/window.rs new file mode 100644 index 00000000..e6001a0d --- /dev/null +++ b/rio-window/src/platform_impl/orbital/window.rs @@ -0,0 +1,526 @@ +use std::collections::VecDeque; +use std::sync::{Arc, Mutex}; + +use crate::cursor::Cursor; +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::platform_impl::Fullscreen; +use crate::window::ImePurpose; +use crate::{error, window}; + +use super::{ + ActiveEventLoop, MonitorHandle, OsError, RedoxSocket, TimeSocket, WindowId, + WindowProperties, +}; + +// These values match the values uses in the `window_new` function in orbital: +// https://gitlab.redox-os.org/redox-os/orbital/-/blob/master/src/scheme.rs +const ORBITAL_FLAG_ASYNC: char = 'a'; +const ORBITAL_FLAG_BACK: char = 'b'; +const ORBITAL_FLAG_FRONT: char = 'f'; +const ORBITAL_FLAG_HIDDEN: char = 'h'; +const ORBITAL_FLAG_BORDERLESS: char = 'l'; +const ORBITAL_FLAG_MAXIMIZED: char = 'm'; +const ORBITAL_FLAG_RESIZABLE: char = 'r'; +const ORBITAL_FLAG_TRANSPARENT: char = 't'; + +pub struct Window { + window_socket: Arc, + redraws: Arc>>, + destroys: Arc>>, + wake_socket: Arc, +} + +impl Window { + pub(crate) fn new( + el: &ActiveEventLoop, + attrs: window::WindowAttributes, + ) -> Result { + let scale = MonitorHandle.scale_factor(); + + let (x, y) = if let Some(pos) = attrs.position { + pos.to_physical::(scale).into() + } else { + // These coordinates are a special value to center the window. + (-1, -1) + }; + + let (w, h): (u32, u32) = if let Some(size) = attrs.inner_size { + size.to_physical::(scale).into() + } else { + (1024, 768) + }; + + // TODO: min/max inner_size + + // Async by default. + let mut flag_str = ORBITAL_FLAG_ASYNC.to_string(); + + if attrs.maximized { + flag_str.push(ORBITAL_FLAG_MAXIMIZED); + } + + if attrs.resizable { + flag_str.push(ORBITAL_FLAG_RESIZABLE); + } + + // TODO: fullscreen + + if attrs.transparent { + flag_str.push(ORBITAL_FLAG_TRANSPARENT); + } + + if !attrs.decorations { + flag_str.push(ORBITAL_FLAG_BORDERLESS); + } + + if !attrs.visible { + flag_str.push(ORBITAL_FLAG_HIDDEN); + } + + match attrs.window_level { + window::WindowLevel::AlwaysOnBottom => { + flag_str.push(ORBITAL_FLAG_BACK); + } + window::WindowLevel::Normal => {} + window::WindowLevel::AlwaysOnTop => { + flag_str.push(ORBITAL_FLAG_FRONT); + } + } + + // TODO: window_icon + + // Open window. + let window = RedoxSocket::orbital(&WindowProperties { + flags: &flag_str, + x, + y, + w, + h, + title: &attrs.title, + }) + .expect("failed to open window"); + + // Add to event socket. + el.event_socket + .write(&syscall::Event { + id: window.fd, + flags: syscall::EventFlags::EVENT_READ, + data: window.fd, + }) + .unwrap(); + + let window_socket = Arc::new(window); + + // Notify event thread that this window was created, it will send some default events. + { + let mut creates = el.creates.lock().unwrap(); + creates.push_back(window_socket.clone()); + } + + el.wake_socket.wake().unwrap(); + + Ok(Self { + window_socket, + redraws: el.redraws.clone(), + destroys: el.destroys.clone(), + wake_socket: el.wake_socket.clone(), + }) + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) { + f(self) + } + + pub(crate) fn maybe_wait_on_main( + &self, + f: impl FnOnce(&Self) -> R + Send, + ) -> R { + f(self) + } + + fn get_flag(&self, flag: char) -> Result { + let mut buf: [u8; 4096] = [0; 4096]; + let path = self + .window_socket + .fpath(&mut buf) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + let properties = WindowProperties::new(path); + Ok(properties.flags.contains(flag)) + } + + fn set_flag(&self, flag: char, value: bool) -> Result<(), error::ExternalError> { + self.window_socket + .write(format!("F,{flag},{}", if value { 1 } else { 0 }).as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) + } + + #[inline] + pub fn id(&self) -> WindowId { + WindowId { + fd: self.window_socket.fd as u64, + } + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + Some(MonitorHandle) + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + let mut v = VecDeque::with_capacity(1); + v.push_back(MonitorHandle); + v + } + + #[inline] + pub fn current_monitor(&self) -> Option { + Some(MonitorHandle) + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + MonitorHandle.scale_factor() + } + + #[inline] + pub fn request_redraw(&self) { + let window_id = self.id(); + let mut redraws = self.redraws.lock().unwrap(); + if !redraws.contains(&window_id) { + redraws.push_back(window_id); + + self.wake_socket.wake().unwrap(); + } + } + + #[inline] + pub fn pre_present_notify(&self) {} + + #[inline] + pub fn reset_dead_keys(&self) { + // TODO? + } + + #[inline] + pub fn inner_position( + &self, + ) -> Result, error::NotSupportedError> { + let mut buf: [u8; 4096] = [0; 4096]; + let path = self + .window_socket + .fpath(&mut buf) + .expect("failed to read properties"); + let properties = WindowProperties::new(path); + Ok((properties.x, properties.y).into()) + } + + #[inline] + pub fn outer_position( + &self, + ) -> Result, error::NotSupportedError> { + // TODO: adjust for window decorations + self.inner_position() + } + + #[inline] + pub fn set_outer_position(&self, position: Position) { + // TODO: adjust for window decorations + let (x, y): (i32, i32) = position.to_physical::(self.scale_factor()).into(); + self.window_socket + .write(format!("P,{x},{y}").as_bytes()) + .expect("failed to set position"); + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let mut buf: [u8; 4096] = [0; 4096]; + let path = self + .window_socket + .fpath(&mut buf) + .expect("failed to read properties"); + let properties = WindowProperties::new(path); + (properties.w, properties.h).into() + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let (w, h): (u32, u32) = size.to_physical::(self.scale_factor()).into(); + self.window_socket + .write(format!("S,{w},{h}").as_bytes()) + .expect("failed to set size"); + None + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + // TODO: adjust for window decorations + self.inner_size() + } + + #[inline] + pub fn set_min_inner_size(&self, _: Option) {} + + #[inline] + pub fn set_max_inner_size(&self, _: Option) {} + + #[inline] + pub fn title(&self) -> String { + let mut buf: [u8; 4096] = [0; 4096]; + let path = self + .window_socket + .fpath(&mut buf) + .expect("failed to read properties"); + let properties = WindowProperties::new(path); + properties.title.to_string() + } + + #[inline] + pub fn set_title(&self, title: &str) { + self.window_socket + .write(format!("T,{title}").as_bytes()) + .expect("failed to set title"); + } + + #[inline] + pub fn set_transparent(&self, transparent: bool) { + let _ = self.set_flag(ORBITAL_FLAG_TRANSPARENT, transparent); + } + + #[inline] + pub fn set_blur(&self, _blur: bool) {} + + #[inline] + pub fn set_visible(&self, visible: bool) { + let _ = self.set_flag(ORBITAL_FLAG_HIDDEN, !visible); + } + + #[inline] + pub fn is_visible(&self) -> Option { + Some(!self.get_flag(ORBITAL_FLAG_HIDDEN).unwrap_or(false)) + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + None + } + + #[inline] + pub fn set_resize_increments(&self, _increments: Option) {} + + #[inline] + pub fn set_resizable(&self, resizeable: bool) { + let _ = self.set_flag(ORBITAL_FLAG_RESIZABLE, resizeable); + } + + #[inline] + pub fn is_resizable(&self) -> bool { + self.get_flag(ORBITAL_FLAG_RESIZABLE).unwrap_or(false) + } + + #[inline] + pub fn set_minimized(&self, _minimized: bool) {} + + #[inline] + pub fn is_minimized(&self) -> Option { + None + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + let _ = self.set_flag(ORBITAL_FLAG_MAXIMIZED, maximized); + } + + #[inline] + pub fn is_maximized(&self) -> bool { + self.get_flag(ORBITAL_FLAG_MAXIMIZED).unwrap_or(false) + } + + #[inline] + pub(crate) fn set_fullscreen(&self, _monitor: Option) {} + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + None + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + let _ = self.set_flag(ORBITAL_FLAG_BORDERLESS, !decorations); + } + + #[inline] + pub fn is_decorated(&self) -> bool { + !self.get_flag(ORBITAL_FLAG_BORDERLESS).unwrap_or(false) + } + + #[inline] + pub fn set_window_level(&self, level: window::WindowLevel) { + match level { + window::WindowLevel::AlwaysOnBottom => { + let _ = self.set_flag(ORBITAL_FLAG_BACK, true); + } + window::WindowLevel::Normal => { + let _ = self.set_flag(ORBITAL_FLAG_BACK, false); + let _ = self.set_flag(ORBITAL_FLAG_FRONT, false); + } + window::WindowLevel::AlwaysOnTop => { + let _ = self.set_flag(ORBITAL_FLAG_FRONT, true); + } + } + } + + #[inline] + pub fn set_window_icon(&self, _window_icon: Option) {} + + #[inline] + pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) {} + + #[inline] + pub fn set_ime_allowed(&self, _allowed: bool) {} + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + + #[inline] + pub fn focus_window(&self) {} + + #[inline] + pub fn request_user_attention( + &self, + _request_type: Option, + ) { + } + + #[inline] + pub fn set_cursor(&self, _: Cursor) {} + + #[inline] + pub fn set_cursor_position(&self, _: Position) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported( + error::NotSupportedError::new(), + )) + } + + #[inline] + pub fn set_cursor_grab( + &self, + mode: window::CursorGrabMode, + ) -> Result<(), error::ExternalError> { + let (grab, relative) = match mode { + window::CursorGrabMode::None => (false, false), + window::CursorGrabMode::Confined => (true, false), + window::CursorGrabMode::Locked => (true, true), + }; + self.window_socket + .write(format!("M,G,{}", if grab { 1 } else { 0 }).as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + self.window_socket + .write(format!("M,R,{}", if relative { 1 } else { 0 }).as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + let _ = self + .window_socket + .write(format!("M,C,{}", if visible { 1 } else { 0 }).as_bytes()); + } + + #[inline] + pub fn drag_window(&self) -> Result<(), error::ExternalError> { + self.window_socket + .write(b"D") + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) + } + + #[inline] + pub fn drag_resize_window( + &self, + direction: window::ResizeDirection, + ) -> Result<(), error::ExternalError> { + let arg = match direction { + window::ResizeDirection::East => "R", + window::ResizeDirection::North => "T", + window::ResizeDirection::NorthEast => "T,R", + window::ResizeDirection::NorthWest => "T,L", + window::ResizeDirection::South => "B", + window::ResizeDirection::SouthEast => "B,R", + window::ResizeDirection::SouthWest => "B,L", + window::ResizeDirection::West => "L", + }; + self.window_socket + .write(format!("D,{}", arg).as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + #[inline] + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported( + error::NotSupportedError::new(), + )) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06( + &self, + ) -> Result { + let handle = rwh_06::OrbitalWindowHandle::new({ + let window = self.window_socket.fd as *mut _; + std::ptr::NonNull::new(window).expect("orbital fd should never be null") + }); + Ok(rwh_06::RawWindowHandle::Orbital(handle)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Orbital( + rwh_06::OrbitalDisplayHandle::new(), + )) + } + + #[inline] + pub fn set_enabled_buttons(&self, _buttons: window::WindowButtons) {} + + #[inline] + pub fn enabled_buttons(&self) -> window::WindowButtons { + window::WindowButtons::all() + } + + #[inline] + pub fn theme(&self) -> Option { + None + } + + #[inline] + pub fn has_focus(&self) -> bool { + false + } + + #[inline] + pub fn set_theme(&self, _theme: Option) {} + + pub fn set_content_protected(&self, _protected: bool) {} +} + +impl Drop for Window { + fn drop(&mut self) { + { + let mut destroys = self.destroys.lock().unwrap(); + destroys.push_back(self.id()); + } + + self.wake_socket.wake().unwrap(); + } +} diff --git a/rio-window/src/platform_impl/web/async/abortable.rs b/rio-window/src/platform_impl/web/async/abortable.rs new file mode 100644 index 00000000..2608fc40 --- /dev/null +++ b/rio-window/src/platform_impl/web/async/abortable.rs @@ -0,0 +1,102 @@ +use std::error::Error; +use std::fmt::{self, Display, Formatter}; +use std::future::Future; +use std::pin::Pin; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use pin_project::pin_project; + +use super::AtomicWaker; + +#[pin_project] +pub struct Abortable { + #[pin] + future: F, + shared: Arc, +} + +impl Abortable { + pub fn new(handle: AbortHandle, future: F) -> Self { + Self { + future, + shared: handle.0, + } + } +} + +impl Future for Abortable { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.shared.aborted.load(Ordering::Relaxed) { + return Poll::Ready(Err(Aborted)); + } + + if let Poll::Ready(value) = self.as_mut().project().future.poll(cx) { + return Poll::Ready(Ok(value)); + } + + self.shared.waker.register(cx.waker()); + + if self.shared.aborted.load(Ordering::Relaxed) { + return Poll::Ready(Err(Aborted)); + } + + Poll::Pending + } +} + +#[derive(Debug)] +struct Shared { + waker: AtomicWaker, + aborted: AtomicBool, +} + +#[derive(Clone, Debug)] +pub struct AbortHandle(Arc); + +impl AbortHandle { + pub fn new() -> Self { + Self(Arc::new(Shared { + waker: AtomicWaker::new(), + aborted: AtomicBool::new(false), + })) + } + + pub fn abort(&self) { + self.0.aborted.store(true, Ordering::Relaxed); + self.0.waker.wake() + } +} + +#[derive(Debug)] +pub struct DropAbortHandle(AbortHandle); + +impl DropAbortHandle { + pub fn new(handle: AbortHandle) -> Self { + Self(handle) + } + + pub fn handle(&self) -> AbortHandle { + self.0.clone() + } +} + +impl Drop for DropAbortHandle { + fn drop(&mut self) { + self.0.abort() + } +} + +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +pub struct Aborted; + +impl Display for Aborted { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "`Abortable` future has been aborted") + } +} + +impl Error for Aborted {} diff --git a/rio-window/src/platform_impl/web/async/atomic_waker.rs b/rio-window/src/platform_impl/web/async/atomic_waker.rs new file mode 100644 index 00000000..87b83612 --- /dev/null +++ b/rio-window/src/platform_impl/web/async/atomic_waker.rs @@ -0,0 +1,35 @@ +use std::cell::RefCell; +use std::ops::Deref; +use std::task::Waker; + +#[derive(Debug)] +pub struct AtomicWaker(RefCell>); + +impl AtomicWaker { + pub const fn new() -> Self { + Self(RefCell::new(None)) + } + + pub fn register(&self, waker: &Waker) { + let mut this = self.0.borrow_mut(); + + if let Some(old_waker) = this.deref() { + if old_waker.will_wake(waker) { + return; + } + } + + *this = Some(waker.clone()); + } + + pub fn wake(&self) { + if let Some(waker) = self.0.borrow_mut().take() { + waker.wake(); + } + } +} + +// SAFETY: Wasm without the `atomics` target feature is single-threaded. +unsafe impl Send for AtomicWaker {} +// SAFETY: Wasm without the `atomics` target feature is single-threaded. +unsafe impl Sync for AtomicWaker {} diff --git a/rio-window/src/platform_impl/web/async/channel.rs b/rio-window/src/platform_impl/web/async/channel.rs new file mode 100644 index 00000000..6b313f32 --- /dev/null +++ b/rio-window/src/platform_impl/web/async/channel.rs @@ -0,0 +1,124 @@ +use std::future; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{self, RecvError, SendError, TryRecvError}; +use std::sync::{Arc, Mutex}; +use std::task::Poll; + +use super::AtomicWaker; + +pub fn channel() -> (Sender, Receiver) { + let (sender, receiver) = mpsc::channel(); + let shared = Arc::new(Shared { + closed: AtomicBool::new(false), + waker: AtomicWaker::new(), + }); + + let sender = Sender(Arc::new(SenderInner { + sender: Mutex::new(sender), + shared: Arc::clone(&shared), + })); + let receiver = Receiver { + receiver: Rc::new(receiver), + shared, + }; + + (sender, receiver) +} + +pub struct Sender(Arc>); + +struct SenderInner { + // We need to wrap it into a `Mutex` to make it `Sync`. So the sender can't + // be accessed on the main thread, as it could block. Additionally we need + // to wrap `Sender` in an `Arc` to make it clonable on the main thread without + // having to block. + sender: Mutex>, + shared: Arc, +} + +impl Sender { + pub fn send(&self, event: T) -> Result<(), SendError> { + self.0.sender.lock().unwrap().send(event)?; + self.0.shared.waker.wake(); + + Ok(()) + } +} + +impl SenderInner { + fn close(&self) { + self.shared.closed.store(true, Ordering::Relaxed); + self.shared.waker.wake(); + } +} + +impl Clone for Sender { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } +} + +impl Drop for SenderInner { + fn drop(&mut self) { + self.close(); + } +} + +pub struct Receiver { + receiver: Rc>, + shared: Arc, +} + +impl Receiver { + pub async fn next(&self) -> Result { + future::poll_fn(|cx| match self.receiver.try_recv() { + Ok(event) => Poll::Ready(Ok(event)), + Err(TryRecvError::Empty) => { + self.shared.waker.register(cx.waker()); + + match self.receiver.try_recv() { + Ok(event) => Poll::Ready(Ok(event)), + Err(TryRecvError::Empty) => { + if self.shared.closed.load(Ordering::Relaxed) { + Poll::Ready(Err(RecvError)) + } else { + Poll::Pending + } + } + Err(TryRecvError::Disconnected) => Poll::Ready(Err(RecvError)), + } + } + Err(TryRecvError::Disconnected) => Poll::Ready(Err(RecvError)), + }) + .await + } + + pub fn try_recv(&self) -> Result, RecvError> { + match self.receiver.try_recv() { + Ok(value) => Ok(Some(value)), + Err(TryRecvError::Empty) => Ok(None), + Err(TryRecvError::Disconnected) => Err(RecvError), + } + } +} + +impl Clone for Receiver { + fn clone(&self) -> Self { + Self { + receiver: Rc::clone(&self.receiver), + shared: Arc::clone(&self.shared), + } + } +} + +impl Drop for Receiver { + fn drop(&mut self) { + self.shared.closed.store(true, Ordering::Relaxed); + } +} + +struct Shared { + closed: AtomicBool, + waker: AtomicWaker, +} diff --git a/rio-window/src/platform_impl/web/async/concurrent_queue.rs b/rio-window/src/platform_impl/web/async/concurrent_queue.rs new file mode 100644 index 00000000..2d7efacd --- /dev/null +++ b/rio-window/src/platform_impl/web/async/concurrent_queue.rs @@ -0,0 +1,55 @@ +use std::cell::{Cell, RefCell}; + +#[derive(Debug)] +pub struct ConcurrentQueue { + queue: RefCell>, + closed: Cell, +} + +pub enum PushError { + #[allow(dead_code)] + Full(T), + Closed(T), +} + +pub enum PopError { + Empty, + Closed, +} + +impl ConcurrentQueue { + pub fn unbounded() -> Self { + Self { + queue: RefCell::new(Vec::new()), + closed: Cell::new(false), + } + } + + pub fn push(&self, value: T) -> Result<(), PushError> { + if self.closed.get() { + return Err(PushError::Closed(value)); + } + + self.queue.borrow_mut().push(value); + Ok(()) + } + + pub fn pop(&self) -> Result { + self.queue.borrow_mut().pop().ok_or_else(|| { + if self.closed.get() { + PopError::Closed + } else { + PopError::Empty + } + }) + } + + pub fn close(&self) -> bool { + !self.closed.replace(true) + } +} + +// SAFETY: Wasm without the `atomics` target feature is single-threaded. +unsafe impl Send for ConcurrentQueue {} +// SAFETY: Wasm without the `atomics` target feature is single-threaded. +unsafe impl Sync for ConcurrentQueue {} diff --git a/rio-window/src/platform_impl/web/async/dispatcher.rs b/rio-window/src/platform_impl/web/async/dispatcher.rs new file mode 100644 index 00000000..32e980a9 --- /dev/null +++ b/rio-window/src/platform_impl/web/async/dispatcher.rs @@ -0,0 +1,111 @@ +use super::super::main_thread::MainThreadMarker; +use super::{channel, Receiver, Sender, Wrapper}; +use std::cell::Ref; +use std::sync::{Arc, Condvar, Mutex}; + +pub struct Dispatcher(Wrapper>, Closure>); + +struct Closure(Box); + +impl Dispatcher { + #[track_caller] + pub fn new( + main_thread: MainThreadMarker, + value: T, + ) -> Option<(Self, DispatchRunner)> { + let (sender, receiver) = channel::>(); + + Wrapper::new( + main_thread, + value, + |value, Closure(closure)| { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do + // anything funny with it here. See `Self::queue()`. + closure(value.borrow().as_ref().unwrap()) + }, + { + let receiver = receiver.clone(); + move |value| async move { + while let Ok(Closure(closure)) = receiver.next().await { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't + // do anything funny with it here. See + // `Self::queue()`. + closure(value.borrow().as_ref().unwrap()) + } + } + }, + sender, + |sender, closure| { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do + // anything funny with it here. See `Self::queue()`. + sender.send(closure).unwrap() + }, + ) + .map(|wrapper| (Self(wrapper.clone()), DispatchRunner { wrapper, receiver })) + } + + pub fn value(&self) -> Option> { + self.0.value() + } + + pub fn dispatch(&self, f: impl 'static + FnOnce(&T) + Send) { + if let Some(value) = self.0.value() { + f(&value) + } else { + self.0.send(Closure(Box::new(f))) + } + } + + pub fn queue(&self, f: impl FnOnce(&T) -> R + Send) -> R { + if let Some(value) = self.0.value() { + f(&value) + } else { + let pair = Arc::new((Mutex::new(None), Condvar::new())); + let closure = Box::new({ + let pair = pair.clone(); + move |value: &T| { + *pair.0.lock().unwrap() = Some(f(value)); + pair.1.notify_one(); + } + }) as Box; + // SAFETY: The `transmute` is necessary because `Closure` requires `'static`. This is + // safe because this function won't return until `f` has finished executing. See + // `Self::new()`. + let closure = Closure(unsafe { std::mem::transmute(closure) }); + + self.0.send(closure); + + let mut started = pair.0.lock().unwrap(); + + while started.is_none() { + started = pair.1.wait(started).unwrap(); + } + + started.take().unwrap() + } + } +} + +pub struct DispatchRunner { + wrapper: Wrapper>, Closure>, + receiver: Receiver>, +} + +impl DispatchRunner { + pub fn run(&self) { + while let Some(Closure(closure)) = self + .receiver + .try_recv() + .expect("should only be closed when `Dispatcher` is dropped") + { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do anything + // funny with it here. See `Self::queue()`. + closure( + &self + .wrapper + .value() + .expect("don't call this outside the main thread"), + ) + } + } +} diff --git a/rio-window/src/platform_impl/web/async/mod.rs b/rio-window/src/platform_impl/web/async/mod.rs new file mode 100644 index 00000000..4681cf92 --- /dev/null +++ b/rio-window/src/platform_impl/web/async/mod.rs @@ -0,0 +1,19 @@ +mod abortable; +#[cfg(not(target_feature = "atomics"))] +mod atomic_waker; +mod channel; +#[cfg(not(target_feature = "atomics"))] +mod concurrent_queue; +mod dispatcher; +mod notifier; +mod waker; +mod wrapper; + +pub use self::abortable::{AbortHandle, Abortable, DropAbortHandle}; +pub use self::channel::{channel, Receiver, Sender}; +pub use self::dispatcher::{DispatchRunner, Dispatcher}; +pub use self::notifier::{Notified, Notifier}; +pub use self::waker::{Waker, WakerSpawner}; +use self::wrapper::Wrapper; +use atomic_waker::AtomicWaker; +use concurrent_queue::{ConcurrentQueue, PushError}; diff --git a/rio-window/src/platform_impl/web/async/notifier.rs b/rio-window/src/platform_impl/web/async/notifier.rs new file mode 100644 index 00000000..c1fa3110 --- /dev/null +++ b/rio-window/src/platform_impl/web/async/notifier.rs @@ -0,0 +1,75 @@ +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, OnceLock}; +use std::task::{Context, Poll, Waker}; + +use super::{ConcurrentQueue, PushError}; + +#[derive(Debug)] +pub struct Notifier(Arc>); + +impl Notifier { + pub fn new() -> Self { + Self(Arc::new(Inner { + queue: ConcurrentQueue::unbounded(), + value: OnceLock::new(), + })) + } + + pub fn notify(self, value: T) { + if self.0.value.set(value).is_err() { + unreachable!("value set before") + } + + self.0.queue.close(); + + while let Ok(waker) = self.0.queue.pop() { + waker.wake() + } + } + + pub fn notified(&self) -> Notified { + Notified(Some(Arc::clone(&self.0))) + } +} + +#[derive(Clone, Debug)] +pub struct Notified(Option>>); + +impl Future for Notified { + type Output = T; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.0.take().expect("`Receiver` polled after completion"); + + if this.value.get().is_none() { + match this.queue.push(cx.waker().clone()) { + Ok(()) => { + if this.value.get().is_none() { + self.0 = Some(this); + return Poll::Pending; + } + } + Err(PushError::Closed(_)) => (), + Err(PushError::Full(_)) => { + unreachable!("found full queue despite using unbounded queue") + } + } + } + + let (Ok(Some(value)) | Err(Some(value))) = Arc::try_unwrap(this) + .map(|mut inner| inner.value.take()) + .map_err(|this| this.value.get().cloned()) + else { + unreachable!("found no value despite being ready") + }; + + Poll::Ready(value) + } +} + +#[derive(Debug)] +struct Inner { + queue: ConcurrentQueue, + value: OnceLock, +} diff --git a/rio-window/src/platform_impl/web/async/waker.rs b/rio-window/src/platform_impl/web/async/waker.rs new file mode 100644 index 00000000..f6be4640 --- /dev/null +++ b/rio-window/src/platform_impl/web/async/waker.rs @@ -0,0 +1,129 @@ +use std::future; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::Arc; +use std::task::Poll; + +use super::super::main_thread::MainThreadMarker; +use super::{AtomicWaker, Wrapper}; + +pub struct WakerSpawner(Wrapper, Sender, usize>); + +pub struct Waker(Wrapper, Sender, usize>); + +struct Handler { + value: T, + handler: fn(&T, usize), +} + +#[derive(Clone)] +struct Sender(Arc); + +impl WakerSpawner { + #[track_caller] + pub fn new( + main_thread: MainThreadMarker, + value: T, + handler: fn(&T, usize), + ) -> Option { + let inner = Arc::new(Inner { + counter: AtomicUsize::new(0), + waker: AtomicWaker::new(), + closed: AtomicBool::new(false), + }); + + let handler = Handler { value, handler }; + + let sender = Sender(Arc::clone(&inner)); + + let wrapper = Wrapper::new( + main_thread, + handler, + |handler, count| { + let handler = handler.borrow(); + let handler = handler.as_ref().unwrap(); + (handler.handler)(&handler.value, count); + }, + { + let inner = Arc::clone(&inner); + + move |handler| async move { + while let Some(count) = future::poll_fn(|cx| { + let count = inner.counter.swap(0, Ordering::Relaxed); + + if count > 0 { + Poll::Ready(Some(count)) + } else { + inner.waker.register(cx.waker()); + + let count = inner.counter.swap(0, Ordering::Relaxed); + + if count > 0 { + Poll::Ready(Some(count)) + } else { + if inner.closed.load(Ordering::Relaxed) { + return Poll::Ready(None); + } + + Poll::Pending + } + } + }) + .await + { + let handler = handler.borrow(); + let handler = handler.as_ref().unwrap(); + (handler.handler)(&handler.value, count); + } + } + }, + sender, + |inner, _| { + inner.0.counter.fetch_add(1, Ordering::Relaxed); + inner.0.waker.wake(); + }, + )?; + + Some(Self(wrapper)) + } + + pub fn waker(&self) -> Waker { + Waker(self.0.clone()) + } + + pub fn fetch(&self) -> usize { + debug_assert!( + MainThreadMarker::new().is_some(), + "this should only be called from the main thread" + ); + + self.0 + .with_sender_data(|inner| inner.0.counter.swap(0, Ordering::Relaxed)) + } +} + +impl Drop for WakerSpawner { + fn drop(&mut self) { + self.0.with_sender_data(|inner| { + inner.0.closed.store(true, Ordering::Relaxed); + inner.0.waker.wake(); + }); + } +} + +impl Waker { + pub fn wake(&self) { + self.0.send(1) + } +} + +impl Clone for Waker { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +struct Inner { + counter: AtomicUsize, + waker: AtomicWaker, + closed: AtomicBool, +} diff --git a/rio-window/src/platform_impl/web/async/wrapper.rs b/rio-window/src/platform_impl/web/async/wrapper.rs new file mode 100644 index 00000000..5fd06b97 --- /dev/null +++ b/rio-window/src/platform_impl/web/async/wrapper.rs @@ -0,0 +1,95 @@ +use super::super::main_thread::MainThreadMarker; +use std::cell::{Ref, RefCell}; +use std::future::Future; +use std::marker::PhantomData; +use std::sync::Arc; + +// Unsafe wrapper type that allows us to use `T` when it's not `Send` from other threads. +// `value` **must** only be accessed on the main thread. +pub struct Wrapper { + value: Value, + handler: fn(&RefCell>, E), + sender_data: S, + sender_handler: fn(&S, E), +} + +struct Value { + // SAFETY: + // This value must not be accessed if not on the main thread. + // + // - We wrap this in an `Arc` to allow it to be safely cloned without accessing the value. + // - The `RefCell` lets us mutably access in the main thread but is safe to drop in any thread + // because it has no `Drop` behavior. + // - The `Option` lets us safely drop `T` only in the main thread. + value: Arc>>, + // Prevent's `Send` or `Sync` to be automatically implemented. + local: PhantomData<*const ()>, +} + +// SAFETY: See `Self::value`. +unsafe impl Send for Value {} +// SAFETY: See `Self::value`. +unsafe impl Sync for Value {} + +impl Wrapper { + #[track_caller] + pub fn new>( + _: MainThreadMarker, + value: V, + handler: fn(&RefCell>, E), + receiver: impl 'static + FnOnce(Arc>>) -> R, + sender_data: S, + sender_handler: fn(&S, E), + ) -> Option { + let value = Arc::new(RefCell::new(Some(value))); + + wasm_bindgen_futures::spawn_local({ + let value = Arc::clone(&value); + async move { + receiver(Arc::clone(&value)).await; + drop(value.borrow_mut().take().unwrap()); + } + }); + + Some(Self { + value: Value { + value, + local: PhantomData, + }, + handler, + sender_data, + sender_handler, + }) + } + + pub fn send(&self, event: E) { + if MainThreadMarker::new().is_some() { + (self.handler)(&self.value.value, event) + } else { + (self.sender_handler)(&self.sender_data, event) + } + } + + pub fn value(&self) -> Option> { + MainThreadMarker::new() + .map(|_| Ref::map(self.value.value.borrow(), |value| value.as_ref().unwrap())) + } + + pub fn with_sender_data(&self, f: impl FnOnce(&S) -> T) -> T { + f(&self.sender_data) + } +} + +impl Clone for Wrapper { + fn clone(&self) -> Self { + Self { + value: Value { + value: self.value.value.clone(), + local: PhantomData, + }, + handler: self.handler, + sender_data: self.sender_data.clone(), + sender_handler: self.sender_handler, + } + } +} diff --git a/rio-window/src/platform_impl/web/cursor.rs b/rio-window/src/platform_impl/web/cursor.rs new file mode 100644 index 00000000..590a7d5a --- /dev/null +++ b/rio-window/src/platform_impl/web/cursor.rs @@ -0,0 +1,813 @@ +use std::cell::RefCell; +use std::future::{self, Future}; +use std::hash::{Hash, Hasher}; +use std::mem; +use std::ops::{Deref, DerefMut}; +use std::pin::Pin; +use std::rc::Rc; +use std::sync::Arc; +use std::task::{ready, Context, Poll, Waker}; +use std::time::Duration; + +use cursor_icon::CursorIcon; +use js_sys::{Array, Object}; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; +use web_sys::{ + Blob, Document, DomException, HtmlCanvasElement, HtmlImageElement, ImageBitmap, + ImageBitmapOptions, ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, + Window, +}; + +use super::backend::Style; +use super::main_thread::{MainThreadMarker, MainThreadSafe}; +use super::r#async::{AbortHandle, Abortable, DropAbortHandle, Notified, Notifier}; +use super::ActiveEventLoop; +use crate::cursor::{BadImage, Cursor, CursorImage, CustomCursor as RootCustomCursor}; +use crate::platform::web::CustomCursorError; + +#[derive(Debug)] +pub(crate) enum CustomCursorSource { + Image(CursorImage), + Url { + url: String, + hotspot_x: u16, + hotspot_y: u16, + }, + Animation { + duration: Duration, + cursors: Vec, + }, +} + +impl CustomCursorSource { + pub fn from_rgba( + rgba: Vec, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + Ok(CustomCursorSource::Image(CursorImage::from_rgba( + rgba, width, height, hotspot_x, hotspot_y, + )?)) + } +} + +#[derive(Clone, Debug)] +pub struct CustomCursor { + pub(crate) animation: bool, + state: Arc>>, +} + +impl Hash for CustomCursor { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.state).hash(state); + } +} + +impl PartialEq for CustomCursor { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.state, &other.state) + } +} + +impl Eq for CustomCursor {} + +impl CustomCursor { + pub(crate) fn new(event_loop: &ActiveEventLoop, source: CustomCursorSource) -> Self { + match source { + CustomCursorSource::Image(image) => Self::build_spawn( + event_loop, + from_rgba( + event_loop.runner.window(), + event_loop.runner.document().clone(), + &image, + ), + false, + ), + CustomCursorSource::Url { + url, + hotspot_x, + hotspot_y, + } => Self::build_spawn( + event_loop, + from_url(UrlType::Plain(url), hotspot_x, hotspot_y), + false, + ), + CustomCursorSource::Animation { duration, cursors } => Self::build_spawn( + event_loop, + from_animation( + event_loop.runner.main_thread(), + duration, + cursors.into_iter().map(|cursor| cursor.inner), + ), + true, + ), + } + } + + fn build_spawn( + window_target: &ActiveEventLoop, + task: F, + animation: bool, + ) -> CustomCursor + where + F: 'static + Future>, + S: Into, + { + let handle = AbortHandle::new(); + let this = CustomCursor { + animation, + state: Arc::new(MainThreadSafe::new( + window_target.runner.main_thread(), + RefCell::new(ImageState::Loading { + notifier: Notifier::new(), + _handle: DropAbortHandle::new(handle.clone()), + }), + )), + }; + let weak = Arc::downgrade(&this.state); + let main_thread = window_target.runner.main_thread(); + + let task = Abortable::new(handle, { + async move { + let result = task.await; + + let this = weak + .upgrade() + .expect("`CursorHandler` invalidated without aborting"); + let mut this = this.get(main_thread).borrow_mut(); + + match result { + Ok(new_state) => { + let ImageState::Loading { notifier, .. } = + mem::replace(this.deref_mut(), new_state.into()) + else { + unreachable!("found invalid state"); + }; + notifier.notify(Ok(())); + } + Err(error) => { + let ImageState::Loading { notifier, .. } = mem::replace( + this.deref_mut(), + ImageState::Failed(error.clone()), + ) else { + unreachable!("found invalid state"); + }; + notifier.notify(Err(error)); + } + } + } + }); + + wasm_bindgen_futures::spawn_local(async move { + let _ = task.await; + }); + + this + } + + pub(crate) fn new_async( + event_loop: &ActiveEventLoop, + source: CustomCursorSource, + ) -> CustomCursorFuture { + let CustomCursor { animation, state } = Self::new(event_loop, source); + let binding = state.get(event_loop.runner.main_thread()).borrow(); + let ImageState::Loading { notifier, .. } = binding.deref() else { + unreachable!("found invalid state") + }; + let notified = notifier.notified(); + drop(binding); + + CustomCursorFuture { + notified, + animation, + state: Some(state), + } + } +} + +#[derive(Debug)] +pub struct CustomCursorFuture { + notified: Notified>, + animation: bool, + state: Option>>>, +} + +impl Future for CustomCursorFuture { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.state.is_none() { + panic!("`CustomCursorFuture` polled after completion") + } + + let result = ready!(Pin::new(&mut self.notified).poll(cx)); + let state = self + .state + .take() + .expect("`CustomCursorFuture` polled after completion"); + + Poll::Ready(result.map(|_| CustomCursor { + animation: self.animation, + state, + })) + } +} + +#[derive(Debug)] +pub struct CursorHandler(Rc>); + +#[derive(Debug)] +struct Inner { + main_thread: MainThreadMarker, + canvas: HtmlCanvasElement, + style: Style, + visible: bool, + cursor: SelectedCursor, +} + +impl CursorHandler { + pub(crate) fn new( + main_thread: MainThreadMarker, + canvas: HtmlCanvasElement, + style: Style, + ) -> Self { + Self(Rc::new(RefCell::new(Inner { + main_thread, + canvas, + style, + visible: true, + cursor: SelectedCursor::default(), + }))) + } + + pub fn set_cursor(&self, cursor: Cursor) { + let mut this = self.0.borrow_mut(); + + match cursor { + Cursor::Icon(icon) => { + if let SelectedCursor::Icon(old_icon) + | SelectedCursor::Loading { + previous: Previous::Icon(old_icon), + .. + } = &this.cursor + { + if *old_icon == icon { + return; + } + } + + this.cursor = SelectedCursor::Icon(icon); + this.set_style(); + } + Cursor::Custom(cursor) => { + let cursor = cursor.inner; + + if let SelectedCursor::Loading { + cursor: old_cursor, .. + } + | SelectedCursor::Image(old_cursor) + | SelectedCursor::Animation { + cursor: old_cursor, .. + } = &this.cursor + { + if *old_cursor == cursor { + return; + } + } + + let state = cursor.state.get(this.main_thread).borrow(); + + match state.deref() { + ImageState::Loading { notifier, .. } => { + let notified = notifier.notified(); + let handle = DropAbortHandle::new(AbortHandle::new()); + let task = Abortable::new(handle.handle(), { + let weak = Rc::downgrade(&self.0); + async move { + let _ = notified.await; + let handler = weak.upgrade().expect( + "`CursorHandler` invalidated without aborting", + ); + handler.borrow_mut().notify(); + } + }); + wasm_bindgen_futures::spawn_local(async move { + let _ = task.await; + }); + + drop(state); + this.cursor = SelectedCursor::Loading { + cursor, + previous: mem::take(&mut this.cursor).into(), + _handle: handle, + }; + } + ImageState::Failed(error) => { + tracing::error!( + "trying to load custom cursor that has failed to load: {error}" + ) + } + ImageState::Image(_) => { + drop(state); + this.cursor = SelectedCursor::Image(cursor); + this.set_style(); + } + ImageState::Animation(animation) => { + let canvas: &CanvasAnimateExt = this.canvas.unchecked_ref(); + let animation = canvas.animate_with_keyframe_animation_options( + Some(&animation.keyframes), + &animation.options, + ); + drop(state); + + if !this.visible { + animation.cancel(); + } + + this.cursor = SelectedCursor::Animation { + animation: AnimationDropper(animation), + cursor, + }; + this.set_style(); + } + }; + } + } + } + + pub fn set_cursor_visible(&self, visible: bool) { + let mut this = self.0.borrow_mut(); + + if !visible && this.visible { + this.visible = false; + this.style.set("cursor", "none"); + + if let SelectedCursor::Animation { animation, .. } = &this.cursor { + animation.0.cancel(); + } + } else if visible && !this.visible { + this.visible = true; + this.set_style(); + } + } +} + +impl Inner { + fn set_style(&self) { + if self.visible { + match &self.cursor { + SelectedCursor::Icon(icon) + | SelectedCursor::Loading { + previous: Previous::Icon(icon), + .. + } => self.style.set("cursor", icon.name()), + SelectedCursor::Loading { + previous: Previous::Image(cursor), + .. + } + | SelectedCursor::Image(cursor) => { + match cursor.state.get(self.main_thread).borrow().deref() { + ImageState::Image(Image { style, .. }) => { + self.style.set("cursor", style) + } + _ => unreachable!("found invalid saved state"), + } + } + SelectedCursor::Loading { + previous: Previous::Animation { animation, .. }, + .. + } + | SelectedCursor::Animation { animation, .. } => { + self.style.remove("cursor"); + animation.0.play() + } + } + } + } + + fn notify(&mut self) { + let SelectedCursor::Loading { + cursor, previous, .. + } = mem::take(&mut self.cursor) + else { + unreachable!("found wrong state") + }; + + let state = cursor.state.get(self.main_thread).borrow(); + match state.deref() { + ImageState::Image(_) => { + drop(state); + self.cursor = SelectedCursor::Image(cursor); + self.set_style(); + } + ImageState::Animation(animation) => { + let canvas: &CanvasAnimateExt = self.canvas.unchecked_ref(); + let animation = canvas.animate_with_keyframe_animation_options( + Some(&animation.keyframes), + &animation.options, + ); + drop(state); + + if !self.visible { + animation.cancel(); + } + + self.cursor = SelectedCursor::Animation { + animation: AnimationDropper(animation), + cursor, + }; + self.set_style(); + } + ImageState::Failed(error) => { + tracing::error!("custom cursor failed to load: {error}"); + self.cursor = previous.into() + } + ImageState::Loading { .. } => unreachable!("notified without being ready"), + } + } +} + +#[derive(Debug)] +enum SelectedCursor { + Icon(CursorIcon), + Loading { + cursor: CustomCursor, + previous: Previous, + _handle: DropAbortHandle, + }, + Image(CustomCursor), + Animation { + cursor: CustomCursor, + animation: AnimationDropper, + }, +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Icon(Default::default()) + } +} + +impl From for SelectedCursor { + fn from(previous: Previous) -> Self { + match previous { + Previous::Icon(icon) => Self::Icon(icon), + Previous::Image(cursor) => Self::Image(cursor), + Previous::Animation { cursor, animation } => { + Self::Animation { cursor, animation } + } + } + } +} + +#[derive(Debug)] +enum Previous { + Icon(CursorIcon), + Image(CustomCursor), + Animation { + cursor: CustomCursor, + animation: AnimationDropper, + }, +} + +impl From for Previous { + fn from(value: SelectedCursor) -> Self { + match value { + SelectedCursor::Icon(icon) => Self::Icon(icon), + SelectedCursor::Loading { previous, .. } => previous, + SelectedCursor::Image(image) => Self::Image(image), + SelectedCursor::Animation { cursor, animation } => { + Self::Animation { cursor, animation } + } + } + } +} + +#[derive(Debug)] +enum ImageState { + Loading { + notifier: Notifier>, + _handle: DropAbortHandle, + }, + Failed(CustomCursorError), + Image(Image), + Animation(Animation), +} + +#[derive(Debug)] +struct Image { + style: String, + _object_url: Option, + _image: HtmlImageElement, +} + +impl From for ImageState { + fn from(image: Image) -> Self { + Self::Image(image) + } +} + +#[derive(Debug)] +struct Animation { + keyframes: Array, + options: KeyframeAnimationOptions, + _images: Vec, +} + +impl From for ImageState { + fn from(animation: Animation) -> Self { + Self::Animation(animation) + } +} + +#[derive(Debug)] +enum UrlType { + Plain(String), + Object(ObjectUrl), +} + +impl UrlType { + fn url(&self) -> &str { + match &self { + UrlType::Plain(url) => url, + UrlType::Object(object_url) => &object_url.0, + } + } +} + +#[derive(Debug)] +struct ObjectUrl(String); + +impl Drop for ObjectUrl { + fn drop(&mut self) { + Url::revoke_object_url(&self.0) + .expect("unexpected exception in `URL.revokeObjectURL()`"); + } +} + +#[derive(Debug)] +struct AnimationDropper(WebAnimation); + +impl Drop for AnimationDropper { + fn drop(&mut self) { + self.0.cancel() + } +} + +fn from_rgba( + window: &Window, + document: Document, + image: &CursorImage, +) -> impl Future> { + // 1. Create an `ImageData` from the RGBA data. + // 2. Create an `ImageBitmap` from the `ImageData`. + // 3. Draw `ImageBitmap` on an `HTMLCanvasElement`. + // 4. Create a `Blob` from the `HTMLCanvasElement`. + // 5. Create an object URL from the `Blob`. + // 6. Decode the image on an `HTMLImageElement` from the URL. + + // 1. Create an `ImageData` from the RGBA data. + // Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223 + #[cfg(target_feature = "atomics")] + // Can't share `SharedArrayBuffer` with `ImageData`. + let result = { + use js_sys::{Uint8Array, Uint8ClampedArray}; + use wasm_bindgen::JsValue; + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(js_namespace = ImageData)] + type ImageDataExt; + #[wasm_bindgen(catch, constructor, js_class = ImageData)] + fn new(array: Uint8ClampedArray, sw: u32) -> Result; + } + + let array = Uint8Array::new_with_length(image.rgba.len() as u32); + array.copy_from(&image.rgba); + let array = Uint8ClampedArray::new(&array); + ImageDataExt::new(array, image.width as u32) + .map(JsValue::from) + .map(ImageData::unchecked_from_js) + }; + #[cfg(not(target_feature = "atomics"))] + let result = ImageData::new_with_u8_clamped_array( + wasm_bindgen::Clamped(&image.rgba), + image.width as u32, + ); + let image_data = result.expect("found wrong image size"); + + // 2. Create an `ImageBitmap` from the `ImageData`. + // + // We call `createImageBitmap()` before spawning the future, + // to not have to clone the image buffer. + let mut options = ImageBitmapOptions::new(); + options.premultiply_alpha(PremultiplyAlpha::None); + let bitmap = JsFuture::from( + window + .create_image_bitmap_with_image_data_and_image_bitmap_options( + &image_data, + &options, + ) + .expect("unexpected exception in `createImageBitmap()`"), + ); + + let CursorImage { + width, + height, + hotspot_x, + hotspot_y, + .. + } = *image; + async move { + let bitmap: ImageBitmap = bitmap + .await + .expect("found invalid state in `ImageData`") + .unchecked_into(); + + let canvas: HtmlCanvasElement = document + .create_element("canvas") + .expect("invalid tag name") + .unchecked_into(); + #[allow(clippy::disallowed_methods)] + canvas.set_width(width as u32); + #[allow(clippy::disallowed_methods)] + canvas.set_height(height as u32); + + // 3. Draw `ImageBitmap` on an `HTMLCanvasElement`. + let context: ImageBitmapRenderingContext = canvas + .get_context("bitmaprenderer") + .expect("unexpected exception in `HTMLCanvasElement.getContext()`") + .expect("`bitmaprenderer` context unsupported") + .unchecked_into(); + context.transfer_from_image_bitmap(&bitmap); + drop(bitmap); + drop(context); + + // 4. Create a `Blob` from the `HTMLCanvasElement`. + // + // To keep the `Closure` alive until `HTMLCanvasElement.toBlob()` is done, + // we do the whole `Waker` strategy. Commonly on `Drop` the callback is aborted, + // but it would increase complexity and isn't possible in this case. + // Keep in mind that `HTMLCanvasElement.toBlob()` can call the callback immediately. + let value = Rc::new(RefCell::new(None)); + let waker = Rc::new(RefCell::>::new(None)); + let callback = Closure::once({ + let value = value.clone(); + let waker = waker.clone(); + move |blob: Option| { + *value.borrow_mut() = Some(blob); + if let Some(waker) = waker.borrow_mut().take() { + waker.wake(); + } + } + }); + canvas + .to_blob(callback.as_ref().unchecked_ref()) + .expect("failed with `SecurityError` despite only source coming from memory"); + let blob = future::poll_fn(|cx| { + if let Some(blob) = value.borrow_mut().take() { + Poll::Ready(blob) + } else { + *waker.borrow_mut() = Some(cx.waker().clone()); + Poll::Pending + } + }) + .await; + drop(canvas); + + let Some(blob) = blob else { + return Err(CustomCursorError::Blob); + }; + + // 5. Create an object URL from the `Blob`. + let url = Url::create_object_url_with_blob(&blob) + .expect("unexpected exception in `URL.createObjectURL()`"); + let url = UrlType::Object(ObjectUrl(url)); + + from_url(url, hotspot_x, hotspot_y).await + } +} + +async fn from_url( + url: UrlType, + hotspot_x: u16, + hotspot_y: u16, +) -> Result { + // 6. Decode the image on an `HTMLImageElement` from the URL. + let image = + HtmlImageElement::new().expect("unexpected exception in `new HtmlImageElement`"); + image.set_src(url.url()); + let result = JsFuture::from(image.decode()).await; + + if let Err(error) = result { + debug_assert!(error.has_type::()); + let error: DomException = error.unchecked_into(); + debug_assert_eq!(error.name(), "EncodingError"); + let error = error.message(); + + return Err(CustomCursorError::Decode(error)); + } + + Ok(Image { + style: format!("url({}) {hotspot_x} {hotspot_y}, auto", url.url()), + _object_url: match url { + UrlType::Plain(_) => None, + UrlType::Object(object_url) => Some(object_url), + }, + _image: image, + }) +} + +#[allow(clippy::await_holding_refcell_ref)] // false-positive +async fn from_animation( + main_thread: MainThreadMarker, + duration: Duration, + cursors: impl ExactSizeIterator, +) -> Result { + let keyframes = Array::new(); + let mut images = Vec::with_capacity(cursors.len()); + + for cursor in cursors { + let state = cursor.state.get(main_thread).borrow(); + + match state.deref() { + ImageState::Loading { notifier, .. } => { + let notified = notifier.notified(); + drop(state); + notified.await?; + } + ImageState::Failed(error) => return Err(error.clone()), + ImageState::Image(_) => drop(state), + ImageState::Animation(_) => { + unreachable!("check in `CustomCursorSource` failed") + } + } + + let state = cursor.state.get(main_thread).borrow(); + let style = match state.deref() { + ImageState::Image(Image { style, .. }) => style, + _ => unreachable!("found invalid state"), + }; + + let keyframe: Keyframe = Object::new().unchecked_into(); + keyframe.set_cursor(style); + keyframes.push(&keyframe); + drop(state); + + images.push(cursor); + } + + keyframes.push(&keyframes.get(0)); + + let options: KeyframeAnimationOptions = Object::new().unchecked_into(); + options.set_duration(duration.as_millis() as f64); + options.set_iterations(f64::INFINITY); + + Ok(Animation { + keyframes, + options, + _images: images, + }) +} + +#[wasm_bindgen] +extern "C" { + type CanvasAnimateExt; + + #[wasm_bindgen(method, js_name = animate)] + fn animate_with_keyframe_animation_options( + this: &CanvasAnimateExt, + keyframes: Option<&Object>, + options: &KeyframeAnimationOptions, + ) -> WebAnimation; + + #[derive(Debug)] + type WebAnimation; + + #[wasm_bindgen(method)] + fn cancel(this: &WebAnimation); + + #[wasm_bindgen(method)] + fn play(this: &WebAnimation); + + #[wasm_bindgen(extends = Object)] + type Keyframe; + + #[wasm_bindgen(method, setter, js_name = cursor)] + fn set_cursor(this: &Keyframe, value: &str); + + #[derive(Debug)] + #[wasm_bindgen(extends = Object)] + type KeyframeAnimationOptions; + + #[wasm_bindgen(method, setter, js_name = duration)] + fn set_duration(this: &KeyframeAnimationOptions, value: f64); + + #[wasm_bindgen(method, setter, js_name = iterations)] + fn set_iterations(this: &KeyframeAnimationOptions, value: f64); +} diff --git a/rio-window/src/platform_impl/web/device.rs b/rio-window/src/platform_impl/web/device.rs new file mode 100644 index 00000000..383bf3a1 --- /dev/null +++ b/rio-window/src/platform_impl/web/device.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(pub i32); + +impl DeviceId { + pub const unsafe fn dummy() -> Self { + Self(0) + } +} diff --git a/rio-window/src/platform_impl/web/error.rs b/rio-window/src/platform_impl/web/error.rs new file mode 100644 index 00000000..6995f2bc --- /dev/null +++ b/rio-window/src/platform_impl/web/error.rs @@ -0,0 +1,10 @@ +use std::fmt; + +#[derive(Debug)] +pub struct OsError(pub String); + +impl fmt::Display for OsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/rio-window/src/platform_impl/web/event_loop/mod.rs b/rio-window/src/platform_impl/web/event_loop/mod.rs new file mode 100644 index 00000000..af77b758 --- /dev/null +++ b/rio-window/src/platform_impl/web/event_loop/mod.rs @@ -0,0 +1,113 @@ +use std::marker::PhantomData; +use std::sync::mpsc::{self, Receiver, Sender}; + +use crate::error::EventLoopError; +use crate::event::Event; +use crate::event_loop::ActiveEventLoop as RootActiveEventLoop; + +use super::{backend, device, window}; + +mod proxy; +pub(crate) mod runner; +mod state; +mod window_target; + +pub(crate) use proxy::EventLoopProxy; +pub(crate) use window_target::{ActiveEventLoop, OwnedDisplayHandle}; + +pub struct EventLoop { + elw: RootActiveEventLoop, + user_event_sender: Sender, + user_event_receiver: Receiver, +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes {} + +impl EventLoop { + pub(crate) fn new( + _: &PlatformSpecificEventLoopAttributes, + ) -> Result { + let (user_event_sender, user_event_receiver) = mpsc::channel(); + let elw = RootActiveEventLoop { + p: ActiveEventLoop::new(), + _marker: PhantomData, + }; + Ok(EventLoop { + elw, + user_event_sender, + user_event_receiver, + }) + } + + pub fn run(self, mut event_handler: F) -> ! + where + F: FnMut(Event, &RootActiveEventLoop), + { + let target = RootActiveEventLoop { + p: self.elw.p.clone(), + _marker: PhantomData, + }; + + // SAFETY: Don't use `move` to make sure we leak the `event_handler` and `target`. + let handler: Box)> = Box::new(|event| { + let event = match event.map_nonuser_event() { + Ok(event) => event, + Err(Event::UserEvent(())) => Event::UserEvent( + self.user_event_receiver + .try_recv() + .expect("handler woken up without user event"), + ), + Err(_) => unreachable!(), + }; + event_handler(event, &target) + }); + // SAFETY: The `transmute` is necessary because `run()` requires `'static`. This is safe + // because this function will never return and all resources not cleaned up by the point we + // `throw` will leak, making this actually `'static`. + let handler = unsafe { std::mem::transmute(handler) }; + self.elw.p.run(handler, false); + + // Throw an exception to break out of Rust execution and use unreachable to tell the + // compiler this function won't return, giving it a return type of '!' + backend::throw( + "Using exceptions for control flow, don't mind me. This isn't actually an error!", + ); + + unreachable!(); + } + + pub fn spawn(self, mut event_handler: F) + where + F: 'static + FnMut(Event, &RootActiveEventLoop), + { + let target = RootActiveEventLoop { + p: self.elw.p.clone(), + _marker: PhantomData, + }; + + self.elw.p.run( + Box::new(move |event| { + let event = match event.map_nonuser_event() { + Ok(event) => event, + Err(Event::UserEvent(())) => Event::UserEvent( + self.user_event_receiver + .try_recv() + .expect("handler woken up without user event"), + ), + Err(_) => unreachable!(), + }; + event_handler(event, &target) + }), + true, + ); + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.elw.p.waker(), self.user_event_sender.clone()) + } + + pub fn window_target(&self) -> &RootActiveEventLoop { + &self.elw + } +} diff --git a/rio-window/src/platform_impl/web/event_loop/proxy.rs b/rio-window/src/platform_impl/web/event_loop/proxy.rs new file mode 100644 index 00000000..691efa32 --- /dev/null +++ b/rio-window/src/platform_impl/web/event_loop/proxy.rs @@ -0,0 +1,34 @@ +use std::rc::Weak; +use std::sync::mpsc::{SendError, Sender}; + +use super::runner::Execution; +use crate::event_loop::EventLoopClosed; +use crate::platform_impl::platform::r#async::Waker; + +pub struct EventLoopProxy { + runner: Waker>, + sender: Sender, +} + +impl EventLoopProxy { + pub fn new(runner: Waker>, sender: Sender) -> Self { + Self { runner, sender } + } + + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.sender + .send(event) + .map_err(|SendError(event)| EventLoopClosed(event))?; + self.runner.wake(); + Ok(()) + } +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + Self { + runner: self.runner.clone(), + sender: self.sender.clone(), + } + } +} diff --git a/rio-window/src/platform_impl/web/event_loop/runner.rs b/rio-window/src/platform_impl/web/event_loop/runner.rs new file mode 100644 index 00000000..a34aad26 --- /dev/null +++ b/rio-window/src/platform_impl/web/event_loop/runner.rs @@ -0,0 +1,842 @@ +use super::super::main_thread::MainThreadMarker; +use super::super::DeviceId; +use super::backend; +use super::state::State; +use crate::dpi::PhysicalSize; +use crate::event::{ + DeviceEvent, DeviceId as RootDeviceId, ElementState, Event, RawKeyEvent, StartCause, + WindowEvent, +}; +use crate::event_loop::{ControlFlow, DeviceEvents}; +use crate::platform::web::PollStrategy; +use crate::platform_impl::platform::backend::EventListenerHandle; +use crate::platform_impl::platform::r#async::{DispatchRunner, Waker, WakerSpawner}; +use crate::platform_impl::platform::window::Inner; +use crate::window::WindowId; + +use std::cell::{Cell, RefCell}; +use std::collections::{HashSet, VecDeque}; +use std::iter; +use std::ops::Deref; +use std::rc::{Rc, Weak}; +use wasm_bindgen::prelude::Closure; +use web_sys::{Document, KeyboardEvent, PageTransitionEvent, PointerEvent, WheelEvent}; +use web_time::{Duration, Instant}; + +pub struct Shared(Rc); + +pub(super) type EventHandler = dyn FnMut(Event<()>); + +impl Clone for Shared { + fn clone(&self) -> Self { + Shared(self.0.clone()) + } +} + +type OnEventHandle = RefCell>>; + +pub struct Execution { + main_thread: MainThreadMarker, + proxy_spawner: WakerSpawner>, + control_flow: Cell, + poll_strategy: Cell, + exit: Cell, + runner: RefCell, + suspended: Cell, + event_loop_recreation: Cell, + events: RefCell>, + id: RefCell, + window: web_sys::Window, + document: Document, + #[allow(clippy::type_complexity)] + all_canvases: RefCell< + Vec<( + WindowId, + Weak>, + DispatchRunner, + )>, + >, + redraw_pending: RefCell>, + destroy_pending: RefCell>, + page_transition_event_handle: RefCell>, + device_events: Cell, + on_mouse_move: OnEventHandle, + on_wheel: OnEventHandle, + on_mouse_press: OnEventHandle, + on_mouse_release: OnEventHandle, + on_key_press: OnEventHandle, + on_key_release: OnEventHandle, + on_visibility_change: OnEventHandle, +} + +enum RunnerEnum { + /// The `EventLoop` is created but not being run. + Pending, + /// The `EventLoop` is being run. + Running(Runner), + /// The `EventLoop` is exited after being started with `EventLoop::run_app`. Since + /// `EventLoop::run_app` takes ownership of the `EventLoop`, we can be certain + /// that this event loop will never be run again. + Destroyed, +} + +impl RunnerEnum { + fn maybe_runner(&self) -> Option<&Runner> { + match self { + RunnerEnum::Running(runner) => Some(runner), + _ => None, + } + } +} + +struct Runner { + state: State, + event_handler: Box, +} + +impl Runner { + pub fn new(event_handler: Box) -> Self { + Runner { + state: State::Init, + event_handler, + } + } + + /// Returns the corresponding `StartCause` for the current `state`, or `None` + /// when in `Exit` state. + fn maybe_start_cause(&self) -> Option { + Some(match self.state { + State::Init => StartCause::Init, + State::Poll { .. } => StartCause::Poll, + State::Wait { start } => StartCause::WaitCancelled { + start, + requested_resume: None, + }, + State::WaitUntil { start, end, .. } => StartCause::WaitCancelled { + start, + requested_resume: Some(end), + }, + State::Exit => return None, + }) + } + + fn handle_single_event(&mut self, runner: &Shared, event: impl Into) { + match event.into() { + EventWrapper::Event(event) => (self.event_handler)(event), + EventWrapper::ScaleChange { + canvas, + size, + scale, + } => { + if let Some(canvas) = canvas.upgrade() { + canvas.borrow().handle_scale_change( + runner, + |event| (self.event_handler)(event), + size, + scale, + ) + } + } + } + } +} + +impl Shared { + pub fn new() -> Self { + let main_thread = + MainThreadMarker::new().expect("only callable from inside the `Window`"); + #[allow(clippy::disallowed_methods)] + let window = web_sys::window().expect("only callable from inside the `Window`"); + #[allow(clippy::disallowed_methods)] + let document = window.document().expect("Failed to obtain document"); + + Shared(Rc::::new_cyclic(|weak| { + let proxy_spawner = + WakerSpawner::new(main_thread, weak.clone(), |runner, count| { + if let Some(runner) = runner.upgrade() { + Shared(runner) + .send_events(iter::repeat(Event::UserEvent(())).take(count)) + } + }) + .expect("`EventLoop` has to be created in the main thread"); + + Execution { + main_thread, + proxy_spawner, + control_flow: Cell::new(ControlFlow::default()), + poll_strategy: Cell::new(PollStrategy::default()), + exit: Cell::new(false), + runner: RefCell::new(RunnerEnum::Pending), + suspended: Cell::new(false), + event_loop_recreation: Cell::new(false), + events: RefCell::new(VecDeque::new()), + window, + document, + id: RefCell::new(0), + all_canvases: RefCell::new(Vec::new()), + redraw_pending: RefCell::new(HashSet::new()), + destroy_pending: RefCell::new(VecDeque::new()), + page_transition_event_handle: RefCell::new(None), + device_events: Cell::default(), + on_mouse_move: RefCell::new(None), + on_wheel: RefCell::new(None), + on_mouse_press: RefCell::new(None), + on_mouse_release: RefCell::new(None), + on_key_press: RefCell::new(None), + on_key_release: RefCell::new(None), + on_visibility_change: RefCell::new(None), + } + })) + } + + pub fn main_thread(&self) -> MainThreadMarker { + self.0.main_thread + } + + pub fn window(&self) -> &web_sys::Window { + &self.0.window + } + + pub fn document(&self) -> &Document { + &self.0.document + } + + pub fn add_canvas( + &self, + id: WindowId, + canvas: Weak>, + runner: DispatchRunner, + ) { + self.0.all_canvases.borrow_mut().push((id, canvas, runner)); + } + + pub fn notify_destroy_window(&self, id: WindowId) { + self.0.destroy_pending.borrow_mut().push_back(id); + } + + // Set the event callback to use for the event loop runner + // This the event callback is a fairly thin layer over the user-provided callback that closes + // over a RootActiveEventLoop reference + pub fn set_listener(&self, event_handler: Box) { + { + let mut runner = self.0.runner.borrow_mut(); + assert!(matches!(*runner, RunnerEnum::Pending)); + *runner = RunnerEnum::Running(Runner::new(event_handler)); + } + self.init(); + + *self.0.page_transition_event_handle.borrow_mut() = + Some(backend::on_page_transition( + self.window().clone(), + { + let runner = self.clone(); + move |event: PageTransitionEvent| { + if event.persisted() { + runner.0.suspended.set(false); + runner.send_event(Event::Resumed); + } + } + }, + { + let runner = self.clone(); + move |event: PageTransitionEvent| { + runner.0.suspended.set(true); + if event.persisted() { + runner.send_event(Event::Suspended); + } else { + runner.handle_unload(); + } + } + }, + )); + + let runner = self.clone(); + let window = self.window().clone(); + *self.0.on_mouse_move.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "pointermove", + Closure::new(move |event: PointerEvent| { + if !runner.device_events() { + return; + } + + let pointer_type = event.pointer_type(); + + if pointer_type != "mouse" { + return; + } + + // chorded button event + let device_id = RootDeviceId(DeviceId(event.pointer_id())); + + if let Some(button) = backend::event::mouse_button(&event) { + debug_assert_eq!( + pointer_type, "mouse", + "expect pointer type of a chorded button event to be a mouse" + ); + + let state = if backend::event::mouse_buttons(&event) + .contains(button.into()) + { + ElementState::Pressed + } else { + ElementState::Released + }; + + runner.send_event(Event::DeviceEvent { + device_id, + event: DeviceEvent::Button { + button: button.to_id(), + state, + }, + }); + + return; + } + + // pointer move event + let mut delta = backend::event::MouseDelta::init(&window, &event); + runner.send_events(backend::event::pointer_move_event(event).flat_map( + |event| { + let delta = delta + .delta(&event) + .to_physical(backend::scale_factor(&window)); + + let x_motion = (delta.x != 0.0).then_some(Event::DeviceEvent { + device_id, + event: DeviceEvent::Motion { + axis: 0, + value: delta.x, + }, + }); + + let y_motion = (delta.y != 0.0).then_some(Event::DeviceEvent { + device_id, + event: DeviceEvent::Motion { + axis: 1, + value: delta.y, + }, + }); + + x_motion.into_iter().chain(y_motion).chain(iter::once( + Event::DeviceEvent { + device_id, + event: DeviceEvent::MouseMotion { + delta: (delta.x, delta.y), + }, + }, + )) + }, + )); + }), + )); + let runner = self.clone(); + let window = self.window().clone(); + *self.0.on_wheel.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "wheel", + Closure::new(move |event: WheelEvent| { + if !runner.device_events() { + return; + } + + if let Some(delta) = backend::event::mouse_scroll_delta(&window, &event) { + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId(0)), + event: DeviceEvent::MouseWheel { delta }, + }); + } + }), + )); + let runner = self.clone(); + *self.0.on_mouse_press.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "pointerdown", + Closure::new(move |event: PointerEvent| { + if !runner.device_events() { + return; + } + + if event.pointer_type() != "mouse" { + return; + } + + let button = backend::event::mouse_button(&event) + .expect("no mouse button pressed"); + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId(event.pointer_id())), + event: DeviceEvent::Button { + button: button.to_id(), + state: ElementState::Pressed, + }, + }); + }), + )); + let runner = self.clone(); + *self.0.on_mouse_release.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "pointerup", + Closure::new(move |event: PointerEvent| { + if !runner.device_events() { + return; + } + + if event.pointer_type() != "mouse" { + return; + } + + let button = backend::event::mouse_button(&event) + .expect("no mouse button pressed"); + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId(event.pointer_id())), + event: DeviceEvent::Button { + button: button.to_id(), + state: ElementState::Released, + }, + }); + }), + )); + let runner = self.clone(); + *self.0.on_key_press.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "keydown", + Closure::new(move |event: KeyboardEvent| { + if !runner.device_events() { + return; + } + + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(unsafe { DeviceId::dummy() }), + event: DeviceEvent::Key(RawKeyEvent { + physical_key: backend::event::key_code(&event), + state: ElementState::Pressed, + }), + }); + }), + )); + let runner = self.clone(); + *self.0.on_key_release.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "keyup", + Closure::new(move |event: KeyboardEvent| { + if !runner.device_events() { + return; + } + + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(unsafe { DeviceId::dummy() }), + event: DeviceEvent::Key(RawKeyEvent { + physical_key: backend::event::key_code(&event), + state: ElementState::Released, + }), + }); + }), + )); + let runner = self.clone(); + *self.0.on_visibility_change.borrow_mut() = Some(EventListenerHandle::new( + // Safari <14 doesn't support the `visibilitychange` event on `Window`. + self.document().clone(), + "visibilitychange", + Closure::new(move |_| { + if !runner.0.suspended.get() { + for (id, canvas, _) in &*runner.0.all_canvases.borrow() { + if let Some(canvas) = canvas.upgrade() { + let is_visible = backend::is_visible(runner.document()); + // only fire if: + // - not visible and intersects + // - not visible and we don't know if it intersects yet + // - visible and intersects + if let (false, Some(true) | None) | (true, Some(true)) = + (is_visible, canvas.borrow().is_intersecting) + { + runner.send_event(Event::WindowEvent { + window_id: *id, + event: WindowEvent::Occluded(!is_visible), + }); + } + } + } + } + }), + )); + } + + // Generate a strictly increasing ID + // This is used to differentiate windows when handling events + pub fn generate_id(&self) -> u32 { + let mut id = self.0.id.borrow_mut(); + *id += 1; + + *id + } + + pub fn request_redraw(&self, id: WindowId) { + self.0.redraw_pending.borrow_mut().insert(id); + self.send_events::(iter::empty()); + } + + pub fn init(&self) { + // NB: For consistency all platforms must emit a 'resumed' event even though web + // applications don't themselves have a formal suspend/resume lifecycle. + self.run_until_cleared( + [Event::NewEvents(StartCause::Init), Event::Resumed].into_iter(), + ); + } + + // Run the polling logic for the Poll ControlFlow, which involves clearing the queue + pub fn poll(&self) { + let start_cause = Event::NewEvents(StartCause::Poll); + self.run_until_cleared(iter::once(start_cause)); + } + + // Run the logic for waking from a WaitUntil, which involves clearing the queue + // Generally there shouldn't be events built up when this is called + pub fn resume_time_reached(&self, start: Instant, requested_resume: Instant) { + let start_cause = Event::NewEvents(StartCause::ResumeTimeReached { + start, + requested_resume, + }); + self.run_until_cleared(iter::once(start_cause)); + } + + // Add an event to the event loop runner, from the user or an event handler + // + // It will determine if the event should be immediately sent to the user or buffered for later + pub(crate) fn send_event>(&self, event: E) { + self.send_events(iter::once(event)); + } + + // Add a series of events to the event loop runner + // + // It will determine if the event should be immediately sent to the user or buffered for later + pub(crate) fn send_events>( + &self, + events: impl IntoIterator, + ) { + // If the event loop is closed, it should discard any new events + if self.is_closed() { + return; + } + // If we can run the event processing right now, or need to queue this and wait for later + let mut process_immediately = true; + match self.0.runner.try_borrow().as_ref().map(Deref::deref) { + Ok(RunnerEnum::Running(ref runner)) => { + // If we're currently polling, queue this and wait for the poll() method to be + // called + if let State::Poll { .. } = runner.state { + process_immediately = false; + } + } + Ok(RunnerEnum::Pending) => { + // The runner still hasn't been attached: queue this event and wait for it to be + process_immediately = false; + } + // Some other code is mutating the runner, which most likely means + // the event loop is running and busy. So we queue this event for + // it to be processed later. + Err(_) => { + process_immediately = false; + } + // This is unreachable since `self.is_closed() == true`. + Ok(RunnerEnum::Destroyed) => unreachable!(), + } + if !process_immediately { + // Queue these events to look at later + self.0 + .events + .borrow_mut() + .extend(events.into_iter().map(Into::into)); + return; + } + // At this point, we know this is a fresh set of events + // Now we determine why new events are incoming, and handle the events + let start_cause = match (self.0.runner.borrow().maybe_runner()) + .unwrap_or_else(|| { + unreachable!("The runner cannot process events when it is not attached") + }) + .maybe_start_cause() + { + Some(c) => c, + // If we're in the exit state, don't do event processing + None => return, + }; + // Take the start event, then the events provided to this function, and run an iteration of + // the event loop + let start_event = Event::NewEvents(start_cause); + let events = iter::once(EventWrapper::from(start_event)) + .chain(events.into_iter().map(Into::into)); + self.run_until_cleared(events); + } + + // Process the destroy-pending windows. This should only be called from + // `run_until_cleared`, somewhere between emitting `NewEvents` and `AboutToWait`. + fn process_destroy_pending_windows(&self) { + while let Some(id) = self.0.destroy_pending.borrow_mut().pop_front() { + self.0 + .all_canvases + .borrow_mut() + .retain(|&(item_id, ..)| item_id != id); + self.handle_event(Event::WindowEvent { + window_id: id, + event: crate::event::WindowEvent::Destroyed, + }); + self.0.redraw_pending.borrow_mut().remove(&id); + } + } + + // Given the set of new events, run the event loop until the main events and redraw events are + // cleared + // + // This will also process any events that have been queued or that are queued during processing + fn run_until_cleared>(&self, events: impl Iterator) { + for event in events { + self.handle_event(event.into()); + } + self.process_destroy_pending_windows(); + + // Collect all of the redraw events to avoid double-locking the RefCell + let redraw_events: Vec = + self.0.redraw_pending.borrow_mut().drain().collect(); + for window_id in redraw_events { + self.handle_event(Event::WindowEvent { + window_id, + event: WindowEvent::RedrawRequested, + }); + } + + self.handle_event(Event::AboutToWait); + + self.apply_control_flow(); + // If the event loop is closed, it has been closed this iteration and now the closing + // event should be emitted + if self.is_closed() { + self.handle_loop_destroyed(); + } + } + + fn handle_unload(&self) { + self.exit(); + self.apply_control_flow(); + // We don't call `handle_loop_destroyed` here because we don't need to + // perform cleanup when the web browser is going to destroy the page. + self.handle_event(Event::LoopExiting); + } + + // handle_event takes in events and either queues them or applies a callback + // + // It should only ever be called from `run_until_cleared`. + fn handle_event(&self, event: impl Into) { + if self.is_closed() { + self.exit(); + } + match *self.0.runner.borrow_mut() { + RunnerEnum::Running(ref mut runner) => { + runner.handle_single_event(self, event); + } + // If an event is being handled without a runner somehow, add it to the event queue so + // it will eventually be processed + RunnerEnum::Pending => self.0.events.borrow_mut().push_back(event.into()), + // If the Runner has been destroyed, there is nothing to do. + RunnerEnum::Destroyed => return, + } + + let is_closed = self.exiting(); + + // Don't take events out of the queue if the loop is closed or the runner doesn't exist + // If the runner doesn't exist and this method recurses, it will recurse infinitely + if !is_closed && self.0.runner.borrow().maybe_runner().is_some() { + // Pre-fetch window commands to avoid having to wait until the next event loop cycle + // and potentially block other threads in the meantime. + for (_, window, runner) in self.0.all_canvases.borrow().iter() { + if let Some(window) = window.upgrade() { + runner.run(); + drop(window) + } + } + + // Take an event out of the queue and handle it + // Make sure not to let the borrow_mut live during the next handle_event + let event = { + let mut events = self.0.events.borrow_mut(); + + // Pre-fetch `UserEvent`s to avoid having to wait until the next event loop cycle. + events.extend( + iter::repeat(Event::UserEvent(())) + .take(self.0.proxy_spawner.fetch()) + .map(EventWrapper::from), + ); + + events.pop_front() + }; + if let Some(event) = event { + self.handle_event(event); + } + } + } + + // Apply the new ControlFlow that has been selected by the user + // Start any necessary timeouts etc + fn apply_control_flow(&self) { + let new_state = if self.exiting() { + State::Exit + } else { + match self.control_flow() { + ControlFlow::Poll => { + let cloned = self.clone(); + State::Poll { + _request: backend::Schedule::new( + self.poll_strategy(), + self.window(), + move || cloned.poll(), + ), + } + } + ControlFlow::Wait => State::Wait { + start: Instant::now(), + }, + ControlFlow::WaitUntil(end) => { + let start = Instant::now(); + + let delay = if end <= start { + Duration::from_millis(0) + } else { + end - start + }; + + let cloned = self.clone(); + + State::WaitUntil { + start, + end, + _timeout: backend::Schedule::new_with_duration( + self.window(), + move || cloned.resume_time_reached(start, end), + delay, + ), + } + } + } + }; + + if let RunnerEnum::Running(ref mut runner) = *self.0.runner.borrow_mut() { + runner.state = new_state; + } + } + + fn handle_loop_destroyed(&self) { + self.handle_event(Event::LoopExiting); + let all_canvases = std::mem::take(&mut *self.0.all_canvases.borrow_mut()); + *self.0.page_transition_event_handle.borrow_mut() = None; + *self.0.on_mouse_move.borrow_mut() = None; + *self.0.on_wheel.borrow_mut() = None; + *self.0.on_mouse_press.borrow_mut() = None; + *self.0.on_mouse_release.borrow_mut() = None; + *self.0.on_key_press.borrow_mut() = None; + *self.0.on_key_release.borrow_mut() = None; + *self.0.on_visibility_change.borrow_mut() = None; + // Dropping the `Runner` drops the event handler closure, which will in + // turn drop all `Window`s moved into the closure. + *self.0.runner.borrow_mut() = RunnerEnum::Destroyed; + for (_, canvas, _) in all_canvases { + // In case any remaining `Window`s are still not dropped, we will need + // to explicitly remove the event handlers associated with their canvases. + if let Some(canvas) = canvas.upgrade() { + let mut canvas = canvas.borrow_mut(); + canvas.remove_listeners(); + } + } + // At this point, the `self.0` `Rc` should only be strongly referenced + // by the following: + // * `self`, i.e. the item which triggered this event loop wakeup, which is usually a + // `wasm-bindgen` `Closure`, which will be dropped after returning to the JS glue code. + // * The `ActiveEventLoop` leaked inside `EventLoop::run_app` due to the JS exception thrown + // at the end. + // * For each undropped `Window`: + // * The `register_redraw_request` closure. + // * The `destroy_fn` closure. + if self.0.event_loop_recreation.get() { + crate::event_loop::EventLoopBuilder::<()>::allow_event_loop_recreation(); + } + } + + // Check if the event loop is currently closed + fn is_closed(&self) -> bool { + match self.0.runner.try_borrow().as_ref().map(Deref::deref) { + Ok(RunnerEnum::Running(runner)) => runner.state.exiting(), + // The event loop is not closed since it is not initialized. + Ok(RunnerEnum::Pending) => false, + // The event loop is closed since it has been destroyed. + Ok(RunnerEnum::Destroyed) => true, + // Some other code is mutating the runner, which most likely means + // the event loop is running and busy. + Err(_) => false, + } + } + + pub fn listen_device_events(&self, allowed: DeviceEvents) { + self.0.device_events.set(allowed) + } + + fn device_events(&self) -> bool { + match self.0.device_events.get() { + DeviceEvents::Always => true, + DeviceEvents::WhenFocused => { + self.0.all_canvases.borrow().iter().any(|(_, canvas, _)| { + if let Some(canvas) = canvas.upgrade() { + canvas.borrow().has_focus.get() + } else { + false + } + }) + } + DeviceEvents::Never => false, + } + } + + pub fn event_loop_recreation(&self, allow: bool) { + self.0.event_loop_recreation.set(allow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.0.control_flow.get() + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.0.control_flow.set(control_flow) + } + + pub(crate) fn exit(&self) { + self.0.exit.set(true) + } + + pub(crate) fn exiting(&self) -> bool { + self.0.exit.get() + } + + pub(crate) fn set_poll_strategy(&self, strategy: PollStrategy) { + self.0.poll_strategy.set(strategy) + } + + pub(crate) fn poll_strategy(&self) -> PollStrategy { + self.0.poll_strategy.get() + } + + pub(crate) fn waker(&self) -> Waker> { + self.0.proxy_spawner.waker() + } +} + +pub(crate) enum EventWrapper { + Event(Event<()>), + ScaleChange { + canvas: Weak>, + size: PhysicalSize, + scale: f64, + }, +} + +impl From> for EventWrapper { + fn from(value: Event<()>) -> Self { + Self::Event(value) + } +} diff --git a/rio-window/src/platform_impl/web/event_loop/state.rs b/rio-window/src/platform_impl/web/event_loop/state.rs new file mode 100644 index 00000000..309a93fd --- /dev/null +++ b/rio-window/src/platform_impl/web/event_loop/state.rs @@ -0,0 +1,26 @@ +use super::backend; + +use web_time::Instant; + +#[derive(Debug)] +pub enum State { + Init, + WaitUntil { + _timeout: backend::Schedule, + start: Instant, + end: Instant, + }, + Wait { + start: Instant, + }, + Poll { + _request: backend::Schedule, + }, + Exit, +} + +impl State { + pub fn exiting(&self) -> bool { + matches!(self, State::Exit) + } +} diff --git a/rio-window/src/platform_impl/web/event_loop/window_target.rs b/rio-window/src/platform_impl/web/event_loop/window_target.rs new file mode 100644 index 00000000..442d2b9e --- /dev/null +++ b/rio-window/src/platform_impl/web/event_loop/window_target.rs @@ -0,0 +1,753 @@ +use std::cell::{Cell, RefCell}; +use std::clone::Clone; +use std::collections::vec_deque::IntoIter as VecDequeIter; +use std::collections::VecDeque; +use std::iter; +use std::rc::{Rc, Weak}; + +use web_sys::Element; + +use super::super::monitor::MonitorHandle; +use super::super::KeyEventExtra; +use super::device::DeviceId; +use super::runner::{EventWrapper, Execution}; +use super::window::WindowId; +use super::{backend, runner}; +use crate::event::{ + DeviceId as RootDeviceId, ElementState, Event, KeyEvent, Touch, TouchPhase, + WindowEvent, +}; +use crate::event_loop::{ControlFlow, DeviceEvents}; +use crate::keyboard::ModifiersState; +use crate::platform::web::{CustomCursorFuture, PollStrategy}; +use crate::platform_impl::platform::cursor::CustomCursor; +use crate::platform_impl::platform::r#async::Waker; +use crate::window::{ + CustomCursor as RootCustomCursor, CustomCursorSource, Theme, WindowId as RootWindowId, +}; + +#[derive(Default)] +struct ModifiersShared(Rc>); + +impl ModifiersShared { + fn set(&self, new: ModifiersState) { + self.0.set(new) + } + + fn get(&self) -> ModifiersState { + self.0.get() + } +} + +impl Clone for ModifiersShared { + fn clone(&self) -> Self { + Self(Rc::clone(&self.0)) + } +} + +#[derive(Clone)] +pub struct ActiveEventLoop { + pub(crate) runner: runner::Shared, + modifiers: ModifiersShared, +} + +impl ActiveEventLoop { + pub fn new() -> Self { + Self { + runner: runner::Shared::new(), + modifiers: ModifiersShared::default(), + } + } + + pub fn run( + &self, + event_handler: Box, + event_loop_recreation: bool, + ) { + self.runner.event_loop_recreation(event_loop_recreation); + self.runner.set_listener(event_handler); + } + + pub fn generate_id(&self) -> WindowId { + WindowId(self.runner.generate_id()) + } + + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> RootCustomCursor { + RootCustomCursor { + inner: CustomCursor::new(self, source.inner), + } + } + + pub fn create_custom_cursor_async( + &self, + source: CustomCursorSource, + ) -> CustomCursorFuture { + CustomCursorFuture(CustomCursor::new_async(self, source.inner)) + } + + pub fn register(&self, canvas: &Rc>, id: WindowId) { + let canvas_clone = canvas.clone(); + canvas.on_touch_start(); + + let runner = self.runner.clone(); + let has_focus = canvas.has_focus.clone(); + let modifiers = self.modifiers.clone(); + canvas.on_blur(move || { + has_focus.set(false); + + let clear_modifiers = (!modifiers.get().is_empty()).then(|| { + modifiers.set(ModifiersState::empty()); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(ModifiersState::empty().into()), + } + }); + + runner.send_events(clear_modifiers.into_iter().chain(iter::once( + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Focused(false), + }, + ))); + }); + + let runner = self.runner.clone(); + let has_focus = canvas.has_focus.clone(); + canvas.on_focus(move || { + if !has_focus.replace(true) { + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Focused(true), + }); + } + }); + + // It is possible that at this point the canvas has + // been focused before the callback can be called. + let focused = canvas + .document() + .active_element() + .filter(|element| { + let canvas: &Element = canvas.raw(); + element == canvas + }) + .is_some(); + + if focused { + canvas.has_focus.set(true); + self.runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Focused(true), + }) + } + + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + canvas.on_keyboard_press( + move |physical_key, logical_key, text, location, repeat, active_modifiers| { + let modifiers_changed = + (modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let device_id = RootDeviceId(unsafe { DeviceId::dummy() }); + + runner.send_events( + iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::KeyboardInput { + device_id, + event: KeyEvent { + physical_key, + logical_key, + text, + location, + state: ElementState::Pressed, + repeat, + platform_specific: KeyEventExtra, + }, + is_synthetic: false, + }, + }) + .chain(modifiers_changed), + ); + }, + ); + + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + canvas.on_keyboard_release( + move |physical_key, logical_key, text, location, repeat, active_modifiers| { + let modifiers_changed = + (modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let device_id = RootDeviceId(unsafe { DeviceId::dummy() }); + + runner.send_events( + iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::KeyboardInput { + device_id, + event: KeyEvent { + physical_key, + logical_key, + text, + location, + state: ElementState::Released, + repeat, + platform_specific: KeyEventExtra, + }, + is_synthetic: false, + }, + }) + .chain(modifiers_changed), + ) + }, + ); + + let has_focus = canvas.has_focus.clone(); + canvas.on_cursor_leave({ + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, pointer_id| { + let focus = (has_focus.get() && modifiers.get() != active_modifiers) + .then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let pointer = pointer_id.map(|pointer_id| Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorLeft { + device_id: RootDeviceId(DeviceId(pointer_id)), + }, + }); + + if focus.is_some() || pointer.is_some() { + runner.send_events(focus.into_iter().chain(pointer)) + } + } + }); + + canvas.on_cursor_enter({ + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, pointer_id| { + let focus = (has_focus.get() && modifiers.get() != active_modifiers) + .then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let pointer = pointer_id.map(|pointer_id| Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorEntered { + device_id: RootDeviceId(DeviceId(pointer_id)), + }, + }); + + if focus.is_some() || pointer.is_some() { + runner.send_events(focus.into_iter().chain(pointer)) + } + } + }); + + canvas.on_cursor_move( + { + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers| { + if has_focus.get() && modifiers.get() != active_modifiers { + modifiers.set(active_modifiers); + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + }) + } + } + }, + { + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, pointer_id, events| { + let modifiers = (has_focus.get() + && modifiers.get() != active_modifiers) + .then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged( + active_modifiers.into(), + ), + } + }); + + runner.send_events(modifiers.into_iter().chain(events.flat_map( + |position| { + let device_id = RootDeviceId(DeviceId(pointer_id)); + + iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorMoved { + device_id, + position, + }, + }) + }, + ))); + } + }, + { + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, device_id, events| { + let modifiers = (has_focus.get() + && modifiers.get() != active_modifiers) + .then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged( + active_modifiers.into(), + ), + } + }); + + runner.send_events(modifiers.into_iter().chain(events.map( + |(location, force)| Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Moved, + force: Some(force), + location, + }), + }, + ))); + } + }, + { + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, + pointer_id, + position: crate::dpi::PhysicalPosition, + buttons, + button| { + let modifiers = (has_focus.get() + && modifiers.get() != active_modifiers) + .then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged( + active_modifiers.into(), + ), + } + }); + + let device_id = RootDeviceId(DeviceId(pointer_id)); + + let state = if buttons.contains(button.into()) { + ElementState::Pressed + } else { + ElementState::Released + }; + + // A chorded button event may come in without any prior CursorMoved events, + // therefore we should send a CursorMoved event to make sure that the + // user code has the correct cursor position. + runner.send_events(modifiers.into_iter().chain([ + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorMoved { + device_id, + position, + }, + }, + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseInput { + device_id, + state, + button, + }, + }, + ])); + } + }, + ); + + canvas.on_mouse_press( + { + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers| { + if modifiers.get() != active_modifiers { + modifiers.set(active_modifiers); + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + }) + } + } + }, + { + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, pointer_id, position, button| { + let modifiers = (modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let device_id: RootDeviceId = RootDeviceId(DeviceId(pointer_id)); + + // A mouse down event may come in without any prior CursorMoved events, + // therefore we should send a CursorMoved event to make sure that the + // user code has the correct cursor position. + runner.send_events(modifiers.into_iter().chain([ + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorMoved { + device_id, + position, + }, + }, + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseInput { + device_id, + state: ElementState::Pressed, + button, + }, + }, + ])); + } + }, + { + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, device_id, location, force| { + let modifiers = (modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + runner.send_events(modifiers.into_iter().chain(iter::once( + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Started, + force: Some(force), + location, + }), + }, + ))) + } + }, + ); + + canvas.on_mouse_release( + { + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers| { + if has_focus.get() && modifiers.get() != active_modifiers { + modifiers.set(active_modifiers); + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + }); + } + } + }, + { + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, pointer_id, position, button| { + let modifiers = (has_focus.get() + && modifiers.get() != active_modifiers) + .then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged( + active_modifiers.into(), + ), + } + }); + + let device_id: RootDeviceId = RootDeviceId(DeviceId(pointer_id)); + + // A mouse up event may come in without any prior CursorMoved events, + // therefore we should send a CursorMoved event to make sure that the + // user code has the correct cursor position. + runner.send_events(modifiers.into_iter().chain([ + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorMoved { + device_id, + position, + }, + }, + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseInput { + device_id, + state: ElementState::Released, + button, + }, + }, + ])); + } + }, + { + let runner_touch = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, device_id, location, force| { + let modifiers = (has_focus.get() + && modifiers.get() != active_modifiers) + .then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged( + active_modifiers.into(), + ), + } + }); + + runner_touch.send_events(modifiers.into_iter().chain(iter::once( + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Ended, + force: Some(force), + location, + }), + }, + ))); + } + }, + ); + + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + canvas.on_mouse_wheel(move |pointer_id, delta, active_modifiers| { + let modifiers_changed = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + runner.send_events(modifiers_changed.into_iter().chain(iter::once( + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseWheel { + device_id: RootDeviceId(DeviceId(pointer_id)), + delta, + phase: TouchPhase::Moved, + }, + }, + ))); + }); + + let runner = self.runner.clone(); + canvas.on_touch_cancel(move |device_id, location, force| { + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Cancelled, + force: Some(force), + location, + }), + }); + }); + + let runner = self.runner.clone(); + canvas.on_dark_mode(move |is_dark_mode| { + let theme = if is_dark_mode { + Theme::Dark + } else { + Theme::Light + }; + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ThemeChanged(theme), + }); + }); + + canvas.on_resize_scale( + { + let runner = self.runner.clone(); + let canvas = canvas_clone.clone(); + + move |size, scale| { + runner.send_event(EventWrapper::ScaleChange { + canvas: Rc::downgrade(&canvas), + size, + scale, + }) + } + }, + { + let runner = self.runner.clone(); + let canvas = canvas_clone.clone(); + + move |new_size| { + let canvas = canvas.borrow(); + canvas.set_current_size(new_size); + if canvas.old_size() != new_size { + canvas.set_old_size(new_size); + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Resized(new_size), + }); + runner.request_redraw(RootWindowId(id)); + } + } + }, + ); + + let runner = self.runner.clone(); + canvas.on_intersection(move |is_intersecting| { + // only fire if visible while skipping the first event if it's intersecting + if backend::is_visible(runner.document()) + && !(is_intersecting && canvas_clone.borrow().is_intersecting.is_none()) + { + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Occluded(!is_intersecting), + }); + } + + canvas_clone.borrow_mut().is_intersecting = Some(is_intersecting); + }); + + let runner = self.runner.clone(); + canvas.on_animation_frame(move || runner.request_redraw(RootWindowId(id))); + + canvas.on_context_menu(); + } + + pub fn available_monitors(&self) -> VecDequeIter { + VecDeque::new().into_iter() + } + + pub fn primary_monitor(&self) -> Option { + None + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Web( + rwh_06::WebDisplayHandle::new(), + )) + } + + pub fn listen_device_events(&self, allowed: DeviceEvents) { + self.runner.listen_device_events(allowed) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.runner.set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.runner.control_flow() + } + + pub(crate) fn exit(&self) { + self.runner.exit() + } + + pub(crate) fn exiting(&self) -> bool { + self.runner.exiting() + } + + pub(crate) fn set_poll_strategy(&self, strategy: PollStrategy) { + self.runner.set_poll_strategy(strategy) + } + + pub(crate) fn poll_strategy(&self) -> PollStrategy { + self.runner.poll_strategy() + } + + pub(crate) fn waker(&self) -> Waker> { + self.runner.waker() + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::WebDisplayHandle::new().into()) + } +} diff --git a/rio-window/src/platform_impl/web/keyboard.rs b/rio-window/src/platform_impl/web/keyboard.rs new file mode 100644 index 00000000..391777bb --- /dev/null +++ b/rio-window/src/platform_impl/web/keyboard.rs @@ -0,0 +1,524 @@ +use smol_str::SmolStr; + +use crate::keyboard::{Key, KeyCode, NamedKey, NativeKey, NativeKeyCode, PhysicalKey}; + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub(crate) struct KeyEventExtra; + +impl Key { + pub(crate) fn from_key_attribute_value(kav: &str) -> Self { + Key::Named(match kav { + "Unidentified" => { + return Key::Unidentified(NativeKey::Web(SmolStr::new(kav))) + } + "Dead" => return Key::Dead(None), + "Alt" => NamedKey::Alt, + "AltGraph" => NamedKey::AltGraph, + "CapsLock" => NamedKey::CapsLock, + "Control" => NamedKey::Control, + "Fn" => NamedKey::Fn, + "FnLock" => NamedKey::FnLock, + "NumLock" => NamedKey::NumLock, + "ScrollLock" => NamedKey::ScrollLock, + "Shift" => NamedKey::Shift, + "Symbol" => NamedKey::Symbol, + "SymbolLock" => NamedKey::SymbolLock, + "Hyper" => NamedKey::Hyper, + "Meta" => NamedKey::Super, + "Enter" => NamedKey::Enter, + "Tab" => NamedKey::Tab, + " " => NamedKey::Space, + "ArrowDown" => NamedKey::ArrowDown, + "ArrowLeft" => NamedKey::ArrowLeft, + "ArrowRight" => NamedKey::ArrowRight, + "ArrowUp" => NamedKey::ArrowUp, + "End" => NamedKey::End, + "Home" => NamedKey::Home, + "PageDown" => NamedKey::PageDown, + "PageUp" => NamedKey::PageUp, + "Backspace" => NamedKey::Backspace, + "Clear" => NamedKey::Clear, + "Copy" => NamedKey::Copy, + "CrSel" => NamedKey::CrSel, + "Cut" => NamedKey::Cut, + "Delete" => NamedKey::Delete, + "EraseEof" => NamedKey::EraseEof, + "ExSel" => NamedKey::ExSel, + "Insert" => NamedKey::Insert, + "Paste" => NamedKey::Paste, + "Redo" => NamedKey::Redo, + "Undo" => NamedKey::Undo, + "Accept" => NamedKey::Accept, + "Again" => NamedKey::Again, + "Attn" => NamedKey::Attn, + "Cancel" => NamedKey::Cancel, + "ContextMenu" => NamedKey::ContextMenu, + "Escape" => NamedKey::Escape, + "Execute" => NamedKey::Execute, + "Find" => NamedKey::Find, + "Help" => NamedKey::Help, + "Pause" => NamedKey::Pause, + "Play" => NamedKey::Play, + "Props" => NamedKey::Props, + "Select" => NamedKey::Select, + "ZoomIn" => NamedKey::ZoomIn, + "ZoomOut" => NamedKey::ZoomOut, + "BrightnessDown" => NamedKey::BrightnessDown, + "BrightnessUp" => NamedKey::BrightnessUp, + "Eject" => NamedKey::Eject, + "LogOff" => NamedKey::LogOff, + "Power" => NamedKey::Power, + "PowerOff" => NamedKey::PowerOff, + "PrintScreen" => NamedKey::PrintScreen, + "Hibernate" => NamedKey::Hibernate, + "Standby" => NamedKey::Standby, + "WakeUp" => NamedKey::WakeUp, + "AllCandidates" => NamedKey::AllCandidates, + "Alphanumeric" => NamedKey::Alphanumeric, + "CodeInput" => NamedKey::CodeInput, + "Compose" => NamedKey::Compose, + "Convert" => NamedKey::Convert, + "FinalMode" => NamedKey::FinalMode, + "GroupFirst" => NamedKey::GroupFirst, + "GroupLast" => NamedKey::GroupLast, + "GroupNext" => NamedKey::GroupNext, + "GroupPrevious" => NamedKey::GroupPrevious, + "ModeChange" => NamedKey::ModeChange, + "NextCandidate" => NamedKey::NextCandidate, + "NonConvert" => NamedKey::NonConvert, + "PreviousCandidate" => NamedKey::PreviousCandidate, + "Process" => NamedKey::Process, + "SingleCandidate" => NamedKey::SingleCandidate, + "HangulMode" => NamedKey::HangulMode, + "HanjaMode" => NamedKey::HanjaMode, + "JunjaMode" => NamedKey::JunjaMode, + "Eisu" => NamedKey::Eisu, + "Hankaku" => NamedKey::Hankaku, + "Hiragana" => NamedKey::Hiragana, + "HiraganaKatakana" => NamedKey::HiraganaKatakana, + "KanaMode" => NamedKey::KanaMode, + "KanjiMode" => NamedKey::KanjiMode, + "Katakana" => NamedKey::Katakana, + "Romaji" => NamedKey::Romaji, + "Zenkaku" => NamedKey::Zenkaku, + "ZenkakuHankaku" => NamedKey::ZenkakuHankaku, + "Soft1" => NamedKey::Soft1, + "Soft2" => NamedKey::Soft2, + "Soft3" => NamedKey::Soft3, + "Soft4" => NamedKey::Soft4, + "ChannelDown" => NamedKey::ChannelDown, + "ChannelUp" => NamedKey::ChannelUp, + "Close" => NamedKey::Close, + "MailForward" => NamedKey::MailForward, + "MailReply" => NamedKey::MailReply, + "MailSend" => NamedKey::MailSend, + "MediaClose" => NamedKey::MediaClose, + "MediaFastForward" => NamedKey::MediaFastForward, + "MediaPause" => NamedKey::MediaPause, + "MediaPlay" => NamedKey::MediaPlay, + "MediaPlayPause" => NamedKey::MediaPlayPause, + "MediaRecord" => NamedKey::MediaRecord, + "MediaRewind" => NamedKey::MediaRewind, + "MediaStop" => NamedKey::MediaStop, + "MediaTrackNext" => NamedKey::MediaTrackNext, + "MediaTrackPrevious" => NamedKey::MediaTrackPrevious, + "New" => NamedKey::New, + "Open" => NamedKey::Open, + "Print" => NamedKey::Print, + "Save" => NamedKey::Save, + "SpellCheck" => NamedKey::SpellCheck, + "Key11" => NamedKey::Key11, + "Key12" => NamedKey::Key12, + "AudioBalanceLeft" => NamedKey::AudioBalanceLeft, + "AudioBalanceRight" => NamedKey::AudioBalanceRight, + "AudioBassBoostDown" => NamedKey::AudioBassBoostDown, + "AudioBassBoostToggle" => NamedKey::AudioBassBoostToggle, + "AudioBassBoostUp" => NamedKey::AudioBassBoostUp, + "AudioFaderFront" => NamedKey::AudioFaderFront, + "AudioFaderRear" => NamedKey::AudioFaderRear, + "AudioSurroundModeNext" => NamedKey::AudioSurroundModeNext, + "AudioTrebleDown" => NamedKey::AudioTrebleDown, + "AudioTrebleUp" => NamedKey::AudioTrebleUp, + "AudioVolumeDown" => NamedKey::AudioVolumeDown, + "AudioVolumeUp" => NamedKey::AudioVolumeUp, + "AudioVolumeMute" => NamedKey::AudioVolumeMute, + "MicrophoneToggle" => NamedKey::MicrophoneToggle, + "MicrophoneVolumeDown" => NamedKey::MicrophoneVolumeDown, + "MicrophoneVolumeUp" => NamedKey::MicrophoneVolumeUp, + "MicrophoneVolumeMute" => NamedKey::MicrophoneVolumeMute, + "SpeechCorrectionList" => NamedKey::SpeechCorrectionList, + "SpeechInputToggle" => NamedKey::SpeechInputToggle, + "LaunchApplication1" => NamedKey::LaunchApplication1, + "LaunchApplication2" => NamedKey::LaunchApplication2, + "LaunchCalendar" => NamedKey::LaunchCalendar, + "LaunchContacts" => NamedKey::LaunchContacts, + "LaunchMail" => NamedKey::LaunchMail, + "LaunchMediaPlayer" => NamedKey::LaunchMediaPlayer, + "LaunchMusicPlayer" => NamedKey::LaunchMusicPlayer, + "LaunchPhone" => NamedKey::LaunchPhone, + "LaunchScreenSaver" => NamedKey::LaunchScreenSaver, + "LaunchSpreadsheet" => NamedKey::LaunchSpreadsheet, + "LaunchWebBrowser" => NamedKey::LaunchWebBrowser, + "LaunchWebCam" => NamedKey::LaunchWebCam, + "LaunchWordProcessor" => NamedKey::LaunchWordProcessor, + "BrowserBack" => NamedKey::BrowserBack, + "BrowserFavorites" => NamedKey::BrowserFavorites, + "BrowserForward" => NamedKey::BrowserForward, + "BrowserHome" => NamedKey::BrowserHome, + "BrowserRefresh" => NamedKey::BrowserRefresh, + "BrowserSearch" => NamedKey::BrowserSearch, + "BrowserStop" => NamedKey::BrowserStop, + "AppSwitch" => NamedKey::AppSwitch, + "Call" => NamedKey::Call, + "Camera" => NamedKey::Camera, + "CameraFocus" => NamedKey::CameraFocus, + "EndCall" => NamedKey::EndCall, + "GoBack" => NamedKey::GoBack, + "GoHome" => NamedKey::GoHome, + "HeadsetHook" => NamedKey::HeadsetHook, + "LastNumberRedial" => NamedKey::LastNumberRedial, + "Notification" => NamedKey::Notification, + "MannerMode" => NamedKey::MannerMode, + "VoiceDial" => NamedKey::VoiceDial, + "TV" => NamedKey::TV, + "TV3DMode" => NamedKey::TV3DMode, + "TVAntennaCable" => NamedKey::TVAntennaCable, + "TVAudioDescription" => NamedKey::TVAudioDescription, + "TVAudioDescriptionMixDown" => NamedKey::TVAudioDescriptionMixDown, + "TVAudioDescriptionMixUp" => NamedKey::TVAudioDescriptionMixUp, + "TVContentsMenu" => NamedKey::TVContentsMenu, + "TVDataService" => NamedKey::TVDataService, + "TVInput" => NamedKey::TVInput, + "TVInputComponent1" => NamedKey::TVInputComponent1, + "TVInputComponent2" => NamedKey::TVInputComponent2, + "TVInputComposite1" => NamedKey::TVInputComposite1, + "TVInputComposite2" => NamedKey::TVInputComposite2, + "TVInputHDMI1" => NamedKey::TVInputHDMI1, + "TVInputHDMI2" => NamedKey::TVInputHDMI2, + "TVInputHDMI3" => NamedKey::TVInputHDMI3, + "TVInputHDMI4" => NamedKey::TVInputHDMI4, + "TVInputVGA1" => NamedKey::TVInputVGA1, + "TVMediaContext" => NamedKey::TVMediaContext, + "TVNetwork" => NamedKey::TVNetwork, + "TVNumberEntry" => NamedKey::TVNumberEntry, + "TVPower" => NamedKey::TVPower, + "TVRadioService" => NamedKey::TVRadioService, + "TVSatellite" => NamedKey::TVSatellite, + "TVSatelliteBS" => NamedKey::TVSatelliteBS, + "TVSatelliteCS" => NamedKey::TVSatelliteCS, + "TVSatelliteToggle" => NamedKey::TVSatelliteToggle, + "TVTerrestrialAnalog" => NamedKey::TVTerrestrialAnalog, + "TVTerrestrialDigital" => NamedKey::TVTerrestrialDigital, + "TVTimer" => NamedKey::TVTimer, + "AVRInput" => NamedKey::AVRInput, + "AVRPower" => NamedKey::AVRPower, + "ColorF0Red" => NamedKey::ColorF0Red, + "ColorF1Green" => NamedKey::ColorF1Green, + "ColorF2Yellow" => NamedKey::ColorF2Yellow, + "ColorF3Blue" => NamedKey::ColorF3Blue, + "ColorF4Grey" => NamedKey::ColorF4Grey, + "ColorF5Brown" => NamedKey::ColorF5Brown, + "ClosedCaptionToggle" => NamedKey::ClosedCaptionToggle, + "Dimmer" => NamedKey::Dimmer, + "DisplaySwap" => NamedKey::DisplaySwap, + "DVR" => NamedKey::DVR, + "Exit" => NamedKey::Exit, + "FavoriteClear0" => NamedKey::FavoriteClear0, + "FavoriteClear1" => NamedKey::FavoriteClear1, + "FavoriteClear2" => NamedKey::FavoriteClear2, + "FavoriteClear3" => NamedKey::FavoriteClear3, + "FavoriteRecall0" => NamedKey::FavoriteRecall0, + "FavoriteRecall1" => NamedKey::FavoriteRecall1, + "FavoriteRecall2" => NamedKey::FavoriteRecall2, + "FavoriteRecall3" => NamedKey::FavoriteRecall3, + "FavoriteStore0" => NamedKey::FavoriteStore0, + "FavoriteStore1" => NamedKey::FavoriteStore1, + "FavoriteStore2" => NamedKey::FavoriteStore2, + "FavoriteStore3" => NamedKey::FavoriteStore3, + "Guide" => NamedKey::Guide, + "GuideNextDay" => NamedKey::GuideNextDay, + "GuidePreviousDay" => NamedKey::GuidePreviousDay, + "Info" => NamedKey::Info, + "InstantReplay" => NamedKey::InstantReplay, + "Link" => NamedKey::Link, + "ListProgram" => NamedKey::ListProgram, + "LiveContent" => NamedKey::LiveContent, + "Lock" => NamedKey::Lock, + "MediaApps" => NamedKey::MediaApps, + "MediaAudioTrack" => NamedKey::MediaAudioTrack, + "MediaLast" => NamedKey::MediaLast, + "MediaSkipBackward" => NamedKey::MediaSkipBackward, + "MediaSkipForward" => NamedKey::MediaSkipForward, + "MediaStepBackward" => NamedKey::MediaStepBackward, + "MediaStepForward" => NamedKey::MediaStepForward, + "MediaTopMenu" => NamedKey::MediaTopMenu, + "NavigateIn" => NamedKey::NavigateIn, + "NavigateNext" => NamedKey::NavigateNext, + "NavigateOut" => NamedKey::NavigateOut, + "NavigatePrevious" => NamedKey::NavigatePrevious, + "NextFavoriteChannel" => NamedKey::NextFavoriteChannel, + "NextUserProfile" => NamedKey::NextUserProfile, + "OnDemand" => NamedKey::OnDemand, + "Pairing" => NamedKey::Pairing, + "PinPDown" => NamedKey::PinPDown, + "PinPMove" => NamedKey::PinPMove, + "PinPToggle" => NamedKey::PinPToggle, + "PinPUp" => NamedKey::PinPUp, + "PlaySpeedDown" => NamedKey::PlaySpeedDown, + "PlaySpeedReset" => NamedKey::PlaySpeedReset, + "PlaySpeedUp" => NamedKey::PlaySpeedUp, + "RandomToggle" => NamedKey::RandomToggle, + "RcLowBattery" => NamedKey::RcLowBattery, + "RecordSpeedNext" => NamedKey::RecordSpeedNext, + "RfBypass" => NamedKey::RfBypass, + "ScanChannelsToggle" => NamedKey::ScanChannelsToggle, + "ScreenModeNext" => NamedKey::ScreenModeNext, + "Settings" => NamedKey::Settings, + "SplitScreenToggle" => NamedKey::SplitScreenToggle, + "STBInput" => NamedKey::STBInput, + "STBPower" => NamedKey::STBPower, + "Subtitle" => NamedKey::Subtitle, + "Teletext" => NamedKey::Teletext, + "VideoModeNext" => NamedKey::VideoModeNext, + "Wink" => NamedKey::Wink, + "ZoomToggle" => NamedKey::ZoomToggle, + "F1" => NamedKey::F1, + "F2" => NamedKey::F2, + "F3" => NamedKey::F3, + "F4" => NamedKey::F4, + "F5" => NamedKey::F5, + "F6" => NamedKey::F6, + "F7" => NamedKey::F7, + "F8" => NamedKey::F8, + "F9" => NamedKey::F9, + "F10" => NamedKey::F10, + "F11" => NamedKey::F11, + "F12" => NamedKey::F12, + "F13" => NamedKey::F13, + "F14" => NamedKey::F14, + "F15" => NamedKey::F15, + "F16" => NamedKey::F16, + "F17" => NamedKey::F17, + "F18" => NamedKey::F18, + "F19" => NamedKey::F19, + "F20" => NamedKey::F20, + "F21" => NamedKey::F21, + "F22" => NamedKey::F22, + "F23" => NamedKey::F23, + "F24" => NamedKey::F24, + "F25" => NamedKey::F25, + "F26" => NamedKey::F26, + "F27" => NamedKey::F27, + "F28" => NamedKey::F28, + "F29" => NamedKey::F29, + "F30" => NamedKey::F30, + "F31" => NamedKey::F31, + "F32" => NamedKey::F32, + "F33" => NamedKey::F33, + "F34" => NamedKey::F34, + "F35" => NamedKey::F35, + string => return Key::Character(SmolStr::new(string)), + }) + } +} + +impl PhysicalKey { + pub fn from_key_code_attribute_value(kcav: &str) -> Self { + PhysicalKey::Code(match kcav { + "Backquote" => KeyCode::Backquote, + "Backslash" => KeyCode::Backslash, + "BracketLeft" => KeyCode::BracketLeft, + "BracketRight" => KeyCode::BracketRight, + "Comma" => KeyCode::Comma, + "Digit0" => KeyCode::Digit0, + "Digit1" => KeyCode::Digit1, + "Digit2" => KeyCode::Digit2, + "Digit3" => KeyCode::Digit3, + "Digit4" => KeyCode::Digit4, + "Digit5" => KeyCode::Digit5, + "Digit6" => KeyCode::Digit6, + "Digit7" => KeyCode::Digit7, + "Digit8" => KeyCode::Digit8, + "Digit9" => KeyCode::Digit9, + "Equal" => KeyCode::Equal, + "IntlBackslash" => KeyCode::IntlBackslash, + "IntlRo" => KeyCode::IntlRo, + "IntlYen" => KeyCode::IntlYen, + "KeyA" => KeyCode::KeyA, + "KeyB" => KeyCode::KeyB, + "KeyC" => KeyCode::KeyC, + "KeyD" => KeyCode::KeyD, + "KeyE" => KeyCode::KeyE, + "KeyF" => KeyCode::KeyF, + "KeyG" => KeyCode::KeyG, + "KeyH" => KeyCode::KeyH, + "KeyI" => KeyCode::KeyI, + "KeyJ" => KeyCode::KeyJ, + "KeyK" => KeyCode::KeyK, + "KeyL" => KeyCode::KeyL, + "KeyM" => KeyCode::KeyM, + "KeyN" => KeyCode::KeyN, + "KeyO" => KeyCode::KeyO, + "KeyP" => KeyCode::KeyP, + "KeyQ" => KeyCode::KeyQ, + "KeyR" => KeyCode::KeyR, + "KeyS" => KeyCode::KeyS, + "KeyT" => KeyCode::KeyT, + "KeyU" => KeyCode::KeyU, + "KeyV" => KeyCode::KeyV, + "KeyW" => KeyCode::KeyW, + "KeyX" => KeyCode::KeyX, + "KeyY" => KeyCode::KeyY, + "KeyZ" => KeyCode::KeyZ, + "Minus" => KeyCode::Minus, + "Period" => KeyCode::Period, + "Quote" => KeyCode::Quote, + "Semicolon" => KeyCode::Semicolon, + "Slash" => KeyCode::Slash, + "AltLeft" => KeyCode::AltLeft, + "AltRight" => KeyCode::AltRight, + "Backspace" => KeyCode::Backspace, + "CapsLock" => KeyCode::CapsLock, + "ContextMenu" => KeyCode::ContextMenu, + "ControlLeft" => KeyCode::ControlLeft, + "ControlRight" => KeyCode::ControlRight, + "Enter" => KeyCode::Enter, + "MetaLeft" => KeyCode::SuperLeft, + "MetaRight" => KeyCode::SuperRight, + "ShiftLeft" => KeyCode::ShiftLeft, + "ShiftRight" => KeyCode::ShiftRight, + "Space" => KeyCode::Space, + "Tab" => KeyCode::Tab, + "Convert" => KeyCode::Convert, + "KanaMode" => KeyCode::KanaMode, + "Lang1" => KeyCode::Lang1, + "Lang2" => KeyCode::Lang2, + "Lang3" => KeyCode::Lang3, + "Lang4" => KeyCode::Lang4, + "Lang5" => KeyCode::Lang5, + "NonConvert" => KeyCode::NonConvert, + "Delete" => KeyCode::Delete, + "End" => KeyCode::End, + "Help" => KeyCode::Help, + "Home" => KeyCode::Home, + "Insert" => KeyCode::Insert, + "PageDown" => KeyCode::PageDown, + "PageUp" => KeyCode::PageUp, + "ArrowDown" => KeyCode::ArrowDown, + "ArrowLeft" => KeyCode::ArrowLeft, + "ArrowRight" => KeyCode::ArrowRight, + "ArrowUp" => KeyCode::ArrowUp, + "NumLock" => KeyCode::NumLock, + "Numpad0" => KeyCode::Numpad0, + "Numpad1" => KeyCode::Numpad1, + "Numpad2" => KeyCode::Numpad2, + "Numpad3" => KeyCode::Numpad3, + "Numpad4" => KeyCode::Numpad4, + "Numpad5" => KeyCode::Numpad5, + "Numpad6" => KeyCode::Numpad6, + "Numpad7" => KeyCode::Numpad7, + "Numpad8" => KeyCode::Numpad8, + "Numpad9" => KeyCode::Numpad9, + "NumpadAdd" => KeyCode::NumpadAdd, + "NumpadBackspace" => KeyCode::NumpadBackspace, + "NumpadClear" => KeyCode::NumpadClear, + "NumpadClearEntry" => KeyCode::NumpadClearEntry, + "NumpadComma" => KeyCode::NumpadComma, + "NumpadDecimal" => KeyCode::NumpadDecimal, + "NumpadDivide" => KeyCode::NumpadDivide, + "NumpadEnter" => KeyCode::NumpadEnter, + "NumpadEqual" => KeyCode::NumpadEqual, + "NumpadHash" => KeyCode::NumpadHash, + "NumpadMemoryAdd" => KeyCode::NumpadMemoryAdd, + "NumpadMemoryClear" => KeyCode::NumpadMemoryClear, + "NumpadMemoryRecall" => KeyCode::NumpadMemoryRecall, + "NumpadMemoryStore" => KeyCode::NumpadMemoryStore, + "NumpadMemorySubtract" => KeyCode::NumpadMemorySubtract, + "NumpadMultiply" => KeyCode::NumpadMultiply, + "NumpadParenLeft" => KeyCode::NumpadParenLeft, + "NumpadParenRight" => KeyCode::NumpadParenRight, + "NumpadStar" => KeyCode::NumpadStar, + "NumpadSubtract" => KeyCode::NumpadSubtract, + "Escape" => KeyCode::Escape, + "Fn" => KeyCode::Fn, + "FnLock" => KeyCode::FnLock, + "PrintScreen" => KeyCode::PrintScreen, + "ScrollLock" => KeyCode::ScrollLock, + "Pause" => KeyCode::Pause, + "BrowserBack" => KeyCode::BrowserBack, + "BrowserFavorites" => KeyCode::BrowserFavorites, + "BrowserForward" => KeyCode::BrowserForward, + "BrowserHome" => KeyCode::BrowserHome, + "BrowserRefresh" => KeyCode::BrowserRefresh, + "BrowserSearch" => KeyCode::BrowserSearch, + "BrowserStop" => KeyCode::BrowserStop, + "Eject" => KeyCode::Eject, + "LaunchApp1" => KeyCode::LaunchApp1, + "LaunchApp2" => KeyCode::LaunchApp2, + "LaunchMail" => KeyCode::LaunchMail, + "MediaPlayPause" => KeyCode::MediaPlayPause, + "MediaSelect" => KeyCode::MediaSelect, + "MediaStop" => KeyCode::MediaStop, + "MediaTrackNext" => KeyCode::MediaTrackNext, + "MediaTrackPrevious" => KeyCode::MediaTrackPrevious, + "Power" => KeyCode::Power, + "Sleep" => KeyCode::Sleep, + "AudioVolumeDown" => KeyCode::AudioVolumeDown, + "AudioVolumeMute" => KeyCode::AudioVolumeMute, + "AudioVolumeUp" => KeyCode::AudioVolumeUp, + "WakeUp" => KeyCode::WakeUp, + "Hyper" => KeyCode::Hyper, + "Turbo" => KeyCode::Turbo, + "Abort" => KeyCode::Abort, + "Resume" => KeyCode::Resume, + "Suspend" => KeyCode::Suspend, + "Again" => KeyCode::Again, + "Copy" => KeyCode::Copy, + "Cut" => KeyCode::Cut, + "Find" => KeyCode::Find, + "Open" => KeyCode::Open, + "Paste" => KeyCode::Paste, + "Props" => KeyCode::Props, + "Select" => KeyCode::Select, + "Undo" => KeyCode::Undo, + "Hiragana" => KeyCode::Hiragana, + "Katakana" => KeyCode::Katakana, + "F1" => KeyCode::F1, + "F2" => KeyCode::F2, + "F3" => KeyCode::F3, + "F4" => KeyCode::F4, + "F5" => KeyCode::F5, + "F6" => KeyCode::F6, + "F7" => KeyCode::F7, + "F8" => KeyCode::F8, + "F9" => KeyCode::F9, + "F10" => KeyCode::F10, + "F11" => KeyCode::F11, + "F12" => KeyCode::F12, + "F13" => KeyCode::F13, + "F14" => KeyCode::F14, + "F15" => KeyCode::F15, + "F16" => KeyCode::F16, + "F17" => KeyCode::F17, + "F18" => KeyCode::F18, + "F19" => KeyCode::F19, + "F20" => KeyCode::F20, + "F21" => KeyCode::F21, + "F22" => KeyCode::F22, + "F23" => KeyCode::F23, + "F24" => KeyCode::F24, + "F25" => KeyCode::F25, + "F26" => KeyCode::F26, + "F27" => KeyCode::F27, + "F28" => KeyCode::F28, + "F29" => KeyCode::F29, + "F30" => KeyCode::F30, + "F31" => KeyCode::F31, + "F32" => KeyCode::F32, + "F33" => KeyCode::F33, + "F34" => KeyCode::F34, + "F35" => KeyCode::F35, + _ => return PhysicalKey::Unidentified(NativeKeyCode::Unidentified), + }) + } +} diff --git a/rio-window/src/platform_impl/web/main_thread.rs b/rio-window/src/platform_impl/web/main_thread.rs new file mode 100644 index 00000000..740e9e27 --- /dev/null +++ b/rio-window/src/platform_impl/web/main_thread.rs @@ -0,0 +1,96 @@ +use std::fmt::{self, Debug, Formatter}; +use std::marker::PhantomData; +use std::mem; +use std::sync::OnceLock; + +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; + +use super::r#async::{self, Sender}; + +thread_local! { + static MAIN_THREAD: bool = { + #[wasm_bindgen] + extern "C" { + #[derive(Clone)] + type Global; + + #[wasm_bindgen(method, getter, js_name = Window)] + fn window(this: &Global) -> JsValue; + } + + let global: Global = js_sys::global().unchecked_into(); + !global.window().is_undefined() + }; +} + +#[derive(Clone, Copy, Debug)] +pub struct MainThreadMarker(PhantomData<*const ()>); + +impl MainThreadMarker { + pub fn new() -> Option { + MAIN_THREAD.with(|is| is.then_some(Self(PhantomData))) + } +} + +pub struct MainThreadSafe(Option); + +impl MainThreadSafe { + pub fn new(_: MainThreadMarker, value: T) -> Self { + DROP_HANDLER.get_or_init(|| { + let (sender, receiver) = r#async::channel(); + wasm_bindgen_futures::spawn_local(async move { + while receiver.next().await.is_ok() {} + }); + + sender + }); + + Self(Some(value)) + } + + pub fn into_inner(mut self, _: MainThreadMarker) -> T { + self.0.take().expect("already taken or dropped") + } + + pub fn get(&self, _: MainThreadMarker) -> &T { + self.0.as_ref().expect("already taken or dropped") + } +} + +impl Debug for MainThreadSafe { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if MainThreadMarker::new().is_some() { + f.debug_tuple("MainThreadSafe").field(&self.0).finish() + } else { + f.debug_struct("MainThreadSafe").finish_non_exhaustive() + } + } +} + +impl Drop for MainThreadSafe { + fn drop(&mut self) { + if let Some(value) = self.0.take() { + if mem::needs_drop::() && MainThreadMarker::new().is_none() { + DROP_HANDLER + .get() + .expect("drop handler not initialized when setting canvas") + .send(DropBox(Box::new(value))) + .expect("sender dropped in main thread") + } + } + } +} + +unsafe impl Send for MainThreadSafe {} +unsafe impl Sync for MainThreadSafe {} + +static DROP_HANDLER: OnceLock> = OnceLock::new(); + +struct DropBox(#[allow(dead_code)] Box); + +unsafe impl Send for DropBox {} +unsafe impl Sync for DropBox {} + +trait Any {} +impl Any for T {} diff --git a/rio-window/src/platform_impl/web/mod.rs b/rio-window/src/platform_impl/web/mod.rs new file mode 100644 index 00000000..7b896b4c --- /dev/null +++ b/rio-window/src/platform_impl/web/mod.rs @@ -0,0 +1,51 @@ +// Brief introduction to the internals of the web backend: +// The web backend used to support both wasm-bindgen and stdweb as methods of binding to the +// environment. Because they are both supporting the same underlying APIs, the actual web bindings +// are cordoned off into backend abstractions, which present the thinnest unifying layer possible. +// +// When adding support for new events or interactions with the browser, first consult trusted +// documentation (such as MDN) to ensure it is well-standardised and supported across many browsers. +// Once you have decided on the relevant web APIs, add support to both backends. +// +// The backend is used by the rest of the module to implement Winit's business logic, which forms +// the rest of the code. 'device', 'error', 'monitor', and 'window' define web-specific structures +// for winit's cross-platform structures. They are all relatively simple translations. +// +// The event_loop module handles listening for and processing events. 'Proxy' implements +// EventLoopProxy and 'WindowTarget' implements ActiveEventLoop. WindowTarget also handles +// registering the event handlers. The 'Execution' struct in the 'runner' module handles taking +// incoming events (from the registered handlers) and ensuring they are passed to the user in a +// compliant way. + +// TODO: FP, remove when is fixed. +#![allow(clippy::empty_docs)] + +mod r#async; +mod cursor; +mod device; +mod error; +mod event_loop; +mod keyboard; +mod main_thread; +mod monitor; +mod window; + +#[path = "web_sys/mod.rs"] +mod backend; + +pub use self::device::DeviceId; +pub use self::error::OsError; +pub(crate) use self::event_loop::{ + ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle, + PlatformSpecificEventLoopAttributes, +}; +pub use self::monitor::{MonitorHandle, VideoModeHandle}; +pub use self::window::{PlatformSpecificWindowAttributes, Window, WindowId}; + +pub(crate) use self::keyboard::KeyEventExtra; +pub(crate) use crate::icon::NoIcon as PlatformIcon; +pub(crate) use crate::platform_impl::Fullscreen; +pub(crate) use cursor::{ + CustomCursor as PlatformCustomCursor, CustomCursorFuture, + CustomCursorSource as PlatformCustomCursorSource, +}; diff --git a/rio-window/src/platform_impl/web/monitor.rs b/rio-window/src/platform_impl/web/monitor.rs new file mode 100644 index 00000000..1870284a --- /dev/null +++ b/rio-window/src/platform_impl/web/monitor.rs @@ -0,0 +1,53 @@ +use std::iter::Empty; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct MonitorHandle; + +impl MonitorHandle { + pub fn scale_factor(&self) -> f64 { + unreachable!() + } + + pub fn position(&self) -> PhysicalPosition { + unreachable!() + } + + pub fn name(&self) -> Option { + unreachable!() + } + + pub fn refresh_rate_millihertz(&self) -> Option { + unreachable!() + } + + pub fn size(&self) -> PhysicalSize { + unreachable!() + } + + pub fn video_modes(&self) -> Empty { + unreachable!() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct VideoModeHandle; + +impl VideoModeHandle { + pub fn size(&self) -> PhysicalSize { + unreachable!(); + } + + pub fn bit_depth(&self) -> u16 { + unreachable!(); + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + unreachable!(); + } + + pub fn monitor(&self) -> MonitorHandle { + unreachable!(); + } +} diff --git a/rio-window/src/platform_impl/web/web_sys/animation_frame.rs b/rio-window/src/platform_impl/web/web_sys/animation_frame.rs new file mode 100644 index 00000000..b761f9a9 --- /dev/null +++ b/rio-window/src/platform_impl/web/web_sys/animation_frame.rs @@ -0,0 +1,70 @@ +use std::cell::Cell; +use std::rc::Rc; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::JsCast; + +pub struct AnimationFrameHandler { + window: web_sys::Window, + closure: Closure, + handle: Rc>>, +} + +impl AnimationFrameHandler { + pub fn new(window: web_sys::Window) -> Self { + let handle = Rc::new(Cell::new(None)); + let closure = Closure::new({ + let handle = handle.clone(); + move || handle.set(None) + }); + + Self { + window, + closure, + handle, + } + } + + pub fn on_animation_frame(&mut self, mut f: F) + where + F: 'static + FnMut(), + { + let handle = self.handle.clone(); + self.closure = Closure::new(move || { + handle.set(None); + f(); + }) + } + + pub fn request(&self) { + if let Some(handle) = self.handle.take() { + self.window + .cancel_animation_frame(handle) + .expect("Failed to cancel animation frame"); + } + + let handle = self + .window + .request_animation_frame(self.closure.as_ref().unchecked_ref()) + .expect("Failed to request animation frame"); + + self.handle.set(Some(handle)); + } + + pub fn cancel(&mut self) { + if let Some(handle) = self.handle.take() { + self.window + .cancel_animation_frame(handle) + .expect("Failed to cancel animation frame"); + } + } +} + +impl Drop for AnimationFrameHandler { + fn drop(&mut self) { + if let Some(handle) = self.handle.take() { + self.window + .cancel_animation_frame(handle) + .expect("Failed to cancel animation frame"); + } + } +} diff --git a/rio-window/src/platform_impl/web/web_sys/canvas.rs b/rio-window/src/platform_impl/web/web_sys/canvas.rs new file mode 100644 index 00000000..89752c55 --- /dev/null +++ b/rio-window/src/platform_impl/web/web_sys/canvas.rs @@ -0,0 +1,634 @@ +use std::cell::Cell; +use std::ops::Deref; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; + +use smol_str::SmolStr; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::JsCast; +use web_sys::{ + CssStyleDeclaration, Document, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, + PointerEvent, WheelEvent, +}; + +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; +use crate::error::OsError as RootOE; +use crate::event::{Force, InnerSizeWriter, MouseButton, MouseScrollDelta}; +use crate::keyboard::{Key, KeyLocation, ModifiersState, PhysicalKey}; +use crate::platform_impl::OsError; +use crate::window::{WindowAttributes, WindowId as RootWindowId}; + +use super::super::cursor::CursorHandler; +use super::super::main_thread::MainThreadMarker; +use super::super::WindowId; +use super::animation_frame::AnimationFrameHandler; +use super::event_handle::EventListenerHandle; +use super::intersection_handle::IntersectionObserverHandle; +use super::media_query_handle::MediaQueryListHandle; +use super::pointer::PointerHandler; +use super::{event, fullscreen, ButtonsState, ResizeScaleHandle}; + +#[allow(dead_code)] +pub struct Canvas { + common: Common, + id: WindowId, + pub has_focus: Rc>, + pub prevent_default: Rc>, + pub is_intersecting: Option, + on_touch_start: Option>, + on_focus: Option>, + on_blur: Option>, + on_keyboard_release: Option>, + on_keyboard_press: Option>, + on_mouse_wheel: Option>, + on_dark_mode: Option, + pointer_handler: PointerHandler, + on_resize_scale: Option, + on_intersect: Option, + animation_frame_handler: AnimationFrameHandler, + on_touch_end: Option>, + on_context_menu: Option>, + pub cursor: CursorHandler, +} + +pub struct Common { + pub window: web_sys::Window, + pub document: Document, + /// Note: resizing the HTMLCanvasElement should go through `backend::set_canvas_size` to ensure + /// the DPI factor is maintained. Note: this is read-only because we use a pointer to this + /// for [`WindowHandle`][rwh_06::WindowHandle]. + raw: Rc, + style: Style, + old_size: Rc>>, + current_size: Rc>>, +} + +#[derive(Clone, Debug)] +pub struct Style { + read: CssStyleDeclaration, + write: CssStyleDeclaration, +} + +impl Canvas { + pub(crate) fn create( + main_thread: MainThreadMarker, + id: WindowId, + window: web_sys::Window, + document: Document, + attr: &mut WindowAttributes, + ) -> Result { + let canvas = match attr.platform_specific.canvas.take().map(|canvas| { + Arc::try_unwrap(canvas) + .map(|canvas| canvas.into_inner(main_thread)) + .unwrap_or_else(|canvas| canvas.get(main_thread).clone()) + }) { + Some(canvas) => canvas, + None => document + .create_element("canvas") + .map_err(|_| { + os_error!(OsError("Failed to create canvas element".to_owned())) + })? + .unchecked_into(), + }; + + if attr.platform_specific.append && !document.contains(Some(&canvas)) { + document + .body() + .expect("Failed to get body from document") + .append_child(&canvas) + .expect("Failed to append canvas to body"); + } + + // A tabindex is needed in order to capture local keyboard events. + // A "0" value means that the element should be focusable in + // sequential keyboard navigation, but its order is defined by the + // document's source order. + // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex + if attr.platform_specific.focusable { + canvas + .set_attribute("tabindex", "0") + .map_err(|_| os_error!(OsError("Failed to set a tabindex".to_owned())))?; + } + + let style = Style::new(&window, &canvas); + + let cursor = CursorHandler::new(main_thread, canvas.clone(), style.clone()); + + let common = Common { + window: window.clone(), + document: document.clone(), + raw: Rc::new(canvas.clone()), + style, + old_size: Rc::default(), + current_size: Rc::default(), + }; + + if let Some(size) = attr.inner_size { + let size = size.to_logical(super::scale_factor(&common.window)); + super::set_canvas_size(&common.document, &common.raw, &common.style, size); + } + + if let Some(size) = attr.min_inner_size { + let size = size.to_logical(super::scale_factor(&common.window)); + super::set_canvas_min_size( + &common.document, + &common.raw, + &common.style, + Some(size), + ); + } + + if let Some(size) = attr.max_inner_size { + let size = size.to_logical(super::scale_factor(&common.window)); + super::set_canvas_max_size( + &common.document, + &common.raw, + &common.style, + Some(size), + ); + } + + if let Some(position) = attr.position { + let position = position.to_logical(super::scale_factor(&common.window)); + super::set_canvas_position( + &common.document, + &common.raw, + &common.style, + position, + ); + } + + if attr.fullscreen.is_some() { + fullscreen::request_fullscreen(&document, &canvas); + } + + if attr.active { + let _ = common.raw.focus(); + } + + Ok(Canvas { + common, + id, + has_focus: Rc::new(Cell::new(false)), + prevent_default: Rc::new(Cell::new(attr.platform_specific.prevent_default)), + is_intersecting: None, + on_touch_start: None, + on_blur: None, + on_focus: None, + on_keyboard_release: None, + on_keyboard_press: None, + on_mouse_wheel: None, + on_dark_mode: None, + pointer_handler: PointerHandler::new(), + on_resize_scale: None, + on_intersect: None, + animation_frame_handler: AnimationFrameHandler::new(window), + on_touch_end: None, + on_context_menu: None, + cursor, + }) + } + + pub fn set_cursor_lock(&self, lock: bool) -> Result<(), RootOE> { + if lock { + self.raw().request_pointer_lock(); + } else { + self.common.document.exit_pointer_lock(); + } + Ok(()) + } + + pub fn set_attribute(&self, attribute: &str, value: &str) { + self.common + .raw + .set_attribute(attribute, value) + .unwrap_or_else(|err| panic!("error: {err:?}\nSet attribute: {attribute}")) + } + + pub fn position(&self) -> LogicalPosition { + let bounds = self.common.raw.get_bounding_client_rect(); + let mut position = LogicalPosition { + x: bounds.x(), + y: bounds.y(), + }; + + if self.document().contains(Some(self.raw())) + && self.style().get("display") != "none" + { + position.x += super::style_size_property(self.style(), "border-left-width") + + super::style_size_property(self.style(), "padding-left"); + position.y += super::style_size_property(self.style(), "border-top-width") + + super::style_size_property(self.style(), "padding-top"); + } + + position + } + + #[inline] + pub fn old_size(&self) -> PhysicalSize { + self.common.old_size.get() + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + self.common.current_size.get() + } + + #[inline] + pub fn set_old_size(&self, size: PhysicalSize) { + self.common.old_size.set(size) + } + + #[inline] + pub fn set_current_size(&self, size: PhysicalSize) { + self.common.current_size.set(size) + } + + #[inline] + pub fn window(&self) -> &web_sys::Window { + &self.common.window + } + + #[inline] + pub fn document(&self) -> &Document { + &self.common.document + } + + #[inline] + pub fn raw(&self) -> &HtmlCanvasElement { + &self.common.raw + } + + #[inline] + pub fn style(&self) -> &Style { + &self.common.style + } + + pub fn on_touch_start(&mut self) { + let prevent_default = Rc::clone(&self.prevent_default); + self.on_touch_start = + Some(self.common.add_event("touchstart", move |event: Event| { + if prevent_default.get() { + event.prevent_default(); + } + })); + } + + pub fn on_blur(&mut self, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_blur = Some(self.common.add_event("blur", move |_: FocusEvent| { + handler(); + })); + } + + pub fn on_focus(&mut self, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_focus = Some(self.common.add_event("focus", move |_: FocusEvent| { + handler(); + })); + } + + pub fn on_keyboard_release(&mut self, mut handler: F) + where + F: 'static + + FnMut(PhysicalKey, Key, Option, KeyLocation, bool, ModifiersState), + { + let prevent_default = Rc::clone(&self.prevent_default); + self.on_keyboard_release = + Some(self.common.add_event("keyup", move |event: KeyboardEvent| { + if prevent_default.get() { + event.prevent_default(); + } + let key = event::key(&event); + let modifiers = event::keyboard_modifiers(&event); + handler( + event::key_code(&event), + key, + event::key_text(&event), + event::key_location(&event), + event.repeat(), + modifiers, + ); + })); + } + + pub fn on_keyboard_press(&mut self, mut handler: F) + where + F: 'static + + FnMut(PhysicalKey, Key, Option, KeyLocation, bool, ModifiersState), + { + let prevent_default = Rc::clone(&self.prevent_default); + self.on_keyboard_press = Some(self.common.add_event( + "keydown", + move |event: KeyboardEvent| { + if prevent_default.get() { + event.prevent_default(); + } + let key = event::key(&event); + let modifiers = event::keyboard_modifiers(&event); + handler( + event::key_code(&event), + key, + event::key_text(&event), + event::key_location(&event), + event.repeat(), + modifiers, + ); + }, + )); + } + + pub fn on_cursor_leave(&mut self, handler: F) + where + F: 'static + FnMut(ModifiersState, Option), + { + self.pointer_handler.on_cursor_leave(&self.common, handler) + } + + pub fn on_cursor_enter(&mut self, handler: F) + where + F: 'static + FnMut(ModifiersState, Option), + { + self.pointer_handler.on_cursor_enter(&self.common, handler) + } + + pub fn on_mouse_release( + &mut self, + modifier_handler: MOD, + mouse_handler: M, + touch_handler: T, + ) where + MOD: 'static + FnMut(ModifiersState), + M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), + T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), + { + self.pointer_handler.on_mouse_release( + &self.common, + modifier_handler, + mouse_handler, + touch_handler, + ) + } + + pub fn on_mouse_press( + &mut self, + modifier_handler: MOD, + mouse_handler: M, + touch_handler: T, + ) where + MOD: 'static + FnMut(ModifiersState), + M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), + T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), + { + self.pointer_handler.on_mouse_press( + &self.common, + modifier_handler, + mouse_handler, + touch_handler, + Rc::clone(&self.prevent_default), + ) + } + + pub fn on_cursor_move( + &mut self, + modifier_handler: MOD, + mouse_handler: M, + touch_handler: T, + button_handler: B, + ) where + MOD: 'static + FnMut(ModifiersState), + M: 'static + + FnMut(ModifiersState, i32, &mut dyn Iterator>), + T: 'static + + FnMut( + ModifiersState, + i32, + &mut dyn Iterator, Force)>, + ), + B: 'static + + FnMut(ModifiersState, i32, PhysicalPosition, ButtonsState, MouseButton), + { + self.pointer_handler.on_cursor_move( + &self.common, + modifier_handler, + mouse_handler, + touch_handler, + button_handler, + Rc::clone(&self.prevent_default), + ) + } + + pub fn on_touch_cancel(&mut self, handler: F) + where + F: 'static + FnMut(i32, PhysicalPosition, Force), + { + self.pointer_handler.on_touch_cancel(&self.common, handler) + } + + pub fn on_mouse_wheel(&mut self, mut handler: F) + where + F: 'static + FnMut(i32, MouseScrollDelta, ModifiersState), + { + let window = self.common.window.clone(); + let prevent_default = Rc::clone(&self.prevent_default); + self.on_mouse_wheel = + Some(self.common.add_event("wheel", move |event: WheelEvent| { + if prevent_default.get() { + event.prevent_default(); + } + + if let Some(delta) = event::mouse_scroll_delta(&window, &event) { + let modifiers = event::mouse_modifiers(&event); + handler(0, delta, modifiers); + } + })); + } + + pub fn on_dark_mode(&mut self, mut handler: F) + where + F: 'static + FnMut(bool), + { + self.on_dark_mode = Some(MediaQueryListHandle::new( + &self.common.window, + "(prefers-color-scheme: dark)", + move |mql| handler(mql.matches()), + )); + } + + pub(crate) fn on_resize_scale(&mut self, scale_handler: S, size_handler: R) + where + S: 'static + FnMut(PhysicalSize, f64), + R: 'static + FnMut(PhysicalSize), + { + self.on_resize_scale = Some(ResizeScaleHandle::new( + self.window().clone(), + self.document().clone(), + self.raw().clone(), + self.style().clone(), + scale_handler, + size_handler, + )); + } + + pub(crate) fn on_intersection(&mut self, handler: F) + where + F: 'static + FnMut(bool), + { + self.on_intersect = Some(IntersectionObserverHandle::new(self.raw(), handler)); + } + + pub(crate) fn on_animation_frame(&mut self, f: F) + where + F: 'static + FnMut(), + { + self.animation_frame_handler.on_animation_frame(f) + } + + pub(crate) fn on_context_menu(&mut self) { + let prevent_default = Rc::clone(&self.prevent_default); + self.on_context_menu = Some(self.common.add_event( + "contextmenu", + move |event: PointerEvent| { + if prevent_default.get() { + event.prevent_default(); + } + }, + )); + } + + pub fn request_fullscreen(&self) { + fullscreen::request_fullscreen(self.document(), self.raw()); + } + + pub fn exit_fullscreen(&self) { + fullscreen::exit_fullscreen(self.document(), self.raw()); + } + + pub fn is_fullscreen(&self) -> bool { + fullscreen::is_fullscreen(self.document(), self.raw()) + } + + pub fn request_animation_frame(&self) { + self.animation_frame_handler.request(); + } + + pub(crate) fn handle_scale_change( + &self, + runner: &super::super::event_loop::runner::Shared, + event_handler: impl FnOnce(crate::event::Event<()>), + current_size: PhysicalSize, + scale: f64, + ) { + // First, we send the `ScaleFactorChanged` event: + self.set_current_size(current_size); + let new_size = { + let new_size = Arc::new(Mutex::new(current_size)); + event_handler(crate::event::Event::WindowEvent { + window_id: RootWindowId(self.id), + event: crate::event::WindowEvent::ScaleFactorChanged { + scale_factor: scale, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_size)), + }, + }); + + let new_size = *new_size.lock().unwrap(); + new_size + }; + + if current_size != new_size { + // Then we resize the canvas to the new size, a new + // `Resized` event will be sent by the `ResizeObserver`: + let new_size = new_size.to_logical(scale); + super::set_canvas_size(self.document(), self.raw(), self.style(), new_size); + + // Set the size might not trigger the event because the calculation is inaccurate. + self.on_resize_scale + .as_ref() + .expect("expected Window to still be active") + .notify_resize(); + } else if self.old_size() != new_size { + // Then we at least send a resized event. + self.set_old_size(new_size); + runner.send_event(crate::event::Event::WindowEvent { + window_id: RootWindowId(self.id), + event: crate::event::WindowEvent::Resized(new_size), + }) + } + } + + pub fn remove_listeners(&mut self) { + self.on_touch_start = None; + self.on_focus = None; + self.on_blur = None; + self.on_keyboard_release = None; + self.on_keyboard_press = None; + self.on_mouse_wheel = None; + self.on_dark_mode = None; + self.pointer_handler.remove_listeners(); + self.on_resize_scale = None; + self.on_intersect = None; + self.animation_frame_handler.cancel(); + self.on_touch_end = None; + self.on_context_menu = None; + } +} + +impl Common { + pub fn add_event( + &self, + event_name: &'static str, + handler: F, + ) -> EventListenerHandle + where + E: 'static + AsRef + wasm_bindgen::convert::FromWasmAbi, + F: 'static + FnMut(E), + { + EventListenerHandle::new( + self.raw.deref().clone(), + event_name, + Closure::new(handler), + ) + } + + pub fn raw(&self) -> &HtmlCanvasElement { + &self.raw + } +} + +impl Style { + fn new(window: &web_sys::Window, canvas: &HtmlCanvasElement) -> Self { + #[allow(clippy::disallowed_methods)] + let read = window + .get_computed_style(canvas) + .expect("Failed to obtain computed style") + // this can't fail: we aren't using a pseudo-element + .expect("Invalid pseudo-element"); + + #[allow(clippy::disallowed_methods)] + let write = canvas.style(); + + Self { read, write } + } + + pub(crate) fn get(&self, property: &str) -> String { + self.read + .get_property_value(property) + .expect("Invalid property") + } + + pub(crate) fn remove(&self, property: &str) { + self.write + .remove_property(property) + .expect("Property is read only"); + } + + pub(crate) fn set(&self, property: &str, value: &str) { + self.write + .set_property(property, value) + .expect("Property is read only"); + } +} diff --git a/rio-window/src/platform_impl/web/web_sys/event.rs b/rio-window/src/platform_impl/web/web_sys/event.rs new file mode 100644 index 00000000..c09f0e3c --- /dev/null +++ b/rio-window/src/platform_impl/web/web_sys/event.rs @@ -0,0 +1,303 @@ +use crate::dpi::LogicalPosition; +use crate::event::{MouseButton, MouseScrollDelta}; +use crate::keyboard::{Key, KeyLocation, ModifiersState, NamedKey, PhysicalKey}; + +use smol_str::SmolStr; +use std::cell::OnceCell; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{KeyboardEvent, MouseEvent, PointerEvent, WheelEvent}; + +bitflags::bitflags! { + // https://www.w3.org/TR/pointerevents3/#the-buttons-property + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct ButtonsState: u16 { + const LEFT = 0b00001; + const RIGHT = 0b00010; + const MIDDLE = 0b00100; + const BACK = 0b01000; + const FORWARD = 0b10000; + } +} + +impl From for MouseButton { + fn from(value: ButtonsState) -> Self { + match value { + ButtonsState::LEFT => MouseButton::Left, + ButtonsState::RIGHT => MouseButton::Right, + ButtonsState::MIDDLE => MouseButton::Middle, + ButtonsState::BACK => MouseButton::Back, + ButtonsState::FORWARD => MouseButton::Forward, + _ => MouseButton::Other(value.bits()), + } + } +} + +impl From for ButtonsState { + fn from(value: MouseButton) -> Self { + match value { + MouseButton::Left => ButtonsState::LEFT, + MouseButton::Right => ButtonsState::RIGHT, + MouseButton::Middle => ButtonsState::MIDDLE, + MouseButton::Back => ButtonsState::BACK, + MouseButton::Forward => ButtonsState::FORWARD, + MouseButton::Other(value) => ButtonsState::from_bits_retain(value), + } + } +} + +pub fn mouse_buttons(event: &MouseEvent) -> ButtonsState { + ButtonsState::from_bits_retain(event.buttons()) +} + +pub fn mouse_button(event: &MouseEvent) -> Option { + // https://www.w3.org/TR/pointerevents3/#the-button-property + match event.button() { + -1 => None, + 0 => Some(MouseButton::Left), + 1 => Some(MouseButton::Middle), + 2 => Some(MouseButton::Right), + 3 => Some(MouseButton::Back), + 4 => Some(MouseButton::Forward), + i => Some(MouseButton::Other( + i.try_into() + .expect("unexpected negative mouse button value"), + )), + } +} + +impl MouseButton { + pub fn to_id(self) -> u32 { + match self { + MouseButton::Left => 0, + MouseButton::Right => 1, + MouseButton::Middle => 2, + MouseButton::Back => 3, + MouseButton::Forward => 4, + MouseButton::Other(value) => value.into(), + } + } +} + +pub fn mouse_position(event: &MouseEvent) -> LogicalPosition { + #[wasm_bindgen] + extern "C" { + type MouseEventExt; + + #[wasm_bindgen(method, getter, js_name = offsetX)] + fn offset_x(this: &MouseEventExt) -> f64; + + #[wasm_bindgen(method, getter, js_name = offsetY)] + fn offset_y(this: &MouseEventExt) -> f64; + } + + let event: &MouseEventExt = event.unchecked_ref(); + + LogicalPosition { + x: event.offset_x(), + y: event.offset_y(), + } +} + +// TODO: Remove this when Firefox supports correct movement values in coalesced events. +// See . +pub struct MouseDelta(Option); + +pub struct MouseDeltaInner { + old_position: LogicalPosition, + old_delta: LogicalPosition, +} + +impl MouseDelta { + pub fn init(window: &web_sys::Window, event: &PointerEvent) -> Self { + // Firefox has wrong movement values in coalesced events, we will detect that by checking + // for `pointerrawupdate` support. Presumably an implementation of `pointerrawupdate` + // should require correct movement values, otherwise uncoalesced events might be broken as + // well. + Self( + (!has_pointer_raw_support(window) && has_coalesced_events_support(event)) + .then(|| MouseDeltaInner { + old_position: mouse_position(event), + old_delta: LogicalPosition { + x: event.movement_x() as f64, + y: event.movement_y() as f64, + }, + }), + ) + } + + pub fn delta(&mut self, event: &MouseEvent) -> LogicalPosition { + if let Some(inner) = &mut self.0 { + let new_position = mouse_position(event); + let x = new_position.x - inner.old_position.x + inner.old_delta.x; + let y = new_position.y - inner.old_position.y + inner.old_delta.y; + inner.old_position = new_position; + inner.old_delta = LogicalPosition::new(0., 0.); + LogicalPosition::new(x, y) + } else { + LogicalPosition { + x: event.movement_x() as f64, + y: event.movement_y() as f64, + } + } + } +} + +pub fn mouse_scroll_delta( + window: &web_sys::Window, + event: &WheelEvent, +) -> Option { + let x = -event.delta_x(); + let y = -event.delta_y(); + + match event.delta_mode() { + WheelEvent::DOM_DELTA_LINE => { + Some(MouseScrollDelta::LineDelta(x as f32, y as f32)) + } + WheelEvent::DOM_DELTA_PIXEL => { + let delta = + LogicalPosition::new(x, y).to_physical(super::scale_factor(window)); + Some(MouseScrollDelta::PixelDelta(delta)) + } + _ => None, + } +} + +pub fn key_code(event: &KeyboardEvent) -> PhysicalKey { + let code = event.code(); + PhysicalKey::from_key_code_attribute_value(&code) +} + +pub fn key(event: &KeyboardEvent) -> Key { + Key::from_key_attribute_value(&event.key()) +} + +pub fn key_text(event: &KeyboardEvent) -> Option { + let key = event.key(); + let key = Key::from_key_attribute_value(&key); + match &key { + Key::Character(text) => Some(text.clone()), + Key::Named(NamedKey::Tab) => Some(SmolStr::new("\t")), + Key::Named(NamedKey::Enter) => Some(SmolStr::new("\r")), + Key::Named(NamedKey::Space) => Some(SmolStr::new(" ")), + _ => None, + } + .map(SmolStr::new) +} + +pub fn key_location(event: &KeyboardEvent) -> KeyLocation { + match event.location() { + KeyboardEvent::DOM_KEY_LOCATION_LEFT => KeyLocation::Left, + KeyboardEvent::DOM_KEY_LOCATION_RIGHT => KeyLocation::Right, + KeyboardEvent::DOM_KEY_LOCATION_NUMPAD => KeyLocation::Numpad, + KeyboardEvent::DOM_KEY_LOCATION_STANDARD => KeyLocation::Standard, + location => { + tracing::warn!("Unexpected key location: {location}"); + KeyLocation::Standard + } + } +} + +pub fn keyboard_modifiers(event: &KeyboardEvent) -> ModifiersState { + let mut state = ModifiersState::empty(); + + if event.shift_key() { + state |= ModifiersState::SHIFT; + } + if event.ctrl_key() { + state |= ModifiersState::CONTROL; + } + if event.alt_key() { + state |= ModifiersState::ALT; + } + if event.meta_key() { + state |= ModifiersState::SUPER; + } + + state +} + +pub fn mouse_modifiers(event: &MouseEvent) -> ModifiersState { + let mut state = ModifiersState::empty(); + + if event.shift_key() { + state |= ModifiersState::SHIFT; + } + if event.ctrl_key() { + state |= ModifiersState::CONTROL; + } + if event.alt_key() { + state |= ModifiersState::ALT; + } + if event.meta_key() { + state |= ModifiersState::SUPER; + } + + state +} + +pub fn pointer_move_event(event: PointerEvent) -> impl Iterator { + // make a single iterator depending on the availability of coalesced events + if has_coalesced_events_support(&event) { + None.into_iter().chain( + Some( + event + .get_coalesced_events() + .into_iter() + .map(PointerEvent::unchecked_from_js), + ) + .into_iter() + .flatten(), + ) + } else { + Some(event).into_iter().chain(None.into_iter().flatten()) + } +} + +// TODO: Remove when all browsers implement it correctly. +// See . +pub fn has_pointer_raw_support(window: &web_sys::Window) -> bool { + thread_local! { + static POINTER_RAW_SUPPORT: OnceCell = const { OnceCell::new() }; + } + + POINTER_RAW_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type PointerRawSupport; + + #[wasm_bindgen(method, getter, js_name = onpointerrawupdate)] + fn has_on_pointerrawupdate(this: &PointerRawSupport) -> JsValue; + } + + let support: &PointerRawSupport = window.unchecked_ref(); + !support.has_on_pointerrawupdate().is_undefined() + }) + }) +} + +// TODO: Remove when Safari supports `getCoalescedEvents`. +// See . +pub fn has_coalesced_events_support(event: &PointerEvent) -> bool { + thread_local! { + static COALESCED_EVENTS_SUPPORT: OnceCell = const { OnceCell::new() }; + } + + COALESCED_EVENTS_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type PointerCoalescedEventsSupport; + + #[wasm_bindgen(method, getter, js_name = getCoalescedEvents)] + fn has_get_coalesced_events( + this: &PointerCoalescedEventsSupport, + ) -> JsValue; + } + + let support: &PointerCoalescedEventsSupport = event.unchecked_ref(); + !support.has_get_coalesced_events().is_undefined() + }) + }) +} diff --git a/rio-window/src/platform_impl/web/web_sys/event_handle.rs b/rio-window/src/platform_impl/web/web_sys/event_handle.rs new file mode 100644 index 00000000..7320cfbc --- /dev/null +++ b/rio-window/src/platform_impl/web/web_sys/event_handle.rs @@ -0,0 +1,45 @@ +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; +use web_sys::EventTarget; + +pub struct EventListenerHandle { + target: EventTarget, + event_type: &'static str, + listener: Closure, +} + +impl EventListenerHandle { + pub fn new(target: U, event_type: &'static str, listener: Closure) -> Self + where + U: Into, + { + let target = target.into(); + target + .add_event_listener_with_callback( + event_type, + listener.as_ref().unchecked_ref(), + ) + .expect("Failed to add event listener"); + EventListenerHandle { + target, + event_type, + listener, + } + } +} + +impl Drop for EventListenerHandle { + fn drop(&mut self) { + self.target + .remove_event_listener_with_callback( + self.event_type, + self.listener.as_ref().unchecked_ref(), + ) + .unwrap_or_else(|e| { + web_sys::console::error_2( + &format!("Error removing event listener {}", self.event_type).into(), + &e, + ) + }); + } +} diff --git a/rio-window/src/platform_impl/web/web_sys/fullscreen.rs b/rio-window/src/platform_impl/web/web_sys/fullscreen.rs new file mode 100644 index 00000000..b365c88b --- /dev/null +++ b/rio-window/src/platform_impl/web/web_sys/fullscreen.rs @@ -0,0 +1,103 @@ +use std::cell::OnceCell; + +use js_sys::Promise; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{Document, Element, HtmlCanvasElement}; + +pub fn request_fullscreen(document: &Document, canvas: &HtmlCanvasElement) { + if is_fullscreen(document, canvas) { + return; + } + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(extends = HtmlCanvasElement)] + type RequestFullscreen; + + #[wasm_bindgen(method, js_name = requestFullscreen)] + fn request_fullscreen(this: &RequestFullscreen) -> Promise; + + #[wasm_bindgen(method, js_name = webkitRequestFullscreen)] + fn webkit_request_fullscreen(this: &RequestFullscreen); + } + + let canvas: &RequestFullscreen = canvas.unchecked_ref(); + + if has_fullscreen_api_support(canvas) { + thread_local! { + static REJECT_HANDLER: Closure = Closure::new(|_| ()); + } + REJECT_HANDLER.with(|handler| { + let _ = canvas.request_fullscreen().catch(handler); + }); + } else { + canvas.webkit_request_fullscreen(); + } +} + +pub fn is_fullscreen(document: &Document, canvas: &HtmlCanvasElement) -> bool { + #[wasm_bindgen] + extern "C" { + type FullscreenElement; + + #[wasm_bindgen(method, getter, js_name = webkitFullscreenElement)] + fn webkit_fullscreen_element(this: &FullscreenElement) -> Option; + } + + let element = if has_fullscreen_api_support(canvas) { + #[allow(clippy::disallowed_methods)] + document.fullscreen_element() + } else { + let document: &FullscreenElement = document.unchecked_ref(); + document.webkit_fullscreen_element() + }; + + match element { + Some(element) => { + let canvas: &Element = canvas; + canvas == &element + } + None => false, + } +} + +pub fn exit_fullscreen(document: &Document, canvas: &HtmlCanvasElement) { + #[wasm_bindgen] + extern "C" { + type ExitFullscreen; + + #[wasm_bindgen(method, js_name = webkitExitFullscreen)] + fn webkit_exit_fullscreen(this: &ExitFullscreen); + } + + if has_fullscreen_api_support(canvas) { + #[allow(clippy::disallowed_methods)] + document.exit_fullscreen() + } else { + let document: &ExitFullscreen = document.unchecked_ref(); + document.webkit_exit_fullscreen() + } +} + +fn has_fullscreen_api_support(canvas: &HtmlCanvasElement) -> bool { + thread_local! { + static FULLSCREEN_API_SUPPORT: OnceCell = const { OnceCell::new() }; + } + + FULLSCREEN_API_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type CanvasFullScreenApiSupport; + + #[wasm_bindgen(method, getter, js_name = requestFullscreen)] + fn has_request_fullscreen(this: &CanvasFullScreenApiSupport) -> JsValue; + } + + let support: &CanvasFullScreenApiSupport = canvas.unchecked_ref(); + !support.has_request_fullscreen().is_undefined() + }) + }) +} diff --git a/rio-window/src/platform_impl/web/web_sys/intersection_handle.rs b/rio-window/src/platform_impl/web/web_sys/intersection_handle.rs new file mode 100644 index 00000000..61fa544d --- /dev/null +++ b/rio-window/src/platform_impl/web/web_sys/intersection_handle.rs @@ -0,0 +1,36 @@ +use js_sys::Array; +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; +use web_sys::{Element, IntersectionObserver, IntersectionObserverEntry}; + +pub(super) struct IntersectionObserverHandle { + observer: IntersectionObserver, + _closure: Closure, +} + +impl IntersectionObserverHandle { + pub fn new(element: &Element, mut callback: F) -> Self + where + F: 'static + FnMut(bool), + { + let closure = Closure::new(move |entries: Array| { + let entry: IntersectionObserverEntry = entries.get(0).unchecked_into(); + callback(entry.is_intersecting()); + }); + let observer = IntersectionObserver::new(closure.as_ref().unchecked_ref()) + // we don't provide any `options` + .expect("Invalid `options`"); + observer.observe(element); + + Self { + observer, + _closure: closure, + } + } +} + +impl Drop for IntersectionObserverHandle { + fn drop(&mut self) { + self.observer.disconnect() + } +} diff --git a/rio-window/src/platform_impl/web/web_sys/media_query_handle.rs b/rio-window/src/platform_impl/web/web_sys/media_query_handle.rs new file mode 100644 index 00000000..4f7aeda3 --- /dev/null +++ b/rio-window/src/platform_impl/web/web_sys/media_query_handle.rs @@ -0,0 +1,49 @@ +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; +use web_sys::MediaQueryList; + +pub(super) struct MediaQueryListHandle { + mql: MediaQueryList, + closure: Closure, +} + +impl MediaQueryListHandle { + pub fn new(window: &web_sys::Window, media_query: &str, mut listener: F) -> Self + where + F: 'static + FnMut(&MediaQueryList), + { + let mql = window + .match_media(media_query) + .expect("Failed to parse media query") + .expect("Found empty media query"); + + let closure = Closure::new({ + let mql = mql.clone(); + move || listener(&mql) + }); + // TODO: Replace obsolete `addListener()` with `addEventListener()` and use + // `MediaQueryListEvent` instead of cloning the `MediaQueryList`. + // Requires Safari v14. + mql.add_listener_with_opt_callback(Some(closure.as_ref().unchecked_ref())) + .expect("Invalid listener"); + + Self { mql, closure } + } + + pub fn mql(&self) -> &MediaQueryList { + &self.mql + } +} + +impl Drop for MediaQueryListHandle { + fn drop(&mut self) { + remove_listener(&self.mql, &self.closure); + } +} + +fn remove_listener(mql: &MediaQueryList, listener: &Closure) { + mql.remove_listener_with_opt_callback(Some(listener.as_ref().unchecked_ref())) + .unwrap_or_else(|e| { + web_sys::console::error_2(&"Error removing media query listener".into(), &e) + }); +} diff --git a/rio-window/src/platform_impl/web/web_sys/mod.rs b/rio-window/src/platform_impl/web/web_sys/mod.rs new file mode 100644 index 00000000..3ce18ede --- /dev/null +++ b/rio-window/src/platform_impl/web/web_sys/mod.rs @@ -0,0 +1,168 @@ +mod animation_frame; +mod canvas; +pub mod event; +mod event_handle; +mod fullscreen; +mod intersection_handle; +mod media_query_handle; +mod pointer; +mod resize_scaling; +mod schedule; + +pub use self::canvas::{Canvas, Style}; +pub use self::event::ButtonsState; +pub use self::event_handle::EventListenerHandle; +pub use self::resize_scaling::ResizeScaleHandle; +pub use self::schedule::Schedule; + +use crate::dpi::{LogicalPosition, LogicalSize}; +use wasm_bindgen::closure::Closure; +use web_sys::{Document, HtmlCanvasElement, PageTransitionEvent, VisibilityState}; + +pub fn throw(msg: &str) { + wasm_bindgen::throw_str(msg); +} + +pub struct PageTransitionEventHandle { + _show_listener: event_handle::EventListenerHandle, + _hide_listener: event_handle::EventListenerHandle, +} + +pub fn on_page_transition( + window: web_sys::Window, + show_handler: impl FnMut(PageTransitionEvent) + 'static, + hide_handler: impl FnMut(PageTransitionEvent) + 'static, +) -> PageTransitionEventHandle { + let show_closure = Closure::new(show_handler); + let hide_closure = Closure::new(hide_handler); + + let show_listener = + event_handle::EventListenerHandle::new(window.clone(), "pageshow", show_closure); + let hide_listener = + event_handle::EventListenerHandle::new(window, "pagehide", hide_closure); + PageTransitionEventHandle { + _show_listener: show_listener, + _hide_listener: hide_listener, + } +} + +pub fn scale_factor(window: &web_sys::Window) -> f64 { + window.device_pixel_ratio() +} + +fn fix_canvas_size(style: &Style, mut size: LogicalSize) -> LogicalSize { + if style.get("box-sizing") == "border-box" { + size.width += style_size_property(style, "border-left-width") + + style_size_property(style, "border-right-width") + + style_size_property(style, "padding-left") + + style_size_property(style, "padding-right"); + size.height += style_size_property(style, "border-top-width") + + style_size_property(style, "border-bottom-width") + + style_size_property(style, "padding-top") + + style_size_property(style, "padding-bottom"); + } + + size +} + +pub fn set_canvas_size( + document: &Document, + raw: &HtmlCanvasElement, + style: &Style, + new_size: LogicalSize, +) { + if !document.contains(Some(raw)) || style.get("display") == "none" { + return; + } + + let new_size = fix_canvas_size(style, new_size); + + style.set("width", &format!("{}px", new_size.width)); + style.set("height", &format!("{}px", new_size.height)); +} + +pub fn set_canvas_min_size( + document: &Document, + raw: &HtmlCanvasElement, + style: &Style, + dimensions: Option>, +) { + if let Some(dimensions) = dimensions { + if !document.contains(Some(raw)) || style.get("display") == "none" { + return; + } + + let new_size = fix_canvas_size(style, dimensions); + + style.set("min-width", &format!("{}px", new_size.width)); + style.set("min-height", &format!("{}px", new_size.height)); + } else { + style.remove("min-width"); + style.remove("min-height"); + } +} + +pub fn set_canvas_max_size( + document: &Document, + raw: &HtmlCanvasElement, + style: &Style, + dimensions: Option>, +) { + if let Some(dimensions) = dimensions { + if !document.contains(Some(raw)) || style.get("display") == "none" { + return; + } + + let new_size = fix_canvas_size(style, dimensions); + + style.set("max-width", &format!("{}px", new_size.width)); + style.set("max-height", &format!("{}px", new_size.height)); + } else { + style.remove("max-width"); + style.remove("max-height"); + } +} + +pub fn set_canvas_position( + document: &Document, + raw: &HtmlCanvasElement, + style: &Style, + mut position: LogicalPosition, +) { + if document.contains(Some(raw)) && style.get("display") != "none" { + position.x -= style_size_property(style, "margin-left") + + style_size_property(style, "border-left-width") + + style_size_property(style, "padding-left"); + position.y -= style_size_property(style, "margin-top") + + style_size_property(style, "border-top-width") + + style_size_property(style, "padding-top"); + } + + style.set("position", "fixed"); + style.set("left", &format!("{}px", position.x)); + style.set("top", &format!("{}px", position.y)); +} + +/// This function will panic if the element is not inserted in the DOM +/// or is not a CSS property that represents a size in pixel. +pub fn style_size_property(style: &Style, property: &str) -> f64 { + let prop = style.get(property); + prop.strip_suffix("px") + .expect("Element was not inserted into the DOM or is not a size in pixel") + .parse() + .expect("CSS property is not a size in pixel") +} + +pub fn is_dark_mode(window: &web_sys::Window) -> Option { + window + .match_media("(prefers-color-scheme: dark)") + .ok() + .flatten() + .map(|media| media.matches()) +} + +pub fn is_visible(document: &Document) -> bool { + document.visibility_state() == VisibilityState::Visible +} + +pub type RawCanvasType = HtmlCanvasElement; diff --git a/rio-window/src/platform_impl/web/web_sys/pointer.rs b/rio-window/src/platform_impl/web/web_sys/pointer.rs new file mode 100644 index 00000000..335f710f --- /dev/null +++ b/rio-window/src/platform_impl/web/web_sys/pointer.rs @@ -0,0 +1,289 @@ +use std::cell::Cell; +use std::rc::Rc; + +use super::canvas::Common; +use super::event; +use super::event_handle::EventListenerHandle; +use crate::dpi::PhysicalPosition; +use crate::event::{Force, MouseButton}; +use crate::keyboard::ModifiersState; + +use event::ButtonsState; +use web_sys::PointerEvent; + +#[allow(dead_code)] +pub(super) struct PointerHandler { + on_cursor_leave: Option>, + on_cursor_enter: Option>, + on_cursor_move: Option>, + on_pointer_press: Option>, + on_pointer_release: Option>, + on_touch_cancel: Option>, +} + +impl PointerHandler { + pub fn new() -> Self { + Self { + on_cursor_leave: None, + on_cursor_enter: None, + on_cursor_move: None, + on_pointer_press: None, + on_pointer_release: None, + on_touch_cancel: None, + } + } + + pub fn on_cursor_leave(&mut self, canvas_common: &Common, mut handler: F) + where + F: 'static + FnMut(ModifiersState, Option), + { + self.on_cursor_leave = Some(canvas_common.add_event( + "pointerout", + move |event: PointerEvent| { + let modifiers = event::mouse_modifiers(&event); + + // touch events are handled separately + // handling them here would produce duplicate mouse events, inconsistent with + // other platforms. + let pointer_id = + (event.pointer_type() == "mouse").then(|| event.pointer_id()); + + handler(modifiers, pointer_id); + }, + )); + } + + pub fn on_cursor_enter(&mut self, canvas_common: &Common, mut handler: F) + where + F: 'static + FnMut(ModifiersState, Option), + { + self.on_cursor_enter = Some(canvas_common.add_event( + "pointerover", + move |event: PointerEvent| { + let modifiers = event::mouse_modifiers(&event); + + // touch events are handled separately + // handling them here would produce duplicate mouse events, inconsistent with + // other platforms. + let pointer_id = + (event.pointer_type() == "mouse").then(|| event.pointer_id()); + + handler(modifiers, pointer_id); + }, + )); + } + + pub fn on_mouse_release( + &mut self, + canvas_common: &Common, + mut modifier_handler: MOD, + mut mouse_handler: M, + mut touch_handler: T, + ) where + MOD: 'static + FnMut(ModifiersState), + M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), + T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), + { + let window = canvas_common.window.clone(); + self.on_pointer_release = Some(canvas_common.add_event( + "pointerup", + move |event: PointerEvent| { + let modifiers = event::mouse_modifiers(&event); + + match event.pointer_type().as_str() { + "touch" => touch_handler( + modifiers, + event.pointer_id(), + event::mouse_position(&event) + .to_physical(super::scale_factor(&window)), + Force::Normalized(event.pressure() as f64), + ), + "mouse" => mouse_handler( + modifiers, + event.pointer_id(), + event::mouse_position(&event) + .to_physical(super::scale_factor(&window)), + event::mouse_button(&event).expect("no mouse button released"), + ), + _ => modifier_handler(modifiers), + } + }, + )); + } + + pub fn on_mouse_press( + &mut self, + canvas_common: &Common, + mut modifier_handler: MOD, + mut mouse_handler: M, + mut touch_handler: T, + prevent_default: Rc>, + ) where + MOD: 'static + FnMut(ModifiersState), + M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), + T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), + { + let window = canvas_common.window.clone(); + let canvas = canvas_common.raw().clone(); + self.on_pointer_press = Some(canvas_common.add_event( + "pointerdown", + move |event: PointerEvent| { + if prevent_default.get() { + // prevent text selection + event.prevent_default(); + // but still focus element + let _ = canvas.focus(); + } + + let modifiers = event::mouse_modifiers(&event); + + match event.pointer_type().as_str() { + "touch" => { + touch_handler( + modifiers, + event.pointer_id(), + event::mouse_position(&event) + .to_physical(super::scale_factor(&window)), + Force::Normalized(event.pressure() as f64), + ); + } + "mouse" => { + mouse_handler( + modifiers, + event.pointer_id(), + event::mouse_position(&event) + .to_physical(super::scale_factor(&window)), + event::mouse_button(&event).expect("no mouse button pressed"), + ); + + // Error is swallowed here since the error would occur every time the mouse + // is clicked when the cursor is grabbed, and there + // is probably not a situation where this could + // fail, that we care if it fails. + let _e = canvas.set_pointer_capture(event.pointer_id()); + } + _ => modifier_handler(modifiers), + } + }, + )); + } + + pub fn on_cursor_move( + &mut self, + canvas_common: &Common, + mut modifier_handler: MOD, + mut mouse_handler: M, + mut touch_handler: T, + mut button_handler: B, + prevent_default: Rc>, + ) where + MOD: 'static + FnMut(ModifiersState), + M: 'static + + FnMut(ModifiersState, i32, &mut dyn Iterator>), + T: 'static + + FnMut( + ModifiersState, + i32, + &mut dyn Iterator, Force)>, + ), + B: 'static + + FnMut(ModifiersState, i32, PhysicalPosition, ButtonsState, MouseButton), + { + let window = canvas_common.window.clone(); + let canvas = canvas_common.raw().clone(); + self.on_cursor_move = Some(canvas_common.add_event( + "pointermove", + move |event: PointerEvent| { + let modifiers = event::mouse_modifiers(&event); + + let pointer_type = event.pointer_type(); + + if let "touch" | "mouse" = pointer_type.as_str() { + } else { + modifier_handler(modifiers); + return; + } + + let id = event.pointer_id(); + + // chorded button event + if let Some(button) = event::mouse_button(&event) { + debug_assert_eq!( + pointer_type, "mouse", + "expect pointer type of a chorded button event to be a mouse" + ); + + if prevent_default.get() { + // prevent text selection + event.prevent_default(); + // but still focus element + let _ = canvas.focus(); + } + + button_handler( + modifiers, + id, + event::mouse_position(&event) + .to_physical(super::scale_factor(&window)), + event::mouse_buttons(&event), + button, + ); + + return; + } + + // pointer move event + let scale = super::scale_factor(&window); + match pointer_type.as_str() { + "mouse" => mouse_handler( + modifiers, + id, + &mut event::pointer_move_event(event).map(|event| { + event::mouse_position(&event).to_physical(scale) + }), + ), + "touch" => touch_handler( + modifiers, + id, + &mut event::pointer_move_event(event).map(|event| { + ( + event::mouse_position(&event).to_physical(scale), + Force::Normalized(event.pressure() as f64), + ) + }), + ), + _ => unreachable!("didn't return early before"), + }; + }, + )); + } + + pub fn on_touch_cancel(&mut self, canvas_common: &Common, mut handler: F) + where + F: 'static + FnMut(i32, PhysicalPosition, Force), + { + let window = canvas_common.window.clone(); + self.on_touch_cancel = Some(canvas_common.add_event( + "pointercancel", + move |event: PointerEvent| { + if event.pointer_type() == "touch" { + handler( + event.pointer_id(), + event::mouse_position(&event) + .to_physical(super::scale_factor(&window)), + Force::Normalized(event.pressure() as f64), + ); + } + }, + )); + } + + pub fn remove_listeners(&mut self) { + self.on_cursor_leave = None; + self.on_cursor_enter = None; + self.on_cursor_move = None; + self.on_pointer_press = None; + self.on_pointer_release = None; + self.on_touch_cancel = None; + } +} diff --git a/rio-window/src/platform_impl/web/web_sys/resize_scaling.rs b/rio-window/src/platform_impl/web/web_sys/resize_scaling.rs new file mode 100644 index 00000000..53921ae1 --- /dev/null +++ b/rio-window/src/platform_impl/web/web_sys/resize_scaling.rs @@ -0,0 +1,314 @@ +use js_sys::{Array, Object}; +use tracing::warn; +use wasm_bindgen::prelude::{wasm_bindgen, Closure}; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{ + Document, HtmlCanvasElement, MediaQueryList, ResizeObserver, + ResizeObserverBoxOptions, ResizeObserverEntry, ResizeObserverOptions, + ResizeObserverSize, Window, +}; + +use crate::dpi::{LogicalSize, PhysicalSize}; + +use super::super::backend; +use super::canvas::Style; +use super::media_query_handle::MediaQueryListHandle; + +use std::cell::{Cell, RefCell}; +use std::rc::Rc; + +pub struct ResizeScaleHandle(Rc>); + +impl ResizeScaleHandle { + pub(crate) fn new( + window: Window, + document: Document, + canvas: HtmlCanvasElement, + style: Style, + scale_handler: S, + resize_handler: R, + ) -> Self + where + S: 'static + FnMut(PhysicalSize, f64), + R: 'static + FnMut(PhysicalSize), + { + Self(ResizeScaleInternal::new( + window, + document, + canvas, + style, + scale_handler, + resize_handler, + )) + } + + pub(crate) fn notify_resize(&self) { + self.0.borrow_mut().notify() + } +} + +/// This is a helper type to help manage the `MediaQueryList` used for detecting +/// changes of the `devicePixelRatio`. +struct ResizeScaleInternal { + window: Window, + document: Document, + canvas: HtmlCanvasElement, + style: Style, + mql: MediaQueryListHandle, + observer: ResizeObserver, + _observer_closure: Closure, + scale_handler: Box, f64)>, + resize_handler: Box)>, + notify_scale: Cell, +} + +impl ResizeScaleInternal { + fn new( + window: Window, + document: Document, + canvas: HtmlCanvasElement, + style: Style, + scale_handler: S, + resize_handler: R, + ) -> Rc> + where + S: 'static + FnMut(PhysicalSize, f64), + R: 'static + FnMut(PhysicalSize), + { + Rc::>::new_cyclic(|weak_self| { + let mql = Self::create_mql(&window, { + let weak_self = weak_self.clone(); + move |mql| { + if let Some(rc_self) = weak_self.upgrade() { + Self::handle_scale(rc_self, mql); + } + } + }); + + let weak_self = weak_self.clone(); + let observer_closure = Closure::new(move |entries: Array, _| { + if let Some(rc_self) = weak_self.upgrade() { + let mut this = rc_self.borrow_mut(); + + let size = this.process_entry(entries); + + if this.notify_scale.replace(false) { + let scale = backend::scale_factor(&this.window); + (this.scale_handler)(size, scale) + } else { + (this.resize_handler)(size) + } + } + }); + let observer = Self::create_observer(&canvas, observer_closure.as_ref()); + + RefCell::new(Self { + window, + document, + canvas, + style, + mql, + observer, + _observer_closure: observer_closure, + scale_handler: Box::new(scale_handler), + resize_handler: Box::new(resize_handler), + notify_scale: Cell::new(false), + }) + }) + } + + fn create_mql(window: &Window, closure: F) -> MediaQueryListHandle + where + F: 'static + FnMut(&MediaQueryList), + { + let current_scale = super::scale_factor(window); + // TODO: Remove `-webkit-device-pixel-ratio`. Requires Safari v16. + let media_query = format!( + "(resolution: {current_scale}dppx), + (-webkit-device-pixel-ratio: {current_scale})", + ); + let mql = MediaQueryListHandle::new(window, &media_query, closure); + debug_assert!( + mql.mql().matches(), + "created media query doesn't match, {current_scale} != {}", + super::scale_factor(window) + ); + mql + } + + fn create_observer(canvas: &HtmlCanvasElement, closure: &JsValue) -> ResizeObserver { + let observer = ResizeObserver::new(closure.as_ref().unchecked_ref()) + .expect("Failed to create `ResizeObserver`"); + + // Safari doesn't support `devicePixelContentBoxSize` + if has_device_pixel_support() { + observer.observe_with_options( + canvas, + ResizeObserverOptions::new() + .box_(ResizeObserverBoxOptions::DevicePixelContentBox), + ); + } else { + observer.observe(canvas); + } + + observer + } + + fn notify(&mut self) { + if !self.document.contains(Some(&self.canvas)) + || self.style.get("display") == "none" + { + let size = PhysicalSize::new(0, 0); + + if self.notify_scale.replace(false) { + let scale = backend::scale_factor(&self.window); + (self.scale_handler)(size, scale) + } else { + (self.resize_handler)(size) + } + + return; + } + + // Safari doesn't support `devicePixelContentBoxSize` + if has_device_pixel_support() { + self.observer.unobserve(&self.canvas); + self.observer.observe(&self.canvas); + + return; + } + + let mut size = LogicalSize::new( + backend::style_size_property(&self.style, "width"), + backend::style_size_property(&self.style, "height"), + ); + + if self.style.get("box-sizing") == "border-box" { + size.width -= backend::style_size_property(&self.style, "border-left-width") + + backend::style_size_property(&self.style, "border-right-width") + + backend::style_size_property(&self.style, "padding-left") + + backend::style_size_property(&self.style, "padding-right"); + size.height -= backend::style_size_property(&self.style, "border-top-width") + + backend::style_size_property(&self.style, "border-bottom-width") + + backend::style_size_property(&self.style, "padding-top") + + backend::style_size_property(&self.style, "padding-bottom"); + } + + let size = size.to_physical(backend::scale_factor(&self.window)); + + if self.notify_scale.replace(false) { + let scale = backend::scale_factor(&self.window); + (self.scale_handler)(size, scale) + } else { + (self.resize_handler)(size) + } + } + + fn handle_scale(this: Rc>, mql: &MediaQueryList) { + let weak_self = Rc::downgrade(&this); + let mut this = this.borrow_mut(); + let scale = super::scale_factor(&this.window); + + // TODO: confirm/reproduce this problem, see: + // . + // This should never happen, but if it does then apparently the scale factor didn't change. + if mql.matches() { + warn!( + "media query tracking scale factor was triggered without a change:\nMedia Query: \ + {}\nCurrent Scale: {scale}", + mql.media(), + ); + return; + } + + let new_mql = Self::create_mql(&this.window, move |mql| { + if let Some(rc_self) = weak_self.upgrade() { + Self::handle_scale(rc_self, mql); + } + }); + this.mql = new_mql; + + this.notify_scale.set(true); + this.notify(); + } + + fn process_entry(&self, entries: Array) -> PhysicalSize { + let entry: ResizeObserverEntry = entries.get(0).unchecked_into(); + + // Safari doesn't support `devicePixelContentBoxSize` + if !has_device_pixel_support() { + let rect = entry.content_rect(); + + return LogicalSize::new(rect.width(), rect.height()) + .to_physical(backend::scale_factor(&self.window)); + } + + let entry: ResizeObserverSize = entry + .device_pixel_content_box_size() + .get(0) + .unchecked_into(); + + let writing_mode = self.style.get("writing-mode"); + + // means the canvas is not inserted into the DOM + if writing_mode.is_empty() { + debug_assert_eq!(entry.inline_size(), 0.); + debug_assert_eq!(entry.block_size(), 0.); + + return PhysicalSize::new(0, 0); + } + + let horizontal = match writing_mode.as_str() { + _ if writing_mode.starts_with("horizontal") => true, + _ if writing_mode.starts_with("vertical") + | writing_mode.starts_with("sideways") => + { + false + } + // deprecated values + "lr" | "lr-tb" | "rl" => true, + "tb" | "tb-lr" | "tb-rl" => false, + _ => { + warn!("unrecognized `writing-mode`, assuming horizontal"); + true + } + }; + + if horizontal { + PhysicalSize::new(entry.inline_size() as u32, entry.block_size() as u32) + } else { + PhysicalSize::new(entry.block_size() as u32, entry.inline_size() as u32) + } + } +} + +impl Drop for ResizeScaleInternal { + fn drop(&mut self) { + self.observer.disconnect(); + } +} + +// TODO: Remove when Safari supports `devicePixelContentBoxSize`. +// See . +pub fn has_device_pixel_support() -> bool { + thread_local! { + static DEVICE_PIXEL_SUPPORT: bool = { + #[wasm_bindgen] + extern "C" { + type ResizeObserverEntryExt; + + #[wasm_bindgen(js_class = ResizeObserverEntry, static_method_of = ResizeObserverEntryExt, getter)] + fn prototype() -> Object; + } + + let prototype = ResizeObserverEntryExt::prototype(); + let descriptor = Object::get_own_property_descriptor( + &prototype, + &JsValue::from_str("devicePixelContentBoxSize"), + ); + !descriptor.is_undefined() + }; + } + + DEVICE_PIXEL_SUPPORT.with(|support| *support) +} diff --git a/rio-window/src/platform_impl/web/web_sys/schedule.rs b/rio-window/src/platform_impl/web/web_sys/schedule.rs new file mode 100644 index 00000000..56253b68 --- /dev/null +++ b/rio-window/src/platform_impl/web/web_sys/schedule.rs @@ -0,0 +1,294 @@ +use js_sys::{Function, Object, Promise, Reflect}; +use std::cell::OnceCell; +use std::time::Duration; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{AbortController, AbortSignal, MessageChannel, MessagePort}; + +use crate::platform::web::PollStrategy; + +#[derive(Debug)] +pub struct Schedule { + _closure: Closure, + inner: Inner, +} + +#[derive(Debug)] +enum Inner { + Scheduler { + controller: AbortController, + }, + IdleCallback { + window: web_sys::Window, + handle: u32, + }, + Timeout { + window: web_sys::Window, + handle: i32, + port: MessagePort, + _timeout_closure: Closure, + }, +} + +impl Schedule { + pub fn new(strategy: PollStrategy, window: &web_sys::Window, f: F) -> Schedule + where + F: 'static + FnMut(), + { + if strategy == PollStrategy::Scheduler && has_scheduler_support(window) { + Self::new_scheduler(window, f, None) + } else if strategy == PollStrategy::IdleCallback + && has_idle_callback_support(window) + { + Self::new_idle_callback(window.clone(), f) + } else { + Self::new_timeout(window.clone(), f, None) + } + } + + pub fn new_with_duration( + window: &web_sys::Window, + f: F, + duration: Duration, + ) -> Schedule + where + F: 'static + FnMut(), + { + if has_scheduler_support(window) { + Self::new_scheduler(window, f, Some(duration)) + } else { + Self::new_timeout(window.clone(), f, Some(duration)) + } + } + + fn new_scheduler( + window: &web_sys::Window, + f: F, + duration: Option, + ) -> Schedule + where + F: 'static + FnMut(), + { + let window: &WindowSupportExt = window.unchecked_ref(); + let scheduler = window.scheduler(); + + let closure = Closure::new(f); + let mut options = SchedulerPostTaskOptions::new(); + let controller = + AbortController::new().expect("Failed to create `AbortController`"); + options.signal(&controller.signal()); + + if let Some(duration) = duration { + // `Duration::as_millis()` always rounds down (because of truncation), we want to round + // up instead. This makes sure that the we never wake up **before** the given time. + let duration = duration + .as_secs() + .checked_mul(1000) + .and_then(|secs| secs.checked_add(duration_millis_ceil(duration).into())) + .unwrap_or(u64::MAX); + + options.delay(duration as f64); + } + + thread_local! { + static REJECT_HANDLER: Closure = Closure::new(|_| ()); + } + REJECT_HANDLER.with(|handler| { + let _ = scheduler + .post_task_with_options(closure.as_ref().unchecked_ref(), &options) + .catch(handler); + }); + + Schedule { + _closure: closure, + inner: Inner::Scheduler { controller }, + } + } + + fn new_idle_callback(window: web_sys::Window, f: F) -> Schedule + where + F: 'static + FnMut(), + { + let closure = Closure::new(f); + let handle = window + .request_idle_callback(closure.as_ref().unchecked_ref()) + .expect("Failed to request idle callback"); + + Schedule { + _closure: closure, + inner: Inner::IdleCallback { window, handle }, + } + } + + fn new_timeout( + window: web_sys::Window, + f: F, + duration: Option, + ) -> Schedule + where + F: 'static + FnMut(), + { + let channel = MessageChannel::new().unwrap(); + let closure = Closure::new(f); + let port_1 = channel.port1(); + port_1.set_onmessage(Some(closure.as_ref().unchecked_ref())); + port_1.start(); + + let port_2 = channel.port2(); + let timeout_closure = Closure::new(move || { + port_2 + .post_message(&JsValue::UNDEFINED) + .expect("Failed to send message") + }); + let handle = if let Some(duration) = duration { + // `Duration::as_millis()` always rounds down (because of truncation), we want to round + // up instead. This makes sure that the we never wake up **before** the given time. + let duration = duration + .as_secs() + .try_into() + .ok() + .and_then(|secs: i32| secs.checked_mul(1000)) + .and_then(|secs: i32| { + let millis: i32 = duration_millis_ceil(duration) + .try_into() + .expect("millis are somehow bigger then 1K"); + secs.checked_add(millis) + }) + .unwrap_or(i32::MAX); + + window.set_timeout_with_callback_and_timeout_and_arguments_0( + timeout_closure.as_ref().unchecked_ref(), + duration, + ) + } else { + window.set_timeout_with_callback(timeout_closure.as_ref().unchecked_ref()) + } + .expect("Failed to set timeout"); + + Schedule { + _closure: closure, + inner: Inner::Timeout { + window, + handle, + port: port_1, + _timeout_closure: timeout_closure, + }, + } + } +} + +impl Drop for Schedule { + fn drop(&mut self) { + match &self.inner { + Inner::Scheduler { controller, .. } => controller.abort(), + Inner::IdleCallback { window, handle, .. } => { + window.cancel_idle_callback(*handle) + } + Inner::Timeout { + window, + handle, + port, + .. + } => { + window.clear_timeout_with_handle(*handle); + port.close(); + port.set_onmessage(None); + } + } + } +} + +// TODO: Replace with `u32::div_ceil()` when we hit Rust v1.73. +fn duration_millis_ceil(duration: Duration) -> u32 { + let micros = duration.subsec_micros(); + + // From . + let d = micros / 1000; + let r = micros % 1000; + if r > 0 && 1000 > 0 { + d + 1 + } else { + d + } +} + +fn has_scheduler_support(window: &web_sys::Window) -> bool { + thread_local! { + static SCHEDULER_SUPPORT: OnceCell = const { OnceCell::new() }; + } + + SCHEDULER_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type SchedulerSupport; + + #[wasm_bindgen(method, getter, js_name = scheduler)] + fn has_scheduler(this: &SchedulerSupport) -> JsValue; + } + + let support: &SchedulerSupport = window.unchecked_ref(); + + !support.has_scheduler().is_undefined() + }) + }) +} + +fn has_idle_callback_support(window: &web_sys::Window) -> bool { + thread_local! { + static IDLE_CALLBACK_SUPPORT: OnceCell = const { OnceCell::new() }; + } + + IDLE_CALLBACK_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type IdleCallbackSupport; + + #[wasm_bindgen(method, getter, js_name = requestIdleCallback)] + fn has_request_idle_callback(this: &IdleCallbackSupport) -> JsValue; + } + + let support: &IdleCallbackSupport = window.unchecked_ref(); + !support.has_request_idle_callback().is_undefined() + }) + }) +} + +#[wasm_bindgen] +extern "C" { + type WindowSupportExt; + + #[wasm_bindgen(method, getter)] + fn scheduler(this: &WindowSupportExt) -> Scheduler; + + type Scheduler; + + #[wasm_bindgen(method, js_name = postTask)] + fn post_task_with_options( + this: &Scheduler, + callback: &Function, + options: &SchedulerPostTaskOptions, + ) -> Promise; + + type SchedulerPostTaskOptions; +} + +impl SchedulerPostTaskOptions { + fn new() -> Self { + Object::new().unchecked_into() + } + + fn delay(&mut self, val: f64) -> &mut Self { + let r = Reflect::set(self, &JsValue::from("delay"), &val.into()); + debug_assert!(r.is_ok(), "Failed to set `delay` property"); + self + } + + fn signal(&mut self, val: &AbortSignal) -> &mut Self { + let r = Reflect::set(self, &JsValue::from("signal"), &val.into()); + debug_assert!(r.is_ok(), "Failed to set `signal` property"); + self + } +} diff --git a/rio-window/src/platform_impl/web/window.rs b/rio-window/src/platform_impl/web/window.rs new file mode 100644 index 00000000..7e3aa13b --- /dev/null +++ b/rio-window/src/platform_impl/web/window.rs @@ -0,0 +1,506 @@ +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOE}; +use crate::icon::Icon; +use crate::window::{ + Cursor, CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowId as RootWI, WindowLevel, +}; + +use super::main_thread::{MainThreadMarker, MainThreadSafe}; +use super::monitor::MonitorHandle; +use super::r#async::Dispatcher; +use super::{backend, ActiveEventLoop, Fullscreen}; +use web_sys::HtmlCanvasElement; + +use std::cell::RefCell; +use std::collections::VecDeque; +use std::rc::Rc; +use std::sync::Arc; + +pub struct Window { + inner: Dispatcher, +} + +pub struct Inner { + id: WindowId, + pub window: web_sys::Window, + canvas: Rc>, + destroy_fn: Option>, +} + +impl Window { + pub(crate) fn new( + target: &ActiveEventLoop, + mut attr: WindowAttributes, + ) -> Result { + let id = target.generate_id(); + + let window = target.runner.window(); + let document = target.runner.document(); + let canvas = backend::Canvas::create( + target.runner.main_thread(), + id, + window.clone(), + document.clone(), + &mut attr, + )?; + let canvas = Rc::new(RefCell::new(canvas)); + + target.register(&canvas, id); + + let runner = target.runner.clone(); + let destroy_fn = Box::new(move || runner.notify_destroy_window(RootWI(id))); + + let inner = Inner { + id, + window: window.clone(), + canvas, + destroy_fn: Some(destroy_fn), + }; + + inner.set_title(&attr.title); + inner.set_maximized(attr.maximized); + inner.set_visible(attr.visible); + inner.set_window_icon(attr.window_icon); + inner.set_cursor(attr.cursor); + + let canvas = Rc::downgrade(&inner.canvas); + let (dispatcher, runner) = + Dispatcher::new(target.runner.main_thread(), inner).unwrap(); + target.runner.add_canvas(RootWI(id), canvas, runner); + + Ok(Window { inner: dispatcher }) + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Inner) + Send + 'static) { + self.inner.dispatch(f) + } + + pub(crate) fn maybe_wait_on_main( + &self, + f: impl FnOnce(&Inner) -> R + Send, + ) -> R { + self.inner.queue(f) + } + + pub fn canvas(&self) -> Option { + self.inner + .value() + .map(|inner| inner.canvas.borrow().raw().clone()) + } + + pub(crate) fn prevent_default(&self) -> bool { + self.inner + .queue(|inner| inner.canvas.borrow().prevent_default.get()) + } + + pub(crate) fn set_prevent_default(&self, prevent_default: bool) { + self.inner.dispatch(move |inner| { + inner.canvas.borrow().prevent_default.set(prevent_default) + }) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06( + &self, + ) -> Result { + self.inner + .value() + .map(|inner| { + let canvas = inner.canvas.borrow(); + // SAFETY: This will only work if the reference to `HtmlCanvasElement` stays valid. + let canvas: &wasm_bindgen::JsValue = canvas.raw(); + let window_handle = rwh_06::WebCanvasWindowHandle::new( + std::ptr::NonNull::from(canvas).cast(), + ); + rwh_06::RawWindowHandle::WebCanvas(window_handle) + }) + .ok_or(rwh_06::HandleError::Unavailable) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Web( + rwh_06::WebDisplayHandle::new(), + )) + } +} + +impl Inner { + pub fn set_title(&self, title: &str) { + self.canvas.borrow().set_attribute("alt", title) + } + + pub fn set_transparent(&self, _transparent: bool) {} + + pub fn set_blur(&self, _blur: bool) {} + + pub fn set_visible(&self, _visible: bool) { + // Intentionally a no-op + } + + #[inline] + pub fn is_visible(&self) -> Option { + None + } + + pub fn request_redraw(&self) { + self.canvas.borrow().request_animation_frame(); + } + + pub fn pre_present_notify(&self) {} + + pub fn outer_position(&self) -> Result, NotSupportedError> { + Ok(self + .canvas + .borrow() + .position() + .to_physical(self.scale_factor())) + } + + pub fn inner_position(&self) -> Result, NotSupportedError> { + // Note: the canvas element has no window decorations, so this is equal to `outer_position`. + self.outer_position() + } + + pub fn set_outer_position(&self, position: Position) { + let canvas = self.canvas.borrow(); + let position = position.to_logical::(self.scale_factor()); + + backend::set_canvas_position( + canvas.document(), + canvas.raw(), + canvas.style(), + position, + ) + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + self.canvas.borrow().inner_size() + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + // Note: the canvas element has no window decorations, so this is equal to `inner_size`. + self.inner_size() + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let size = size.to_logical(self.scale_factor()); + let canvas = self.canvas.borrow(); + backend::set_canvas_size(canvas.document(), canvas.raw(), canvas.style(), size); + None + } + + #[inline] + pub fn set_min_inner_size(&self, dimensions: Option) { + let dimensions = + dimensions.map(|dimensions| dimensions.to_logical(self.scale_factor())); + let canvas = self.canvas.borrow(); + backend::set_canvas_min_size( + canvas.document(), + canvas.raw(), + canvas.style(), + dimensions, + ) + } + + #[inline] + pub fn set_max_inner_size(&self, dimensions: Option) { + let dimensions = + dimensions.map(|dimensions| dimensions.to_logical(self.scale_factor())); + let canvas = self.canvas.borrow(); + backend::set_canvas_max_size( + canvas.document(), + canvas.raw(), + canvas.style(), + dimensions, + ) + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + None + } + + #[inline] + pub fn set_resize_increments(&self, _increments: Option) { + // Intentionally a no-op: users can't resize canvas elements + } + + #[inline] + pub fn set_resizable(&self, _resizable: bool) { + // Intentionally a no-op: users can't resize canvas elements + } + + pub fn is_resizable(&self) -> bool { + true + } + + #[inline] + pub fn set_enabled_buttons(&self, _buttons: WindowButtons) {} + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + WindowButtons::all() + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + super::backend::scale_factor(&self.window) + } + + #[inline] + pub fn set_cursor(&self, cursor: Cursor) { + self.canvas.borrow_mut().cursor.set_cursor(cursor) + } + + #[inline] + pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let lock = match mode { + CursorGrabMode::None => false, + CursorGrabMode::Locked => true, + CursorGrabMode::Confined => { + return Err(ExternalError::NotSupported(NotSupportedError::new())) + } + }; + + self.canvas + .borrow() + .set_cursor_lock(lock) + .map_err(ExternalError::Os) + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + self.canvas.borrow_mut().cursor.set_cursor_visible(visible) + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn drag_resize_window( + &self, + _direction: ResizeDirection, + ) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + #[inline] + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn set_minimized(&self, _minimized: bool) { + // Intentionally a no-op, as canvases cannot be 'minimized' + } + + #[inline] + pub fn is_minimized(&self) -> Option { + // Canvas cannot be 'minimized' + Some(false) + } + + #[inline] + pub fn set_maximized(&self, _maximized: bool) { + // Intentionally a no-op, as canvases cannot be 'maximized' + } + + #[inline] + pub fn is_maximized(&self) -> bool { + // Canvas cannot be 'maximized' + false + } + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + if self.canvas.borrow().is_fullscreen() { + Some(Fullscreen::Borderless(None)) + } else { + None + } + } + + #[inline] + pub(crate) fn set_fullscreen(&self, fullscreen: Option) { + let canvas = &self.canvas.borrow(); + + if fullscreen.is_some() { + canvas.request_fullscreen(); + } else { + canvas.exit_fullscreen() + } + } + + #[inline] + pub fn set_decorations(&self, _decorations: bool) { + // Intentionally a no-op, no canvas decorations + } + + pub fn is_decorated(&self) -> bool { + true + } + + #[inline] + pub fn set_window_level(&self, _level: WindowLevel) { + // Intentionally a no-op, no window ordering + } + + #[inline] + pub fn set_window_icon(&self, _window_icon: Option) { + // Currently an intentional no-op + } + + #[inline] + pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) { + // Currently a no-op as it does not seem there is good support for this on web + } + + #[inline] + pub fn set_ime_allowed(&self, _allowed: bool) { + // Currently not implemented + } + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) { + // Currently not implemented + } + + #[inline] + pub fn focus_window(&self) { + let _ = self.canvas.borrow().raw().focus(); + } + + #[inline] + pub fn request_user_attention(&self, _request_type: Option) { + // Currently an intentional no-op + } + + #[inline] + pub fn current_monitor(&self) -> Option { + None + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + VecDeque::new() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + None + } + + #[inline] + pub fn id(&self) -> WindowId { + self.id + } + + #[inline] + pub fn set_theme(&self, _theme: Option) {} + + #[inline] + pub fn theme(&self) -> Option { + backend::is_dark_mode(&self.window).map(|is_dark_mode| { + if is_dark_mode { + Theme::Dark + } else { + Theme::Light + } + }) + } + + pub fn set_content_protected(&self, _protected: bool) {} + + #[inline] + pub fn has_focus(&self) -> bool { + self.canvas.borrow().has_focus.get() + } + + pub fn title(&self) -> String { + String::new() + } + + pub fn reset_dead_keys(&self) { + // Not supported + } +} + +impl Drop for Inner { + fn drop(&mut self) { + if let Some(destroy_fn) = self.destroy_fn.take() { + destroy_fn(); + } + } +} +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(pub(crate) u32); + +impl WindowId { + pub const unsafe fn dummy() -> Self { + Self(0) + } +} + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 as u64 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id as u32) + } +} + +#[derive(Clone, Debug)] +pub struct PlatformSpecificWindowAttributes { + pub(crate) canvas: Option>>, + pub(crate) prevent_default: bool, + pub(crate) focusable: bool, + pub(crate) append: bool, +} + +impl PlatformSpecificWindowAttributes { + pub(crate) fn set_canvas(&mut self, canvas: Option) { + let Some(canvas) = canvas else { + self.canvas = None; + return; + }; + + let main_thread = MainThreadMarker::new() + .expect("received a `HtmlCanvasElement` outside the window context"); + + self.canvas = Some(Arc::new(MainThreadSafe::new(main_thread, canvas))); + } +} + +impl Default for PlatformSpecificWindowAttributes { + fn default() -> Self { + Self { + canvas: None, + prevent_default: true, + focusable: true, + append: false, + } + } +} diff --git a/rio-window/src/platform_impl/windows/dark_mode.rs b/rio-window/src/platform_impl/windows/dark_mode.rs new file mode 100644 index 00000000..c84a1bce --- /dev/null +++ b/rio-window/src/platform_impl/windows/dark_mode.rs @@ -0,0 +1,176 @@ +/// This is a simple implementation of support for Windows Dark Mode, +/// which is inspired by the solution in https://github.com/ysc3839/win32-darkmode +use std::{ffi::c_void, ptr}; + +use crate::utils::Lazy; +use windows_sys::core::PCSTR; +use windows_sys::Win32::Foundation::{BOOL, HWND, NTSTATUS, S_OK}; +use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA}; +use windows_sys::Win32::System::SystemInformation::OSVERSIONINFOW; +use windows_sys::Win32::UI::Accessibility::{HCF_HIGHCONTRASTON, HIGHCONTRASTA}; +use windows_sys::Win32::UI::Controls::SetWindowTheme; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + SystemParametersInfoA, SPI_GETHIGHCONTRAST, +}; + +use crate::window::Theme; + +use super::util; + +static WIN10_BUILD_VERSION: Lazy> = Lazy::new(|| { + type RtlGetVersion = unsafe extern "system" fn(*mut OSVERSIONINFOW) -> NTSTATUS; + let handle = get_function!("ntdll.dll", RtlGetVersion); + + if let Some(rtl_get_version) = handle { + unsafe { + let mut vi = OSVERSIONINFOW { + dwOSVersionInfoSize: 0, + dwMajorVersion: 0, + dwMinorVersion: 0, + dwBuildNumber: 0, + dwPlatformId: 0, + szCSDVersion: [0; 128], + }; + + let status = (rtl_get_version)(&mut vi); + + if status >= 0 && vi.dwMajorVersion == 10 && vi.dwMinorVersion == 0 { + Some(vi.dwBuildNumber) + } else { + None + } + } + } else { + None + } +}); + +static DARK_MODE_SUPPORTED: Lazy = Lazy::new(|| { + // We won't try to do anything for windows versions < 17763 + // (Windows 10 October 2018 update) + match *WIN10_BUILD_VERSION { + Some(v) => v >= 17763, + None => false, + } +}); + +static DARK_THEME_NAME: Lazy> = + Lazy::new(|| util::encode_wide("DarkMode_Explorer")); +static LIGHT_THEME_NAME: Lazy> = Lazy::new(|| util::encode_wide("")); + +/// Attempt to set a theme on a window, if necessary. +/// Returns the theme that was picked +pub fn try_theme(hwnd: HWND, preferred_theme: Option) -> Theme { + if *DARK_MODE_SUPPORTED { + let is_dark_mode = match preferred_theme { + Some(theme) => theme == Theme::Dark, + None => should_use_dark_mode(), + }; + + let theme = if is_dark_mode { + Theme::Dark + } else { + Theme::Light + }; + let theme_name = match theme { + Theme::Dark => DARK_THEME_NAME.as_ptr(), + Theme::Light => LIGHT_THEME_NAME.as_ptr(), + }; + + let status = unsafe { SetWindowTheme(hwnd, theme_name, ptr::null()) }; + + if status == S_OK && set_dark_mode_for_window(hwnd, is_dark_mode) { + return theme; + } + } + + Theme::Light +} + +fn set_dark_mode_for_window(hwnd: HWND, is_dark_mode: bool) -> bool { + // Uses Windows undocumented API SetWindowCompositionAttribute, + // as seen in win32-darkmode example linked at top of file. + + type SetWindowCompositionAttribute = + unsafe extern "system" fn(HWND, *mut WINDOWCOMPOSITIONATTRIBDATA) -> BOOL; + + #[allow(clippy::upper_case_acronyms)] + type WINDOWCOMPOSITIONATTRIB = u32; + const WCA_USEDARKMODECOLORS: WINDOWCOMPOSITIONATTRIB = 26; + + #[allow(non_snake_case)] + #[allow(clippy::upper_case_acronyms)] + #[repr(C)] + struct WINDOWCOMPOSITIONATTRIBDATA { + Attrib: WINDOWCOMPOSITIONATTRIB, + pvData: *mut c_void, + cbData: usize, + } + + static SET_WINDOW_COMPOSITION_ATTRIBUTE: Lazy> = + Lazy::new(|| get_function!("user32.dll", SetWindowCompositionAttribute)); + + if let Some(set_window_composition_attribute) = *SET_WINDOW_COMPOSITION_ATTRIBUTE { + unsafe { + // SetWindowCompositionAttribute needs a bigbool (i32), not bool. + let mut is_dark_mode_bigbool = BOOL::from(is_dark_mode); + + let mut data = WINDOWCOMPOSITIONATTRIBDATA { + Attrib: WCA_USEDARKMODECOLORS, + pvData: &mut is_dark_mode_bigbool as *mut _ as _, + cbData: std::mem::size_of_val(&is_dark_mode_bigbool) as _, + }; + + let status = set_window_composition_attribute(hwnd, &mut data); + + status != false.into() + } + } else { + false + } +} + +fn should_use_dark_mode() -> bool { + should_apps_use_dark_mode() && !is_high_contrast() +} + +fn should_apps_use_dark_mode() -> bool { + type ShouldAppsUseDarkMode = unsafe extern "system" fn() -> bool; + static SHOULD_APPS_USE_DARK_MODE: Lazy> = + Lazy::new(|| unsafe { + const UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL: PCSTR = 132 as PCSTR; + + let module = LoadLibraryA("uxtheme.dll\0".as_ptr()); + + if module == 0 { + return None; + } + + let handle = GetProcAddress(module, UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL); + + handle.map(|handle| std::mem::transmute(handle)) + }); + + SHOULD_APPS_USE_DARK_MODE + .map(|should_apps_use_dark_mode| unsafe { (should_apps_use_dark_mode)() }) + .unwrap_or(false) +} + +fn is_high_contrast() -> bool { + let mut hc = HIGHCONTRASTA { + cbSize: 0, + dwFlags: 0, + lpszDefaultScheme: ptr::null_mut(), + }; + + let ok = unsafe { + SystemParametersInfoA( + SPI_GETHIGHCONTRAST, + std::mem::size_of_val(&hc) as _, + &mut hc as *mut _ as _, + 0, + ) + }; + + ok != false.into() && util::has_flag(hc.dwFlags, HCF_HIGHCONTRASTON) +} diff --git a/rio-window/src/platform_impl/windows/definitions.rs b/rio-window/src/platform_impl/windows/definitions.rs new file mode 100644 index 00000000..f5e127f4 --- /dev/null +++ b/rio-window/src/platform_impl/windows/definitions.rs @@ -0,0 +1,154 @@ +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] + +use std::ffi::c_void; + +use windows_sys::core::{IUnknown, GUID, HRESULT}; +use windows_sys::Win32::Foundation::{BOOL, HWND, POINTL}; +use windows_sys::Win32::System::Com::{ + IAdviseSink, IDataObject, IEnumFORMATETC, IEnumSTATDATA, FORMATETC, STGMEDIUM, +}; + +#[repr(C)] +pub struct IUnknownVtbl { + pub QueryInterface: unsafe extern "system" fn( + This: *mut IUnknown, + riid: *const GUID, + ppvObject: *mut *mut c_void, + ) -> HRESULT, + pub AddRef: unsafe extern "system" fn(This: *mut IUnknown) -> u32, + pub Release: unsafe extern "system" fn(This: *mut IUnknown) -> u32, +} + +#[repr(C)] +pub struct IDataObjectVtbl { + pub parent: IUnknownVtbl, + pub GetData: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetcIn: *const FORMATETC, + pmedium: *mut STGMEDIUM, + ) -> HRESULT, + pub GetDataHere: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetc: *const FORMATETC, + pmedium: *mut STGMEDIUM, + ) -> HRESULT, + QueryGetData: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetc: *const FORMATETC, + ) -> HRESULT, + pub GetCanonicalFormatEtc: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetcIn: *const FORMATETC, + pformatetcOut: *mut FORMATETC, + ) -> HRESULT, + pub SetData: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetc: *const FORMATETC, + pformatetcOut: *const FORMATETC, + fRelease: BOOL, + ) -> HRESULT, + pub EnumFormatEtc: unsafe extern "system" fn( + This: *mut IDataObject, + dwDirection: u32, + ppenumFormatEtc: *mut *mut IEnumFORMATETC, + ) -> HRESULT, + pub DAdvise: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetc: *const FORMATETC, + advf: u32, + pAdvSInk: *const IAdviseSink, + pdwConnection: *mut u32, + ) -> HRESULT, + pub DUnadvise: + unsafe extern "system" fn(This: *mut IDataObject, dwConnection: u32) -> HRESULT, + pub EnumDAdvise: unsafe extern "system" fn( + This: *mut IDataObject, + ppenumAdvise: *const *const IEnumSTATDATA, + ) -> HRESULT, +} + +#[repr(C)] +pub struct IDropTargetVtbl { + pub parent: IUnknownVtbl, + pub DragEnter: unsafe extern "system" fn( + This: *mut IDropTarget, + pDataObj: *const IDataObject, + grfKeyState: u32, + pt: *const POINTL, + pdwEffect: *mut u32, + ) -> HRESULT, + pub DragOver: unsafe extern "system" fn( + This: *mut IDropTarget, + grfKeyState: u32, + pt: *const POINTL, + pdwEffect: *mut u32, + ) -> HRESULT, + pub DragLeave: unsafe extern "system" fn(This: *mut IDropTarget) -> HRESULT, + pub Drop: unsafe extern "system" fn( + This: *mut IDropTarget, + pDataObj: *const IDataObject, + grfKeyState: u32, + pt: *const POINTL, + pdwEffect: *mut u32, + ) -> HRESULT, +} + +#[repr(C)] +pub struct IDropTarget { + pub lpVtbl: *const IDropTargetVtbl, +} + +#[repr(C)] +pub struct ITaskbarListVtbl { + pub parent: IUnknownVtbl, + pub HrInit: unsafe extern "system" fn(This: *mut ITaskbarList) -> HRESULT, + pub AddTab: unsafe extern "system" fn(This: *mut ITaskbarList, hwnd: HWND) -> HRESULT, + pub DeleteTab: + unsafe extern "system" fn(This: *mut ITaskbarList, hwnd: HWND) -> HRESULT, + pub ActivateTab: + unsafe extern "system" fn(This: *mut ITaskbarList, hwnd: HWND) -> HRESULT, + pub SetActiveAlt: + unsafe extern "system" fn(This: *mut ITaskbarList, hwnd: HWND) -> HRESULT, +} + +#[repr(C)] +pub struct ITaskbarList { + pub lpVtbl: *const ITaskbarListVtbl, +} + +#[repr(C)] +pub struct ITaskbarList2Vtbl { + pub parent: ITaskbarListVtbl, + pub MarkFullscreenWindow: unsafe extern "system" fn( + This: *mut ITaskbarList2, + hwnd: HWND, + fFullscreen: BOOL, + ) -> HRESULT, +} + +#[repr(C)] +pub struct ITaskbarList2 { + pub lpVtbl: *const ITaskbarList2Vtbl, +} + +pub const CLSID_TaskbarList: GUID = GUID { + data1: 0x56fdf344, + data2: 0xfd6d, + data3: 0x11d0, + data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90], +}; + +pub const IID_ITaskbarList: GUID = GUID { + data1: 0x56fdf342, + data2: 0xfd6d, + data3: 0x11d0, + data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90], +}; + +pub const IID_ITaskbarList2: GUID = GUID { + data1: 0x602d4995, + data2: 0xb13a, + data3: 0x429b, + data4: [0xa6, 0x6e, 0x19, 0x35, 0xe4, 0x4f, 0x43, 0x17], +}; diff --git a/rio-window/src/platform_impl/windows/dpi.rs b/rio-window/src/platform_impl/windows/dpi.rs new file mode 100644 index 00000000..b6e87da7 --- /dev/null +++ b/rio-window/src/platform_impl/windows/dpi.rs @@ -0,0 +1,122 @@ +#![allow(non_snake_case, unused_unsafe)] + +use std::sync::Once; + +use windows_sys::Win32::Foundation::{HWND, S_OK}; +use windows_sys::Win32::Graphics::Gdi::{ + GetDC, GetDeviceCaps, MonitorFromWindow, HMONITOR, LOGPIXELSX, + MONITOR_DEFAULTTONEAREST, +}; +use windows_sys::Win32::UI::HiDpi::{ + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, + MDT_EFFECTIVE_DPI, PROCESS_PER_MONITOR_DPI_AWARE, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::IsProcessDPIAware; + +use crate::platform_impl::platform::util::{ + ENABLE_NON_CLIENT_DPI_SCALING, GET_DPI_FOR_MONITOR, GET_DPI_FOR_WINDOW, + SET_PROCESS_DPI_AWARE, SET_PROCESS_DPI_AWARENESS, SET_PROCESS_DPI_AWARENESS_CONTEXT, +}; + +pub fn become_dpi_aware() { + static ENABLE_DPI_AWARENESS: Once = Once::new(); + ENABLE_DPI_AWARENESS.call_once(|| { + unsafe { + if let Some(SetProcessDpiAwarenessContext) = + *SET_PROCESS_DPI_AWARENESS_CONTEXT + { + // We are on Windows 10 Anniversary Update (1607) or later. + if SetProcessDpiAwarenessContext( + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, + ) == false.into() + { + // V2 only works with Windows 10 Creators Update (1703). Try using the older + // V1 if we can't set V2. + SetProcessDpiAwarenessContext( + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, + ); + } + } else if let Some(SetProcessDpiAwareness) = *SET_PROCESS_DPI_AWARENESS { + // We are on Windows 8.1 or later. + SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); + } else if let Some(SetProcessDPIAware) = *SET_PROCESS_DPI_AWARE { + // We are on Vista or later. + SetProcessDPIAware(); + } + } + }); +} + +pub fn enable_non_client_dpi_scaling(hwnd: HWND) { + unsafe { + if let Some(EnableNonClientDpiScaling) = *ENABLE_NON_CLIENT_DPI_SCALING { + EnableNonClientDpiScaling(hwnd); + } + } +} + +pub fn get_monitor_dpi(hmonitor: HMONITOR) -> Option { + unsafe { + if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR { + // We are on Windows 8.1 or later. + let mut dpi_x = 0; + let mut dpi_y = 0; + if GetDpiForMonitor(hmonitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) + == S_OK + { + // MSDN says that "the values of *dpiX and *dpiY are identical. You only need to + // record one of the values to determine the DPI and respond appropriately". + // https://msdn.microsoft.com/en-us/library/windows/desktop/dn280510(v=vs.85).aspx + return Some(dpi_x); + } + } + } + None +} + +pub const BASE_DPI: u32 = 96; +pub fn dpi_to_scale_factor(dpi: u32) -> f64 { + dpi as f64 / BASE_DPI as f64 +} + +pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 { + let hdc = unsafe { GetDC(hwnd) }; + if hdc == 0 { + panic!("[winit] `GetDC` returned null!"); + } + if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW { + // We are on Windows 10 Anniversary Update (1607) or later. + match unsafe { GetDpiForWindow(hwnd) } { + 0 => BASE_DPI, // 0 is returned if hwnd is invalid + dpi => dpi, + } + } else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR { + // We are on Windows 8.1 or later. + let monitor = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }; + if monitor == 0 { + return BASE_DPI; + } + + let mut dpi_x = 0; + let mut dpi_y = 0; + if unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) } + == S_OK + { + dpi_x + } else { + BASE_DPI + } + } else { + // We are on Vista or later. + if unsafe { IsProcessDPIAware() } != false.into() { + // If the process is DPI aware, then scaling must be handled by the application using + // this DPI value. + unsafe { GetDeviceCaps(hdc, LOGPIXELSX as i32) as u32 } + } else { + // If the process is DPI unaware, then scaling is performed by the OS; we thus return + // 96 (scale factor 1.0) to prevent the window from being re-scaled by both the + // application and the WM. + BASE_DPI + } + } +} diff --git a/rio-window/src/platform_impl/windows/drop_handler.rs b/rio-window/src/platform_impl/windows/drop_handler.rs new file mode 100644 index 00000000..cfc3e28b --- /dev/null +++ b/rio-window/src/platform_impl/windows/drop_handler.rs @@ -0,0 +1,253 @@ +use std::ffi::{c_void, OsString}; +use std::os::windows::ffi::OsStringExt; +use std::path::PathBuf; +use std::ptr; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use windows_sys::core::{IUnknown, GUID, HRESULT}; +use windows_sys::Win32::Foundation::{DV_E_FORMATETC, HWND, POINTL, S_OK}; +use windows_sys::Win32::System::Com::{ + IDataObject, DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL, +}; +use windows_sys::Win32::System::Ole::{CF_HDROP, DROPEFFECT_COPY, DROPEFFECT_NONE}; +use windows_sys::Win32::UI::Shell::{DragFinish, DragQueryFileW, HDROP}; + +use tracing::debug; + +use crate::platform_impl::platform::definitions::{ + IDataObjectVtbl, IDropTarget, IDropTargetVtbl, IUnknownVtbl, +}; +use crate::platform_impl::platform::WindowId; + +use crate::event::Event; +use crate::window::WindowId as RootWindowId; + +#[repr(C)] +pub struct FileDropHandlerData { + pub interface: IDropTarget, + refcount: AtomicUsize, + window: HWND, + send_event: Box)>, + cursor_effect: u32, + hovered_is_valid: bool, /* If the currently hovered item is not valid there must not be any + * `HoveredFileCancelled` emitted */ +} + +pub struct FileDropHandler { + pub data: *mut FileDropHandlerData, +} + +#[allow(non_snake_case)] +impl FileDropHandler { + pub fn new(window: HWND, send_event: Box)>) -> FileDropHandler { + let data = Box::new(FileDropHandlerData { + interface: IDropTarget { + lpVtbl: &DROP_TARGET_VTBL as *const IDropTargetVtbl, + }, + refcount: AtomicUsize::new(1), + window, + send_event, + cursor_effect: DROPEFFECT_NONE, + hovered_is_valid: false, + }); + FileDropHandler { + data: Box::into_raw(data), + } + } + + // Implement IUnknown + pub unsafe extern "system" fn QueryInterface( + _this: *mut IUnknown, + _riid: *const GUID, + _ppvObject: *mut *mut c_void, + ) -> HRESULT { + // This function doesn't appear to be required for an `IDropTarget`. + // An implementation would be nice however. + unimplemented!(); + } + + pub unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { + let drop_handler_data = unsafe { Self::from_interface(this) }; + let count = drop_handler_data.refcount.fetch_add(1, Ordering::Release) + 1; + count as u32 + } + + pub unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { + let drop_handler = unsafe { Self::from_interface(this) }; + let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1; + if count == 0 { + // Destroy the underlying data + drop(unsafe { Box::from_raw(drop_handler as *mut FileDropHandlerData) }); + } + count as u32 + } + + pub unsafe extern "system" fn DragEnter( + this: *mut IDropTarget, + pDataObj: *const IDataObject, + _grfKeyState: u32, + _pt: *const POINTL, + pdwEffect: *mut u32, + ) -> HRESULT { + use crate::event::WindowEvent::HoveredFile; + let drop_handler = unsafe { Self::from_interface(this) }; + let hdrop = unsafe { + Self::iterate_filenames(pDataObj, |filename| { + drop_handler.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(drop_handler.window)), + event: HoveredFile(filename), + }); + }) + }; + drop_handler.hovered_is_valid = hdrop.is_some(); + drop_handler.cursor_effect = if drop_handler.hovered_is_valid { + DROPEFFECT_COPY + } else { + DROPEFFECT_NONE + }; + unsafe { + *pdwEffect = drop_handler.cursor_effect; + } + + S_OK + } + + pub unsafe extern "system" fn DragOver( + this: *mut IDropTarget, + _grfKeyState: u32, + _pt: *const POINTL, + pdwEffect: *mut u32, + ) -> HRESULT { + let drop_handler = unsafe { Self::from_interface(this) }; + unsafe { + *pdwEffect = drop_handler.cursor_effect; + } + + S_OK + } + + pub unsafe extern "system" fn DragLeave(this: *mut IDropTarget) -> HRESULT { + use crate::event::WindowEvent::HoveredFileCancelled; + let drop_handler = unsafe { Self::from_interface(this) }; + if drop_handler.hovered_is_valid { + drop_handler.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(drop_handler.window)), + event: HoveredFileCancelled, + }); + } + + S_OK + } + + pub unsafe extern "system" fn Drop( + this: *mut IDropTarget, + pDataObj: *const IDataObject, + _grfKeyState: u32, + _pt: *const POINTL, + _pdwEffect: *mut u32, + ) -> HRESULT { + use crate::event::WindowEvent::DroppedFile; + let drop_handler = unsafe { Self::from_interface(this) }; + let hdrop = unsafe { + Self::iterate_filenames(pDataObj, |filename| { + drop_handler.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(drop_handler.window)), + event: DroppedFile(filename), + }); + }) + }; + if let Some(hdrop) = hdrop { + unsafe { DragFinish(hdrop) }; + } + + S_OK + } + + unsafe fn from_interface<'a, InterfaceT>( + this: *mut InterfaceT, + ) -> &'a mut FileDropHandlerData { + unsafe { &mut *(this as *mut _) } + } + + unsafe fn iterate_filenames( + data_obj: *const IDataObject, + callback: F, + ) -> Option + where + F: Fn(PathBuf), + { + let drop_format = FORMATETC { + cfFormat: CF_HDROP, + ptd: ptr::null_mut(), + dwAspect: DVASPECT_CONTENT, + lindex: -1, + tymed: TYMED_HGLOBAL as u32, + }; + + let mut medium = unsafe { std::mem::zeroed() }; + let get_data_fn = unsafe { (*(*data_obj).cast::()).GetData }; + let get_data_result = + unsafe { get_data_fn(data_obj as *mut _, &drop_format, &mut medium) }; + if get_data_result >= 0 { + let hdrop = unsafe { medium.u.hGlobal as HDROP }; + + // The second parameter (0xFFFFFFFF) instructs the function to return the item count + let item_count = + unsafe { DragQueryFileW(hdrop, 0xffffffff, ptr::null_mut(), 0) }; + + for i in 0..item_count { + // Get the length of the path string NOT including the terminating null character. + // Previously, this was using a fixed size array of MAX_PATH length, but the + // Windows API allows longer paths under certain circumstances. + let character_count = + unsafe { DragQueryFileW(hdrop, i, ptr::null_mut(), 0) as usize }; + let str_len = character_count + 1; + + // Fill path_buf with the null-terminated file name + let mut path_buf = Vec::with_capacity(str_len); + unsafe { + DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), str_len as u32); + path_buf.set_len(str_len); + } + + callback(OsString::from_wide(&path_buf[0..character_count]).into()); + } + + Some(hdrop) + } else if get_data_result == DV_E_FORMATETC { + // If the dropped item is not a file this error will occur. + // In this case it is OK to return without taking further action. + debug!("Error occurred while processing dropped/hovered item: item is not a file."); + None + } else { + debug!("Unexpected error occurred while processing dropped/hovered item."); + None + } + } +} + +impl FileDropHandlerData { + fn send_event(&self, event: Event<()>) { + (self.send_event)(event); + } +} + +impl Drop for FileDropHandler { + fn drop(&mut self) { + unsafe { + FileDropHandler::Release(self.data as *mut IUnknown); + } + } +} + +static DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl { + parent: IUnknownVtbl { + QueryInterface: FileDropHandler::QueryInterface, + AddRef: FileDropHandler::AddRef, + Release: FileDropHandler::Release, + }, + DragEnter: FileDropHandler::DragEnter, + DragOver: FileDropHandler::DragOver, + DragLeave: FileDropHandler::DragLeave, + Drop: FileDropHandler::Drop, +}; diff --git a/rio-window/src/platform_impl/windows/event.rs b/rio-window/src/platform_impl/windows/event.rs new file mode 100644 index 00000000..e69de29b diff --git a/rio-window/src/platform_impl/windows/event_loop.rs b/rio-window/src/platform_impl/windows/event_loop.rs new file mode 100644 index 00000000..42797854 --- /dev/null +++ b/rio-window/src/platform_impl/windows/event_loop.rs @@ -0,0 +1,2732 @@ +#![allow(non_snake_case)] + +mod runner; + +use std::cell::Cell; +use std::collections::VecDeque; +use std::ffi::c_void; +use std::marker::PhantomData; +use std::rc::Rc; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::mpsc::{self, Receiver, Sender}; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::time::{Duration, Instant}; +use std::{mem, panic, ptr}; + +use crate::utils::Lazy; + +use windows_sys::Win32::Devices::HumanInterfaceDevice::MOUSE_MOVE_RELATIVE; +use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, POINT, RECT, WPARAM}; +use windows_sys::Win32::Graphics::Gdi::{ + GetMonitorInfoW, MonitorFromRect, MonitorFromWindow, RedrawWindow, ScreenToClient, + ValidateRect, MONITORINFO, MONITOR_DEFAULTTONULL, RDW_INTERNALPAINT, SC_SCREENSAVE, +}; +use windows_sys::Win32::System::Ole::RevokeDragDrop; +use windows_sys::Win32::System::Threading::{GetCurrentThreadId, INFINITE}; +use windows_sys::Win32::UI::Controls::{HOVER_DEFAULT, WM_MOUSELEAVE}; +use windows_sys::Win32::UI::Input::Ime::{ + GCS_COMPSTR, GCS_RESULTSTR, ISC_SHOWUICOMPOSITIONWINDOW, +}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + ReleaseCapture, SetCapture, TrackMouseEvent, TME_LEAVE, TRACKMOUSEEVENT, +}; +use windows_sys::Win32::UI::Input::Pointer::{ + POINTER_FLAG_DOWN, POINTER_FLAG_UP, POINTER_FLAG_UPDATE, +}; +use windows_sys::Win32::UI::Input::Touch::{ + CloseTouchInputHandle, GetTouchInputInfo, TOUCHEVENTF_DOWN, TOUCHEVENTF_MOVE, + TOUCHEVENTF_UP, TOUCHINPUT, +}; +use windows_sys::Win32::UI::Input::{RAWINPUT, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetClientRect, + GetCursorPos, GetMenu, GetMessageW, KillTimer, LoadCursorW, PeekMessageW, + PostMessageW, RegisterClassExW, RegisterWindowMessageA, SetCursor, SetTimer, + SetWindowPos, TranslateMessage, CREATESTRUCTW, GIDC_ARRIVAL, GIDC_REMOVAL, GWL_STYLE, + GWL_USERDATA, HTCAPTION, HTCLIENT, MINMAXINFO, MNC_CLOSE, MSG, NCCALCSIZE_PARAMS, + PM_REMOVE, PT_PEN, PT_TOUCH, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, SC_MINIMIZE, + SC_RESTORE, SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, + WHEEL_DELTA, WINDOWPOS, WMSZ_BOTTOM, WMSZ_BOTTOMLEFT, WMSZ_BOTTOMRIGHT, WMSZ_LEFT, + WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, WMSZ_TOPRIGHT, WM_CAPTURECHANGED, WM_CLOSE, + WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, + WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT, + WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, WM_KEYUP, + WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, + WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, + WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, + WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, + WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SIZING, WM_SYSCOMMAND, WM_SYSKEYDOWN, + WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, + WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, + WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE, +}; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::error::EventLoopError; +use crate::event::{ + DeviceEvent, Event, Force, Ime, InnerSizeWriter, RawKeyEvent, Touch, TouchPhase, + WindowEvent, +}; +use crate::event_loop::{ + ActiveEventLoop as RootAEL, ControlFlow, DeviceEvents, EventLoopClosed, +}; +use crate::keyboard::ModifiersState; +use crate::platform::pump_events::PumpStatus; +use crate::platform_impl::platform::dark_mode::try_theme; +use crate::platform_impl::platform::dpi::{become_dpi_aware, dpi_to_scale_factor}; +use crate::platform_impl::platform::drop_handler::FileDropHandler; +use crate::platform_impl::platform::icon::WinCursor; +use crate::platform_impl::platform::ime::ImeContext; +use crate::platform_impl::platform::keyboard::KeyEventBuilder; +use crate::platform_impl::platform::keyboard_layout::LAYOUT_CACHE; +use crate::platform_impl::platform::monitor::{self, MonitorHandle}; +use crate::platform_impl::platform::window::InitData; +use crate::platform_impl::platform::window_state::{ + CursorFlags, ImeState, WindowFlags, WindowState, +}; +use crate::platform_impl::platform::{ + raw_input, util, wrap_device_id, Fullscreen, WindowId, DEVICE_ID, +}; +use crate::window::{ + CustomCursor as RootCustomCursor, CustomCursorSource, WindowId as RootWindowId, +}; +use runner::{EventLoopRunner, EventLoopRunnerShared}; + +use super::window::set_skip_taskbar; +use super::SelectedCursor; + +/// some backends like macos uses an uninhabited `Never` type, +/// on windows, `UserEvent`s are also dispatched through the +/// WNDPROC callback, and due to the re-entrant nature of the +/// callback, recursively delivered events must be queued in a +/// buffer, the current implementation put this queue in +/// `EventLoopRunner`, which is shared between the event pumping +/// loop and the callback. because it's hard to decide from the +/// outside whether a event needs to be buffered, I decided not +/// use `Event` for the shared runner state, but use unit +/// as a placeholder so user events can be buffered as usual, +/// the real `UserEvent` is pulled from the mpsc channel directly +/// when the placeholder event is delivered to the event handler +pub(crate) struct UserEventPlaceholder; + +// here below, the generic `EventLoopRunnerShared` is replaced with +// `EventLoopRunnerShared` so we can get rid +// of the generic parameter T in types which don't depend on T. +// this is the approach which requires minimum changes to current +// backend implementation. it should be considered transitional +// and should be refactored and cleaned up eventually, I hope. + +pub(crate) struct WindowData { + pub window_state: Arc>, + pub event_loop_runner: EventLoopRunnerShared, + pub key_event_builder: KeyEventBuilder, + pub _file_drop_handler: Option, + pub userdata_removed: Cell, + pub recurse_depth: Cell, +} + +impl WindowData { + fn send_event(&self, event: Event) { + self.event_loop_runner.send_event(event); + } + + fn window_state_lock(&self) -> MutexGuard<'_, WindowState> { + self.window_state.lock().unwrap() + } +} + +struct ThreadMsgTargetData { + event_loop_runner: EventLoopRunnerShared, +} + +impl ThreadMsgTargetData { + fn send_event(&self, event: Event) { + self.event_loop_runner.send_event(event); + } +} + +/// The result of a subclass procedure (the message handling callback) +#[derive(Clone, Copy)] +pub(crate) enum ProcResult { + DefWindowProc(WPARAM), + Value(isize), +} + +pub struct EventLoop { + user_event_sender: Sender, + user_event_receiver: Receiver, + window_target: RootAEL, + msg_hook: Option bool + 'static>>, +} + +pub(crate) struct PlatformSpecificEventLoopAttributes { + pub(crate) any_thread: bool, + pub(crate) dpi_aware: bool, + pub(crate) msg_hook: Option bool + 'static>>, +} + +impl Default for PlatformSpecificEventLoopAttributes { + fn default() -> Self { + Self { + any_thread: false, + dpi_aware: true, + msg_hook: None, + } + } +} + +pub struct ActiveEventLoop { + thread_id: u32, + thread_msg_target: HWND, + pub(crate) runner_shared: EventLoopRunnerShared, +} + +impl EventLoop { + pub(crate) fn new( + attributes: &mut PlatformSpecificEventLoopAttributes, + ) -> Result { + let thread_id = unsafe { GetCurrentThreadId() }; + + if !attributes.any_thread && thread_id != main_thread_id() { + panic!( + "Initializing the event loop outside of the main thread is a significant \ + cross-platform compatibility hazard. If you absolutely need to create an \ + EventLoop on a different thread, you can use the \ + `EventLoopBuilderExtWindows::any_thread` function." + ); + } + + if attributes.dpi_aware { + become_dpi_aware(); + } + + let thread_msg_target = create_event_target_window(); + + let runner_shared = Rc::new(EventLoopRunner::new(thread_msg_target)); + + let (user_event_sender, user_event_receiver) = mpsc::channel(); + insert_event_target_window_data(thread_msg_target, runner_shared.clone()); + raw_input::register_all_mice_and_keyboards_for_raw_input( + thread_msg_target, + Default::default(), + ); + + Ok(EventLoop { + user_event_sender, + user_event_receiver, + window_target: RootAEL { + p: ActiveEventLoop { + thread_id, + thread_msg_target, + runner_shared, + }, + _marker: PhantomData, + }, + msg_hook: attributes.msg_hook.take(), + }) + } + + pub fn window_target(&self) -> &RootAEL { + &self.window_target + } + + pub fn run(mut self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootAEL), + { + self.run_on_demand(event_handler) + } + + pub fn run_on_demand(&mut self, mut event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootAEL), + { + { + let runner = &self.window_target.p.runner_shared; + + let event_loop_windows_ref = &self.window_target; + let user_event_receiver = &self.user_event_receiver; + // # Safety + // We make sure to call runner.clear_event_handler() before + // returning + unsafe { + runner.set_event_handler(move |event| { + // the shared `EventLoopRunner` is not parameterized + // `EventLoopProxy::send_event()` calls `PostMessage` + // to wakeup and dispatch a placeholder `UserEvent`, + // when we received the placeholder event here, the + // real UserEvent(T) should already be put in the + // mpsc channel and ready to be pulled + let event = match event.map_nonuser_event() { + Ok(non_user_event) => non_user_event, + Err(_user_event_placeholder) => Event::UserEvent( + user_event_receiver + .try_recv() + .expect("user event signaled but not received"), + ), + }; + event_handler(event, event_loop_windows_ref) + }); + } + } + + let exit_code = loop { + self.wait_and_dispatch_message(None); + + if let Some(code) = self.exit_code() { + break code; + } + + self.dispatch_peeked_messages(); + + if let Some(code) = self.exit_code() { + break code; + } + }; + + let runner = &self.window_target.p.runner_shared; + runner.loop_destroyed(); + + // # Safety + // We assume that this will effectively call `runner.clear_event_handler()` + // to meet the safety requirements for calling `runner.set_event_handler()` above. + runner.reset_runner(); + + if exit_code == 0 { + Ok(()) + } else { + Err(EventLoopError::ExitFailure(exit_code)) + } + } + + pub fn pump_events( + &mut self, + timeout: Option, + mut event_handler: F, + ) -> PumpStatus + where + F: FnMut(Event, &RootAEL), + { + { + let runner = &self.window_target.p.runner_shared; + let event_loop_windows_ref = &self.window_target; + let user_event_receiver = &self.user_event_receiver; + + // # Safety + // We make sure to call runner.clear_event_handler() before + // returning + // + // Note: we're currently assuming nothing can panic and unwind + // to leave the runner in an unsound state with an associated + // event handler. + unsafe { + runner.set_event_handler(move |event| { + let event = match event.map_nonuser_event() { + Ok(non_user_event) => non_user_event, + Err(_user_event_placeholder) => Event::UserEvent( + user_event_receiver + .recv() + .expect("user event signaled but not received"), + ), + }; + event_handler(event, event_loop_windows_ref) + }); + runner.wakeup(); + } + } + + self.wait_and_dispatch_message(timeout); + + if self.exit_code().is_none() { + self.dispatch_peeked_messages(); + } + + let runner = &self.window_target.p.runner_shared; + + let status = if let Some(code) = runner.exit_code() { + runner.loop_destroyed(); + + // Immediately reset the internal state for the loop to allow + // the loop to be run more than once. + runner.reset_runner(); + PumpStatus::Exit(code) + } else { + runner.prepare_wait(); + PumpStatus::Continue + }; + + // We wait until we've checked for an exit status before clearing the + // application callback, in case we need to dispatch a LoopExiting event + // + // # Safety + // This pairs up with our call to `runner.set_event_handler` and ensures + // the application's callback can't be held beyond its lifetime. + runner.clear_event_handler(); + + status + } + + /// Wait for one message and dispatch it, optionally with a timeout + fn wait_and_dispatch_message(&mut self, timeout: Option) { + fn get_msg_with_timeout(msg: &mut MSG, timeout: Option) -> PumpStatus { + unsafe { + // A timeout of None means wait indefinitely (so we don't need to call SetTimer) + let timer_id = + timeout.map(|timeout| SetTimer(0, 0, dur2timeout(timeout), None)); + let get_status = GetMessageW(msg, 0, 0, 0); + if let Some(timer_id) = timer_id { + KillTimer(0, timer_id); + } + // A return value of 0 implies `WM_QUIT` + if get_status == 0 { + PumpStatus::Exit(0) + } else { + PumpStatus::Continue + } + } + } + + /// Fetch the next MSG either via PeekMessage or GetMessage depending on whether the + /// requested timeout is `ZERO` (and so we don't want to block) + /// + /// Returns `None` if no MSG was read, else a `Continue` or `Exit` status + fn wait_for_msg(msg: &mut MSG, timeout: Option) -> Option { + if timeout == Some(Duration::ZERO) { + unsafe { + if PeekMessageW(msg, 0, 0, 0, PM_REMOVE) != 0 { + Some(PumpStatus::Continue) + } else { + None + } + } + } else { + Some(get_msg_with_timeout(msg, timeout)) + } + } + + let runner = &self.window_target.p.runner_shared; + + // We aim to be consistent with the MacOS backend which has a RunLoop + // observer that will dispatch AboutToWait when about to wait for + // events, and NewEvents after the RunLoop wakes up. + // + // We emulate similar behaviour by treating `GetMessage` as our wait + // point and wake up point (when it returns) and we drain all other + // pending messages via `PeekMessage` until we come back to "wait" via + // `GetMessage` + // + runner.prepare_wait(); + + let control_flow_timeout = match runner.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + let start = Instant::now(); + Some(wait_deadline.saturating_duration_since(start)) + } + }; + let timeout = min_timeout(control_flow_timeout, timeout); + + // # Safety + // The Windows API has no documented requirement for bitwise + // initializing a `MSG` struct (it can be uninitialized memory for the C + // API) and there's no API to construct or initialize a `MSG`. This + // is the simplest way avoid uninitialized memory in Rust + let mut msg = unsafe { mem::zeroed() }; + let msg_status = wait_for_msg(&mut msg, timeout); + + // Before we potentially exit, make sure to consistently emit an event for the wake up + runner.wakeup(); + + match msg_status { + None => {} // No MSG to dispatch + Some(PumpStatus::Exit(code)) => { + runner.set_exit_code(code); + } + Some(PumpStatus::Continue) => { + unsafe { + let handled = if let Some(callback) = self.msg_hook.as_deref_mut() { + callback(&mut msg as *mut _ as *mut _) + } else { + false + }; + if !handled { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + + if let Err(payload) = runner.take_panic_error() { + runner.reset_runner(); + panic::resume_unwind(payload); + } + } + } + } + + /// Dispatch all queued messages via `PeekMessageW` + fn dispatch_peeked_messages(&mut self) { + let runner = &self.window_target.p.runner_shared; + + // We generally want to continue dispatching all pending messages + // but we also allow dispatching to be interrupted as a means to + // ensure the `pump_events` won't indefinitely block an external + // event loop if there are too many pending events. This interrupt + // flag will be set after dispatching `RedrawRequested` events. + runner.interrupt_msg_dispatch.set(false); + + // # Safety + // The Windows API has no documented requirement for bitwise + // initializing a `MSG` struct (it can be uninitialized memory for the C + // API) and there's no API to construct or initialize a `MSG`. This + // is the simplest way avoid uninitialized memory in Rust + let mut msg = unsafe { mem::zeroed() }; + + loop { + unsafe { + if PeekMessageW(&mut msg, 0, 0, 0, PM_REMOVE) == false.into() { + break; + } + + let handled = if let Some(callback) = self.msg_hook.as_deref_mut() { + callback(&mut msg as *mut _ as *mut _) + } else { + false + }; + if !handled { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + + if let Err(payload) = runner.take_panic_error() { + runner.reset_runner(); + panic::resume_unwind(payload); + } + + if let Some(_code) = runner.exit_code() { + break; + } + + if runner.interrupt_msg_dispatch.get() { + break; + } + } + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + target_window: self.window_target.p.thread_msg_target, + event_send: self.user_event_sender.clone(), + } + } + + fn exit_code(&self) -> Option { + self.window_target.p.exit_code() + } +} + +impl ActiveEventLoop { + #[inline(always)] + pub(crate) fn create_thread_executor(&self) -> EventLoopThreadExecutor { + EventLoopThreadExecutor { + thread_id: self.thread_id, + target_window: self.thread_msg_target, + } + } + + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> RootCustomCursor { + let inner = match WinCursor::new(&source.inner.0) { + Ok(cursor) => cursor, + Err(err) => { + tracing::warn!("Failed to create custom cursor: {err}"); + WinCursor::Failed + } + }; + + RootCustomCursor { inner } + } + + // TODO: Investigate opportunities for caching + pub fn available_monitors(&self) -> VecDeque { + monitor::available_monitors() + } + + pub fn primary_monitor(&self) -> Option { + let monitor = monitor::primary_monitor(); + Some(monitor) + } + + #[cfg(feature = "rwh_06")] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Windows( + rwh_06::WindowsDisplayHandle::new(), + )) + } + + pub fn listen_device_events(&self, allowed: DeviceEvents) { + raw_input::register_all_mice_and_keyboards_for_raw_input( + self.thread_msg_target, + allowed, + ); + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.runner_shared.set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.runner_shared.control_flow() + } + + pub(crate) fn exit(&self) { + self.runner_shared.set_exit_code(0) + } + + pub(crate) fn exiting(&self) -> bool { + self.runner_shared.exit_code().is_some() + } + + pub(crate) fn clear_exit(&self) { + self.runner_shared.clear_exit(); + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } + + fn exit_code(&self) -> Option { + self.runner_shared.exit_code() + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::WindowsDisplayHandle::new().into()) + } +} + +/// Returns the id of the main thread. +/// +/// Windows has no real API to check if the current executing thread is the "main thread", unlike +/// macOS. +/// +/// Windows will let us look up the current thread's id, but there's no API that lets us check what +/// the id of the main thread is. We would somehow need to get the main thread's id before a +/// developer could spin off any other threads inside of the main entrypoint in order to emulate the +/// capabilities of other platforms. +/// +/// We can get the id of the main thread by using CRT initialization. CRT initialization can be used +/// to setup global state within a program. The OS will call a list of function pointers which +/// assign values to a static variable. To have get a hold of the main thread id, we need to place +/// our function pointer inside of the `.CRT$XCU` section so it is called before the main +/// entrypoint. +/// +/// Full details of CRT initialization can be found here: +/// +fn main_thread_id() -> u32 { + static mut MAIN_THREAD_ID: u32 = 0; + + /// Function pointer used in CRT initialization section to set the above static field's value. + + // Mark as used so this is not removable. + #[used] + #[allow(non_upper_case_globals)] + // Place the function pointer inside of CRT initialization section so it is loaded before + // main entrypoint. + // + // See: https://doc.rust-lang.org/stable/reference/abi.html#the-link_section-attribute + #[link_section = ".CRT$XCU"] + static INIT_MAIN_THREAD_ID: unsafe fn() = { + unsafe fn initer() { + unsafe { MAIN_THREAD_ID = GetCurrentThreadId() }; + } + initer + }; + + unsafe { MAIN_THREAD_ID } +} + +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| { + b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout))) + }) +} + +// Implementation taken from https://github.com/rust-lang/rust/blob/db5476571d9b27c862b95c1e64764b0ac8980e23/src/libstd/sys/windows/mod.rs +fn dur2timeout(dur: Duration) -> u32 { + // Note that a duration is a (u64, u32) (seconds, nanoseconds) pair, and the + // timeouts in windows APIs are typically u32 milliseconds. To translate, we + // have two pieces to take care of: + // + // * Nanosecond precision is rounded up + // * Greater than u32::MAX milliseconds (50 days) is rounded up to INFINITE (never time out). + dur.as_secs() + .checked_mul(1000) + .and_then(|ms| ms.checked_add((dur.subsec_nanos() as u64) / 1_000_000)) + .and_then(|ms| { + if dur.subsec_nanos() % 1_000_000 > 0 { + ms.checked_add(1) + } else { + Some(ms) + } + }) + .map(|ms| { + if ms > u32::MAX as u64 { + INFINITE + } else { + ms as u32 + } + }) + .unwrap_or(INFINITE) +} + +impl Drop for EventLoop { + fn drop(&mut self) { + unsafe { + DestroyWindow(self.window_target.p.thread_msg_target); + } + } +} + +pub(crate) struct EventLoopThreadExecutor { + thread_id: u32, + target_window: HWND, +} + +unsafe impl Send for EventLoopThreadExecutor {} +unsafe impl Sync for EventLoopThreadExecutor {} + +impl EventLoopThreadExecutor { + /// Check to see if we're in the parent event loop's thread. + pub(super) fn in_event_loop_thread(&self) -> bool { + let cur_thread_id = unsafe { GetCurrentThreadId() }; + self.thread_id == cur_thread_id + } + + /// Executes a function in the event loop thread. If we're already in the event loop thread, + /// we just call the function directly. + /// + /// The `Inserted` can be used to inject a `WindowState` for the callback to use. The state is + /// removed automatically if the callback receives a `WM_CLOSE` message for the window. + /// + /// Note that if you are using this to change some property of a window and updating + /// `WindowState` then you should call this within the lock of `WindowState`. Otherwise the + /// events may be sent to the other thread in different order to the one in which you set + /// `WindowState`, leaving them out of sync. + /// + /// Note that we use a FnMut instead of a FnOnce because we're too lazy to create an equivalent + /// to the unstable FnBox. + pub(super) fn execute_in_thread(&self, mut function: F) + where + F: FnMut() + Send + 'static, + { + unsafe { + if self.in_event_loop_thread() { + function(); + } else { + // We double-box because the first box is a fat pointer. + let boxed2: ThreadExecFn = Box::new(Box::new(function)); + + let raw = Box::into_raw(boxed2); + + let res = + PostMessageW(self.target_window, EXEC_MSG_ID.get(), raw as usize, 0); + assert!( + res != false.into(), + "PostMessage failed; is the messages queue full?" + ); + } + } + } +} + +type ThreadExecFn = Box>; + +pub struct EventLoopProxy { + target_window: HWND, + event_send: Sender, +} +unsafe impl Send for EventLoopProxy {} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + Self { + target_window: self.target_window, + event_send: self.event_send.clone(), + } + } +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.event_send + .send(event) + .map(|result| { + unsafe { + PostMessageW(self.target_window, USER_EVENT_MSG_ID.get(), 0, 0) + }; + result + }) + .map_err(|e| EventLoopClosed(e.0)) + } +} + +/// A lazily-initialized window message ID. +pub struct LazyMessageId { + /// The ID. + id: AtomicU32, + + /// The name of the message. + name: &'static str, +} + +/// An invalid custom window ID. +const INVALID_ID: u32 = 0x0; + +impl LazyMessageId { + /// Create a new `LazyId`. + const fn new(name: &'static str) -> Self { + Self { + id: AtomicU32::new(INVALID_ID), + name, + } + } + + /// Get the message ID. + pub fn get(&self) -> u32 { + // Load the ID. + let id = self.id.load(Ordering::Relaxed); + + if id != INVALID_ID { + return id; + } + + // Register the message. + // SAFETY: We are sure that the pointer is a valid C string ending with '\0'. + assert!(self.name.ends_with('\0')); + let new_id = unsafe { RegisterWindowMessageA(self.name.as_ptr()) }; + + assert_ne!( + new_id, + 0, + "RegisterWindowMessageA returned zero for '{}': {}", + self.name, + std::io::Error::last_os_error() + ); + + // Store the new ID. Since `RegisterWindowMessageA` returns the same value for any given + // string, the target value will always either be a). `INVALID_ID` or b). the + // correct ID. Therefore a compare-and-swap operation here (or really any + // consideration) is never necessary. + self.id.store(new_id, Ordering::Relaxed); + + new_id + } +} + +// Message sent by the `EventLoopProxy` when we want to wake up the thread. +// WPARAM and LPARAM are unused. +static USER_EVENT_MSG_ID: LazyMessageId = LazyMessageId::new("Winit::WakeupMsg\0"); +// Message sent when we want to execute a closure in the thread. +// WPARAM contains a Box> that must be retrieved with `Box::from_raw`, +// and LPARAM is unused. +static EXEC_MSG_ID: LazyMessageId = LazyMessageId::new("Winit::ExecMsg\0"); +// Message sent by a `Window` when it wants to be destroyed by the main thread. +// WPARAM and LPARAM are unused. +pub(crate) static DESTROY_MSG_ID: LazyMessageId = + LazyMessageId::new("Winit::DestroyMsg\0"); +// WPARAM is a bool specifying the `WindowFlags::MARKER_RETAIN_STATE_ON_SIZE` flag. See the +// documentation in the `window_state` module for more information. +pub(crate) static SET_RETAIN_STATE_ON_SIZE_MSG_ID: LazyMessageId = + LazyMessageId::new("Winit::SetRetainMaximized\0"); +static THREAD_EVENT_TARGET_WINDOW_CLASS: Lazy> = + Lazy::new(|| util::encode_wide("Winit Thread Event Target")); +/// When the taskbar is created, it registers a message with the "TaskbarCreated" string and then +/// broadcasts this message to all top-level windows +pub(crate) static TASKBAR_CREATED: LazyMessageId = LazyMessageId::new("TaskbarCreated\0"); + +fn create_event_target_window() -> HWND { + use windows_sys::Win32::UI::WindowsAndMessaging::{CS_HREDRAW, CS_VREDRAW}; + unsafe { + let class = WNDCLASSEXW { + cbSize: mem::size_of::() as u32, + style: CS_HREDRAW | CS_VREDRAW, + lpfnWndProc: Some(thread_event_target_callback), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: util::get_instance_handle(), + hIcon: 0, + hCursor: 0, // must be null in order for cursor state to work properly + hbrBackground: 0, + lpszMenuName: ptr::null(), + lpszClassName: THREAD_EVENT_TARGET_WINDOW_CLASS.as_ptr(), + hIconSm: 0, + }; + + RegisterClassExW(&class); + } + + unsafe { + // WS_EX_TOOLWINDOW prevents this window from ever showing up in the taskbar, which + // we want to avoid. If you remove this style, this window won't show up in the + // taskbar *initially*, but it can show up at some later point. This can sometimes + // happen on its own after several hours have passed, although this has proven + // difficult to reproduce. Alternatively, it can be manually triggered by killing + // `explorer.exe` and then starting the process back up. + // It is unclear why the bug is triggered by waiting for several hours. + let window = CreateWindowExW( + WS_EX_NOACTIVATE | WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW, + THREAD_EVENT_TARGET_WINDOW_CLASS.as_ptr(), + ptr::null(), + WS_OVERLAPPED, + 0, + 0, + 0, + 0, + 0, + 0, + util::get_instance_handle(), + ptr::null(), + ); + + super::set_window_long( + window, + GWL_STYLE, + // The window technically has to be visible to receive WM_PAINT messages (which are + // used for delivering events during resizes), but it isn't displayed to + // the user because of the LAYERED style. + (WS_VISIBLE | WS_POPUP) as isize, + ); + window + } +} + +fn insert_event_target_window_data( + thread_msg_target: HWND, + event_loop_runner: EventLoopRunnerShared, +) { + let userdata = ThreadMsgTargetData { event_loop_runner }; + let input_ptr = Box::into_raw(Box::new(userdata)); + + unsafe { + super::set_window_long(thread_msg_target, GWL_USERDATA, input_ptr as isize) + }; +} + +/// Capture mouse input, allowing `window` to receive mouse events when the cursor is outside of +/// the window. +unsafe fn capture_mouse(window: HWND, window_state: &mut WindowState) { + window_state.mouse.capture_count += 1; + unsafe { SetCapture(window) }; +} + +/// Release mouse input, stopping windows on this thread from receiving mouse input when the cursor +/// is outside the window. +unsafe fn release_mouse(mut window_state: MutexGuard<'_, WindowState>) { + window_state.mouse.capture_count = window_state.mouse.capture_count.saturating_sub(1); + if window_state.mouse.capture_count == 0 { + // ReleaseCapture() causes a WM_CAPTURECHANGED where we lock the window_state. + drop(window_state); + unsafe { ReleaseCapture() }; + } +} + +fn normalize_pointer_pressure(pressure: u32) -> Option { + match pressure { + 1..=1024 => Some(Force::Normalized(pressure as f64 / 1024.0)), + _ => None, + } +} + +/// Emit a `ModifiersChanged` event whenever modifiers have changed. +/// Returns the current modifier state +fn update_modifiers(window: HWND, userdata: &WindowData) { + use crate::event::WindowEvent::ModifiersChanged; + + let modifiers = { + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + layouts.get_agnostic_mods() + }; + + let mut window_state = userdata.window_state.lock().unwrap(); + if window_state.modifiers_state != modifiers { + window_state.modifiers_state = modifiers; + + // Drop lock + drop(window_state); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ModifiersChanged(modifiers.into()), + }); + } +} + +unsafe fn gain_active_focus(window: HWND, userdata: &WindowData) { + use crate::event::WindowEvent::Focused; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Focused(true), + }); +} + +unsafe fn lose_active_focus(window: HWND, userdata: &WindowData) { + use crate::event::WindowEvent::{Focused, ModifiersChanged}; + + userdata.window_state_lock().modifiers_state = ModifiersState::empty(); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ModifiersChanged(ModifiersState::empty().into()), + }); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Focused(false), + }); +} + +/// Any window whose callback is configured to this function will have its events propagated +/// through the events loop of the thread the window was created in. +// This is the callback that is called by `DispatchMessage` in the events loop. +// +// Returning 0 tells the Win32 API that the message has been processed. +// FIXME: detect WM_DWMCOMPOSITIONCHANGED and call DwmEnableBlurBehindWindow if necessary +pub(super) unsafe extern "system" fn public_window_callback( + window: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + let userdata = unsafe { super::get_window_long(window, GWL_USERDATA) }; + + let userdata_ptr = match (userdata, msg) { + (0, WM_NCCREATE) => { + let createstruct = unsafe { &mut *(lparam as *mut CREATESTRUCTW) }; + let initdata = + unsafe { &mut *(createstruct.lpCreateParams as *mut InitData<'_>) }; + + let result = match unsafe { initdata.on_nccreate(window) } { + Some(userdata) => unsafe { + super::set_window_long(window, GWL_USERDATA, userdata as _); + DefWindowProcW(window, msg, wparam, lparam) + }, + None => -1, // failed to create the window + }; + + return result; + } + // Getting here should quite frankly be impossible, + // but we'll make window creation fail here just in case. + (0, WM_CREATE) => return -1, + (_, WM_CREATE) => unsafe { + let createstruct = &mut *(lparam as *mut CREATESTRUCTW); + let initdata = createstruct.lpCreateParams; + let initdata = &mut *(initdata as *mut InitData<'_>); + + initdata.on_create(); + return DefWindowProcW(window, msg, wparam, lparam); + }, + (0, _) => return unsafe { DefWindowProcW(window, msg, wparam, lparam) }, + _ => userdata as *mut WindowData, + }; + + let (result, userdata_removed, recurse_depth) = { + let userdata = unsafe { &*(userdata_ptr) }; + + userdata.recurse_depth.set(userdata.recurse_depth.get() + 1); + + let result = unsafe { + public_window_callback_inner(window, msg, wparam, lparam, userdata) + }; + + let userdata_removed = userdata.userdata_removed.get(); + let recurse_depth = userdata.recurse_depth.get() - 1; + userdata.recurse_depth.set(recurse_depth); + + (result, userdata_removed, recurse_depth) + }; + + if userdata_removed && recurse_depth == 0 { + drop(unsafe { Box::from_raw(userdata_ptr) }); + } + + result +} + +unsafe fn public_window_callback_inner( + window: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + userdata: &WindowData, +) -> LRESULT { + let mut result = ProcResult::DefWindowProc(wparam); + + // Send new modifiers before sending key events. + let mods_changed_callback = || match msg { + WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP => { + update_modifiers(window, userdata); + result = ProcResult::Value(0); + } + _ => (), + }; + userdata + .event_loop_runner + .catch_unwind(mods_changed_callback) + .unwrap_or_else(|| result = ProcResult::Value(-1)); + + let keyboard_callback = || { + use crate::event::WindowEvent::KeyboardInput; + let events = userdata.key_event_builder.process_message( + window, + msg, + wparam, + lparam, + &mut result, + ); + for event in events { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: KeyboardInput { + device_id: DEVICE_ID, + event: event.event, + is_synthetic: event.is_synthetic, + }, + }); + } + }; + userdata + .event_loop_runner + .catch_unwind(keyboard_callback) + .unwrap_or_else(|| result = ProcResult::Value(-1)); + + // I decided to bind the closure to `callback` and pass it to catch_unwind rather than passing + // the closure to catch_unwind directly so that the match body indentation wouldn't change and + // the git blame and history would be preserved. + let callback = || match msg { + WM_NCCALCSIZE => { + let window_flags = userdata.window_state_lock().window_flags; + if wparam == 0 || window_flags.contains(WindowFlags::MARKER_DECORATIONS) { + result = ProcResult::DefWindowProc(wparam); + return; + } + + let params = unsafe { &mut *(lparam as *mut NCCALCSIZE_PARAMS) }; + + if util::is_maximized(window) { + // Limit the window size when maximized to the current monitor. + // Otherwise it would include the non-existent decorations. + // + // Use `MonitorFromRect` instead of `MonitorFromWindow` to select + // the correct monitor here. + // See https://github.com/MicrosoftEdge/WebView2Feedback/issues/2549 + let monitor = + unsafe { MonitorFromRect(¶ms.rgrc[0], MONITOR_DEFAULTTONULL) }; + if let Ok(monitor_info) = monitor::get_monitor_info(monitor) { + params.rgrc[0] = monitor_info.monitorInfo.rcWork; + } + } else if window_flags.contains(WindowFlags::MARKER_UNDECORATED_SHADOW) { + // Extend the client area to cover the whole non-client area. + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize#remarks + // + // HACK(msiglreith): To add the drop shadow we slightly tweak the non-client area. + // This leads to a small black 1px border on the top. Adding a margin manually + // on all 4 borders would result in the caption getting drawn by the DWM. + // + // Another option would be to allow the DWM to paint inside the client area. + // Unfortunately this results in janky resize behavior, where the compositor is + // ahead of the window surface. Currently, there seems no option to achieve this + // with the Windows API. + params.rgrc[0].top += 1; + params.rgrc[0].bottom += 1; + } + + result = ProcResult::Value(0); + } + + WM_ENTERSIZEMOVE => { + userdata.window_state_lock().set_window_flags_in_place(|f| { + f.insert(WindowFlags::MARKER_IN_SIZE_MOVE) + }); + result = ProcResult::Value(0); + } + + WM_EXITSIZEMOVE => { + let mut state = userdata.window_state_lock(); + if state.dragging { + state.dragging = false; + unsafe { PostMessageW(window, WM_LBUTTONUP, 0, lparam) }; + } + + state.set_window_flags_in_place(|f| { + f.remove(WindowFlags::MARKER_IN_SIZE_MOVE) + }); + result = ProcResult::Value(0); + } + + WM_NCLBUTTONDOWN => { + if wparam == HTCAPTION as _ { + unsafe { PostMessageW(window, WM_MOUSEMOVE, 0, lparam) }; + } + result = ProcResult::DefWindowProc(wparam); + } + + WM_CLOSE => { + use crate::event::WindowEvent::CloseRequested; + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CloseRequested, + }); + result = ProcResult::Value(0); + } + + WM_DESTROY => { + use crate::event::WindowEvent::Destroyed; + unsafe { RevokeDragDrop(window) }; + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Destroyed, + }); + result = ProcResult::Value(0); + } + + WM_NCDESTROY => { + unsafe { super::set_window_long(window, GWL_USERDATA, 0) }; + userdata.userdata_removed.set(true); + result = ProcResult::Value(0); + } + + WM_PAINT => { + userdata.window_state_lock().redraw_requested = + userdata.event_loop_runner.should_buffer(); + + // We'll buffer only in response to `UpdateWindow`, if win32 decides to redraw the + // window outside the normal flow of the event loop. This way mark event as handled + // and request a normal redraw with `RedrawWindow`. + if !userdata.event_loop_runner.should_buffer() { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::RedrawRequested, + }); + } + + // NOTE: calling `RedrawWindow` during `WM_PAINT` does nothing, since to mark + // `WM_PAINT` as handled we should call the `DefWindowProcW`. Call it and check whether + // user asked for redraw during `RedrawRequested` event handling and request it again + // after marking `WM_PAINT` as handled. + result = + ProcResult::Value(unsafe { DefWindowProcW(window, msg, wparam, lparam) }); + if std::mem::take(&mut userdata.window_state_lock().redraw_requested) { + unsafe { RedrawWindow(window, ptr::null(), 0, RDW_INTERNALPAINT) }; + } + } + WM_WINDOWPOSCHANGING => { + let mut window_state = userdata.window_state_lock(); + if let Some(ref mut fullscreen) = window_state.fullscreen { + let window_pos = unsafe { &mut *(lparam as *mut WINDOWPOS) }; + let new_rect = RECT { + left: window_pos.x, + top: window_pos.y, + right: window_pos.x + window_pos.cx, + bottom: window_pos.y + window_pos.cy, + }; + + const NOMOVE_OR_NOSIZE: u32 = SWP_NOMOVE | SWP_NOSIZE; + + let new_rect = if window_pos.flags & NOMOVE_OR_NOSIZE != 0 { + let cur_rect = util::WindowArea::Outer.get_rect(window).expect( + "Unexpected GetWindowRect failure; please report this error to \ + rust-windowing/winit", + ); + + match window_pos.flags & NOMOVE_OR_NOSIZE { + NOMOVE_OR_NOSIZE => None, + + SWP_NOMOVE => Some(RECT { + left: cur_rect.left, + top: cur_rect.top, + right: cur_rect.left + window_pos.cx, + bottom: cur_rect.top + window_pos.cy, + }), + + SWP_NOSIZE => Some(RECT { + left: window_pos.x, + top: window_pos.y, + right: window_pos.x - cur_rect.left + cur_rect.right, + bottom: window_pos.y - cur_rect.top + cur_rect.bottom, + }), + + _ => unreachable!(), + } + } else { + Some(new_rect) + }; + + if let Some(new_rect) = new_rect { + let new_monitor = + unsafe { MonitorFromRect(&new_rect, MONITOR_DEFAULTTONULL) }; + match fullscreen { + Fullscreen::Borderless(ref mut fullscreen_monitor) => { + if new_monitor != 0 + && fullscreen_monitor + .as_ref() + .map(|monitor| new_monitor != monitor.hmonitor()) + .unwrap_or(true) + { + if let Ok(new_monitor_info) = + monitor::get_monitor_info(new_monitor) + { + let new_monitor_rect = + new_monitor_info.monitorInfo.rcMonitor; + window_pos.x = new_monitor_rect.left; + window_pos.y = new_monitor_rect.top; + window_pos.cx = + new_monitor_rect.right - new_monitor_rect.left; + window_pos.cy = + new_monitor_rect.bottom - new_monitor_rect.top; + } + *fullscreen_monitor = + Some(MonitorHandle::new(new_monitor)); + } + } + Fullscreen::Exclusive(ref video_mode) => { + let old_monitor = video_mode.monitor.hmonitor(); + if let Ok(old_monitor_info) = + monitor::get_monitor_info(old_monitor) + { + let old_monitor_rect = + old_monitor_info.monitorInfo.rcMonitor; + window_pos.x = old_monitor_rect.left; + window_pos.y = old_monitor_rect.top; + window_pos.cx = + old_monitor_rect.right - old_monitor_rect.left; + window_pos.cy = + old_monitor_rect.bottom - old_monitor_rect.top; + } + } + } + } + } + + result = ProcResult::Value(0); + } + + // WM_MOVE supplies client area positions, so we send Moved here instead. + WM_WINDOWPOSCHANGED => { + use crate::event::WindowEvent::Moved; + + let windowpos = lparam as *const WINDOWPOS; + if unsafe { (*windowpos).flags & SWP_NOMOVE != SWP_NOMOVE } { + let physical_position = + unsafe { PhysicalPosition::new((*windowpos).x, (*windowpos).y) }; + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Moved(physical_position), + }); + } + + // This is necessary for us to still get sent WM_SIZE. + result = ProcResult::DefWindowProc(wparam); + } + + WM_SIZE => { + use crate::event::WindowEvent::Resized; + let w = super::loword(lparam as u32) as u32; + let h = super::hiword(lparam as u32) as u32; + + let physical_size = PhysicalSize::new(w, h); + let event = Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Resized(physical_size), + }; + + { + let mut w = userdata.window_state_lock(); + // See WindowFlags::MARKER_RETAIN_STATE_ON_SIZE docs for info on why this `if` check + // exists. + if !w + .window_flags() + .contains(WindowFlags::MARKER_RETAIN_STATE_ON_SIZE) + { + let maximized = wparam == SIZE_MAXIMIZED as usize; + w.set_window_flags_in_place(|f| { + f.set(WindowFlags::MAXIMIZED, maximized) + }); + } + } + userdata.send_event(event); + result = ProcResult::Value(0); + } + + WM_SIZING => { + /// Calculate the amount to add to round `value` to the nearest multiple of `increment`. + fn snap_to_nearest_increment_delta(value: i32, increment: i32) -> i32 { + let half_one = increment / 2; + let half_two = increment - half_one; + half_one - (value - half_two) % increment + } + + let scale_factor = userdata.window_state_lock().scale_factor; + let Some(inc) = userdata + .window_state_lock() + .resize_increments + .map(|inc| inc.to_physical(scale_factor)) + .filter(|inc| inc.width > 0 && inc.height > 0) + else { + result = ProcResult::Value(0); + return; + }; + + let side = wparam as u32; + // The desired new size of the window, decorations included. + let rect = unsafe { &mut *(lparam as *mut RECT) }; + + // We need to calculate the dimensions of the window decorations to get the true + // size of the window's contents + let adj_rect = userdata + .window_state_lock() + .window_flags + .adjust_rect(window, *rect) + .unwrap_or(*rect); + let deco_width = rect.left - adj_rect.left + adj_rect.right - rect.right; + let deco_height = rect.top - adj_rect.top + adj_rect.bottom - rect.bottom; + + let width = rect.right - rect.left - deco_width; + let height = rect.bottom - rect.top - deco_height; + + let mut width_delta = snap_to_nearest_increment_delta(width, inc.width); + let mut height_delta = snap_to_nearest_increment_delta(height, inc.height); + + // Windows won't bound check the value of `rect` after we're done here, so we + // have to check manually. If the width/height we snap to would go out of bounds, just + // set it equal to the min/max bound. + let min_size = userdata + .window_state_lock() + .min_size + .map(|size| size.to_physical(scale_factor)); + let max_size = userdata + .window_state_lock() + .max_size + .map(|size| size.to_physical(scale_factor)); + let final_width = width + width_delta; + let final_height = height + height_delta; + if let Some(min_size) = min_size { + if final_width < min_size.width { + width_delta += min_size.width - final_width; + } + if final_height < min_size.height { + height_delta += min_size.height - final_height; + } + } + if let Some(max_size) = max_size { + if final_width > max_size.width { + width_delta -= final_width - max_size.width; + } + if final_height > max_size.height { + height_delta -= final_height - max_size.height; + } + } + + match side { + WMSZ_LEFT | WMSZ_BOTTOMLEFT | WMSZ_TOPLEFT => { + rect.left -= width_delta; + } + WMSZ_RIGHT | WMSZ_BOTTOMRIGHT | WMSZ_TOPRIGHT => { + rect.right += width_delta; + } + _ => {} + } + + match side { + WMSZ_TOP | WMSZ_TOPLEFT | WMSZ_TOPRIGHT => { + rect.top -= height_delta; + } + WMSZ_BOTTOM | WMSZ_BOTTOMLEFT | WMSZ_BOTTOMRIGHT => { + rect.bottom += height_delta; + } + _ => {} + } + + result = ProcResult::DefWindowProc(wparam); + } + + WM_MENUCHAR => { + result = ProcResult::Value((MNC_CLOSE << 16) as isize); + } + + WM_IME_STARTCOMPOSITION => { + let ime_allowed = userdata.window_state_lock().ime_allowed; + if ime_allowed { + userdata.window_state_lock().ime_state = ImeState::Enabled; + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Enabled), + }); + } + + result = ProcResult::DefWindowProc(wparam); + } + + WM_IME_COMPOSITION => { + let ime_allowed_and_composing = { + let w = userdata.window_state_lock(); + w.ime_allowed && w.ime_state != ImeState::Disabled + }; + // Windows Hangul IME sends WM_IME_COMPOSITION after WM_IME_ENDCOMPOSITION, so + // check whether composing. + if ime_allowed_and_composing { + let ime_context = unsafe { ImeContext::current(window) }; + + if lparam == 0 { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }); + } + + // Google Japanese Input and ATOK have both flags, so + // first, receive composing result if exist. + if (lparam as u32 & GCS_RESULTSTR) != 0 { + if let Some(text) = unsafe { ime_context.get_composed_text() } { + userdata.window_state_lock().ime_state = ImeState::Enabled; + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Commit(text)), + }); + } + } + + // Next, receive preedit range for next composing if exist. + if (lparam as u32 & GCS_COMPSTR) != 0 { + if let Some((text, first, last)) = + unsafe { ime_context.get_composing_text_and_cursor() } + { + userdata.window_state_lock().ime_state = ImeState::Preedit; + let cursor_range = first.map(|f| (f, last.unwrap_or(f))); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(text, cursor_range)), + }); + } + } + } + + // Not calling DefWindowProc to hide composing text drawn by IME. + result = ProcResult::Value(0); + } + + WM_IME_ENDCOMPOSITION => { + let ime_allowed_or_composing = { + let w = userdata.window_state_lock(); + w.ime_allowed || w.ime_state != ImeState::Disabled + }; + if ime_allowed_or_composing { + if userdata.window_state_lock().ime_state == ImeState::Preedit { + // Windows Hangul IME sends WM_IME_COMPOSITION after WM_IME_ENDCOMPOSITION, so + // trying receiving composing result and commit if exists. + let ime_context = unsafe { ImeContext::current(window) }; + if let Some(text) = unsafe { ime_context.get_composed_text() } { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Commit(text)), + }); + } + } + + userdata.window_state_lock().ime_state = ImeState::Disabled; + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Disabled), + }); + } + + result = ProcResult::DefWindowProc(wparam); + } + + WM_IME_SETCONTEXT => { + // Hide composing text drawn by IME. + let wparam = wparam & (!ISC_SHOWUICOMPOSITIONWINDOW as usize); + result = ProcResult::DefWindowProc(wparam); + } + + // this is necessary for us to maintain minimize/restore state + WM_SYSCOMMAND => { + if wparam == SC_RESTORE as usize { + let mut w = userdata.window_state_lock(); + w.set_window_flags_in_place(|f| f.set(WindowFlags::MINIMIZED, false)); + } + if wparam == SC_MINIMIZE as usize { + let mut w = userdata.window_state_lock(); + w.set_window_flags_in_place(|f| f.set(WindowFlags::MINIMIZED, true)); + } + // Send `WindowEvent::Minimized` here if we decide to implement one + + if wparam == SC_SCREENSAVE as usize { + let window_state = userdata.window_state_lock(); + if window_state.fullscreen.is_some() { + result = ProcResult::Value(0); + return; + } + } + + result = ProcResult::DefWindowProc(wparam); + } + + WM_MOUSEMOVE => { + use crate::event::WindowEvent::{CursorEntered, CursorLeft, CursorMoved}; + + let x = super::get_x_lparam(lparam as u32) as i32; + let y = super::get_y_lparam(lparam as u32) as i32; + let position = PhysicalPosition::new(x as f64, y as f64); + + let cursor_moved; + { + let mut w = userdata.window_state_lock(); + let mouse_was_inside_window = + w.mouse.cursor_flags().contains(CursorFlags::IN_WINDOW); + + match get_pointer_move_kind(window, mouse_was_inside_window, x, y) { + PointerMoveKind::Enter => { + w.mouse + .set_cursor_flags(window, |f| { + f.set(CursorFlags::IN_WINDOW, true) + }) + .ok(); + + drop(w); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorEntered { + device_id: DEVICE_ID, + }, + }); + + // Calling TrackMouseEvent in order to receive mouse leave events. + unsafe { + TrackMouseEvent(&mut TRACKMOUSEEVENT { + cbSize: mem::size_of::() as u32, + dwFlags: TME_LEAVE, + hwndTrack: window, + dwHoverTime: HOVER_DEFAULT, + }) + }; + } + PointerMoveKind::Leave => { + w.mouse + .set_cursor_flags(window, |f| { + f.set(CursorFlags::IN_WINDOW, false) + }) + .ok(); + + drop(w); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorLeft { + device_id: DEVICE_ID, + }, + }); + } + PointerMoveKind::None => drop(w), + } + + // handle spurious WM_MOUSEMOVE messages + // see https://devblogs.microsoft.com/oldnewthing/20031001-00/?p=42343 + // and http://debugandconquer.blogspot.com/2015/08/the-cause-of-spurious-mouse-move.html + let mut w = userdata.window_state_lock(); + cursor_moved = w.mouse.last_position != Some(position); + w.mouse.last_position = Some(position); + } + + if cursor_moved { + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorMoved { + device_id: DEVICE_ID, + position, + }, + }); + } + + result = ProcResult::Value(0); + } + + WM_MOUSELEAVE => { + use crate::event::WindowEvent::CursorLeft; + { + let mut w = userdata.window_state_lock(); + w.mouse + .set_cursor_flags(window, |f| f.set(CursorFlags::IN_WINDOW, false)) + .ok(); + } + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorLeft { + device_id: DEVICE_ID, + }, + }); + + result = ProcResult::Value(0); + } + + WM_MOUSEWHEEL => { + use crate::event::MouseScrollDelta::LineDelta; + + let value = (wparam >> 16) as i16; + let value = value as f32 / WHEEL_DELTA as f32; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::MouseWheel { + device_id: DEVICE_ID, + delta: LineDelta(0.0, value), + phase: TouchPhase::Moved, + }, + }); + + result = ProcResult::Value(0); + } + + WM_MOUSEHWHEEL => { + use crate::event::MouseScrollDelta::LineDelta; + + let value = (wparam >> 16) as i16; + let value = -value as f32 / WHEEL_DELTA as f32; // NOTE: inverted! See https://github.com/rust-windowing/winit/pull/2105/ + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::MouseWheel { + device_id: DEVICE_ID, + delta: LineDelta(value, 0.0), + phase: TouchPhase::Moved, + }, + }); + + result = ProcResult::Value(0); + } + + WM_KEYDOWN | WM_SYSKEYDOWN => { + if msg == WM_SYSKEYDOWN { + result = ProcResult::DefWindowProc(wparam); + } + } + + WM_KEYUP | WM_SYSKEYUP => { + if msg == WM_SYSKEYUP && unsafe { GetMenu(window) != 0 } { + // let Windows handle event if the window has a native menu, a modal event loop + // is started here on Alt key up. + result = ProcResult::DefWindowProc(wparam); + } + } + + WM_LBUTTONDOWN => { + use crate::event::ElementState::Pressed; + use crate::event::MouseButton::Left; + use crate::event::WindowEvent::MouseInput; + + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { + device_id: DEVICE_ID, + state: Pressed, + button: Left, + }, + }); + result = ProcResult::Value(0); + } + + WM_LBUTTONUP => { + use crate::event::ElementState::Released; + use crate::event::MouseButton::Left; + use crate::event::WindowEvent::MouseInput; + + unsafe { release_mouse(userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { + device_id: DEVICE_ID, + state: Released, + button: Left, + }, + }); + result = ProcResult::Value(0); + } + + WM_RBUTTONDOWN => { + use crate::event::ElementState::Pressed; + use crate::event::MouseButton::Right; + use crate::event::WindowEvent::MouseInput; + + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { + device_id: DEVICE_ID, + state: Pressed, + button: Right, + }, + }); + result = ProcResult::Value(0); + } + + WM_RBUTTONUP => { + use crate::event::ElementState::Released; + use crate::event::MouseButton::Right; + use crate::event::WindowEvent::MouseInput; + + unsafe { release_mouse(userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { + device_id: DEVICE_ID, + state: Released, + button: Right, + }, + }); + result = ProcResult::Value(0); + } + + WM_MBUTTONDOWN => { + use crate::event::ElementState::Pressed; + use crate::event::MouseButton::Middle; + use crate::event::WindowEvent::MouseInput; + + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { + device_id: DEVICE_ID, + state: Pressed, + button: Middle, + }, + }); + result = ProcResult::Value(0); + } + + WM_MBUTTONUP => { + use crate::event::ElementState::Released; + use crate::event::MouseButton::Middle; + use crate::event::WindowEvent::MouseInput; + + unsafe { release_mouse(userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { + device_id: DEVICE_ID, + state: Released, + button: Middle, + }, + }); + result = ProcResult::Value(0); + } + + WM_XBUTTONDOWN => { + use crate::event::ElementState::Pressed; + use crate::event::MouseButton::{Back, Forward, Other}; + use crate::event::WindowEvent::MouseInput; + let xbutton = super::get_xbutton_wparam(wparam as u32); + + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { + device_id: DEVICE_ID, + state: Pressed, + button: match xbutton { + 1 => Back, + 2 => Forward, + _ => Other(xbutton), + }, + }, + }); + result = ProcResult::Value(0); + } + + WM_XBUTTONUP => { + use crate::event::ElementState::Released; + use crate::event::MouseButton::{Back, Forward, Other}; + use crate::event::WindowEvent::MouseInput; + let xbutton = super::get_xbutton_wparam(wparam as u32); + + unsafe { release_mouse(userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { + device_id: DEVICE_ID, + state: Released, + button: match xbutton { + 1 => Back, + 2 => Forward, + _ => Other(xbutton), + }, + }, + }); + result = ProcResult::Value(0); + } + + WM_CAPTURECHANGED => { + // lparam here is a handle to the window which is gaining mouse capture. + // If it is the same as our window, then we're essentially retaining the capture. This + // can happen if `SetCapture` is called on our window when it already has the mouse + // capture. + if lparam != window { + userdata.window_state_lock().mouse.capture_count = 0; + } + result = ProcResult::Value(0); + } + + WM_TOUCH => { + let pcount = super::loword(wparam as u32) as usize; + let mut inputs = Vec::with_capacity(pcount); + let htouch = lparam; + if unsafe { + GetTouchInputInfo( + htouch, + pcount as u32, + inputs.as_mut_ptr(), + mem::size_of::() as i32, + ) > 0 + } { + unsafe { inputs.set_len(pcount) }; + for input in &inputs { + let mut location = POINT { + x: input.x / 100, + y: input.y / 100, + }; + + if unsafe { ScreenToClient(window, &mut location) } == false.into() { + continue; + } + + let x = location.x as f64 + (input.x % 100) as f64 / 100f64; + let y = location.y as f64 + (input.y % 100) as f64 / 100f64; + let location = PhysicalPosition::new(x, y); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Touch(Touch { + phase: if util::has_flag(input.dwFlags, TOUCHEVENTF_DOWN) { + TouchPhase::Started + } else if util::has_flag(input.dwFlags, TOUCHEVENTF_UP) { + TouchPhase::Ended + } else if util::has_flag(input.dwFlags, TOUCHEVENTF_MOVE) { + TouchPhase::Moved + } else { + continue; + }, + location, + force: None, // WM_TOUCH doesn't support pressure information + id: input.dwID as u64, + device_id: DEVICE_ID, + }), + }); + } + } + unsafe { CloseTouchInputHandle(htouch) }; + result = ProcResult::Value(0); + } + + WM_POINTERDOWN | WM_POINTERUPDATE | WM_POINTERUP => { + if let ( + Some(GetPointerFrameInfoHistory), + Some(SkipPointerFrameMessages), + Some(GetPointerDeviceRects), + ) = ( + *util::GET_POINTER_FRAME_INFO_HISTORY, + *util::SKIP_POINTER_FRAME_MESSAGES, + *util::GET_POINTER_DEVICE_RECTS, + ) { + let pointer_id = super::loword(wparam as u32) as u32; + let mut entries_count = 0u32; + let mut pointers_count = 0u32; + if unsafe { + GetPointerFrameInfoHistory( + pointer_id, + &mut entries_count, + &mut pointers_count, + ptr::null_mut(), + ) + } == false.into() + { + result = ProcResult::Value(0); + return; + } + + let pointer_info_count = (entries_count * pointers_count) as usize; + let mut pointer_infos = Vec::with_capacity(pointer_info_count); + if unsafe { + GetPointerFrameInfoHistory( + pointer_id, + &mut entries_count, + &mut pointers_count, + pointer_infos.as_mut_ptr(), + ) + } == false.into() + { + result = ProcResult::Value(0); + return; + } + unsafe { pointer_infos.set_len(pointer_info_count) }; + + // https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getpointerframeinfohistory + // The information retrieved appears in reverse chronological order, with the most + // recent entry in the first row of the returned array + for pointer_info in pointer_infos.iter().rev() { + let mut device_rect = mem::MaybeUninit::uninit(); + let mut display_rect = mem::MaybeUninit::uninit(); + + if unsafe { + GetPointerDeviceRects( + pointer_info.sourceDevice, + device_rect.as_mut_ptr(), + display_rect.as_mut_ptr(), + ) + } == false.into() + { + continue; + } + + let device_rect = unsafe { device_rect.assume_init() }; + let display_rect = unsafe { display_rect.assume_init() }; + + // For the most precise himetric to pixel conversion we calculate the ratio + // between the resolution of the display device (pixel) and + // the touch device (himetric). + let himetric_to_pixel_ratio_x = + (display_rect.right - display_rect.left) as f64 + / (device_rect.right - device_rect.left) as f64; + let himetric_to_pixel_ratio_y = + (display_rect.bottom - display_rect.top) as f64 + / (device_rect.bottom - device_rect.top) as f64; + + // ptHimetricLocation's origin is 0,0 even on multi-monitor setups. + // On multi-monitor setups we need to translate the himetric location to the + // rect of the display device it's attached to. + let x = display_rect.left as f64 + + pointer_info.ptHimetricLocation.x as f64 + * himetric_to_pixel_ratio_x; + let y = display_rect.top as f64 + + pointer_info.ptHimetricLocation.y as f64 + * himetric_to_pixel_ratio_y; + + let mut location = POINT { + x: x.floor() as i32, + y: y.floor() as i32, + }; + + if unsafe { ScreenToClient(window, &mut location) } == false.into() { + continue; + } + + let force = match pointer_info.pointerType { + PT_TOUCH => { + let mut touch_info = mem::MaybeUninit::uninit(); + util::GET_POINTER_TOUCH_INFO.and_then(|GetPointerTouchInfo| { + match unsafe { + GetPointerTouchInfo( + pointer_info.pointerId, + touch_info.as_mut_ptr(), + ) + } { + 0 => None, + _ => normalize_pointer_pressure(unsafe { + touch_info.assume_init().pressure + }), + } + }) + } + PT_PEN => { + let mut pen_info = mem::MaybeUninit::uninit(); + util::GET_POINTER_PEN_INFO.and_then( + |GetPointerPenInfo| match unsafe { + GetPointerPenInfo( + pointer_info.pointerId, + pen_info.as_mut_ptr(), + ) + } { + 0 => None, + _ => normalize_pointer_pressure(unsafe { + pen_info.assume_init().pressure + }), + }, + ) + } + _ => None, + }; + + let x = location.x as f64 + x.fract(); + let y = location.y as f64 + y.fract(); + let location = PhysicalPosition::new(x, y); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Touch(Touch { + phase: if util::has_flag( + pointer_info.pointerFlags, + POINTER_FLAG_DOWN, + ) { + TouchPhase::Started + } else if util::has_flag( + pointer_info.pointerFlags, + POINTER_FLAG_UP, + ) { + TouchPhase::Ended + } else if util::has_flag( + pointer_info.pointerFlags, + POINTER_FLAG_UPDATE, + ) { + TouchPhase::Moved + } else { + continue; + }, + location, + force, + id: pointer_info.pointerId as u64, + device_id: DEVICE_ID, + }), + }); + } + + unsafe { SkipPointerFrameMessages(pointer_id) }; + } + result = ProcResult::Value(0); + } + + WM_NCACTIVATE => { + let is_active = wparam != false.into(); + let active_focus_changed = userdata.window_state_lock().set_active(is_active); + if active_focus_changed { + if is_active { + unsafe { gain_active_focus(window, userdata) }; + } else { + unsafe { lose_active_focus(window, userdata) }; + } + } + result = ProcResult::DefWindowProc(wparam); + } + + WM_SETFOCUS => { + let active_focus_changed = userdata.window_state_lock().set_focused(true); + if active_focus_changed { + unsafe { gain_active_focus(window, userdata) }; + } + result = ProcResult::Value(0); + } + + WM_KILLFOCUS => { + let active_focus_changed = userdata.window_state_lock().set_focused(false); + if active_focus_changed { + unsafe { lose_active_focus(window, userdata) }; + } + result = ProcResult::Value(0); + } + + WM_SETCURSOR => { + let set_cursor_to = { + let window_state = userdata.window_state_lock(); + // The return value for the preceding `WM_NCHITTEST` message is conveniently + // provided through the low-order word of lParam. We use that here since + // `WM_MOUSEMOVE` seems to come after `WM_SETCURSOR` for a given cursor movement. + let in_client_area = super::loword(lparam as u32) as u32 == HTCLIENT; + if in_client_area { + Some(window_state.mouse.selected_cursor.clone()) + } else { + None + } + }; + + match set_cursor_to { + Some(selected_cursor) => { + let hcursor = match selected_cursor { + SelectedCursor::Named(cursor_icon) => unsafe { + LoadCursorW(0, util::to_windows_cursor(cursor_icon)) + }, + SelectedCursor::Custom(cursor) => cursor.as_raw_handle(), + }; + unsafe { SetCursor(hcursor) }; + result = ProcResult::Value(0); + } + None => result = ProcResult::DefWindowProc(wparam), + } + } + + WM_GETMINMAXINFO => { + let mmi = lparam as *mut MINMAXINFO; + + let window_state = userdata.window_state_lock(); + let window_flags = window_state.window_flags; + + if window_state.min_size.is_some() || window_state.max_size.is_some() { + if let Some(min_size) = window_state.min_size { + let min_size = min_size.to_physical(window_state.scale_factor); + let (width, height): (u32, u32) = + window_flags.adjust_size(window, min_size).into(); + unsafe { + (*mmi).ptMinTrackSize = POINT { + x: width as i32, + y: height as i32, + } + }; + } + if let Some(max_size) = window_state.max_size { + let max_size = max_size.to_physical(window_state.scale_factor); + let (width, height): (u32, u32) = + window_flags.adjust_size(window, max_size).into(); + unsafe { + (*mmi).ptMaxTrackSize = POINT { + x: width as i32, + y: height as i32, + } + }; + } + } + + result = ProcResult::Value(0); + } + + // Only sent on Windows 8.1 or newer. On Windows 7 and older user has to log out to change + // DPI, therefore all applications are closed while DPI is changing. + WM_DPICHANGED => { + use crate::event::WindowEvent::ScaleFactorChanged; + + // This message actually provides two DPI values - x and y. However MSDN says that + // "you only need to use either the X-axis or the Y-axis value when scaling your + // application since they are the same". + // https://msdn.microsoft.com/en-us/library/windows/desktop/dn312083(v=vs.85).aspx + let new_dpi_x = super::loword(wparam as u32) as u32; + let new_scale_factor = dpi_to_scale_factor(new_dpi_x); + let old_scale_factor: f64; + + let (allow_resize, window_flags) = { + let mut window_state = userdata.window_state_lock(); + old_scale_factor = window_state.scale_factor; + window_state.scale_factor = new_scale_factor; + + if new_scale_factor == old_scale_factor { + result = ProcResult::Value(0); + return; + } + + let allow_resize = window_state.fullscreen.is_none() + && !window_state.window_flags().contains(WindowFlags::MAXIMIZED); + + (allow_resize, window_state.window_flags) + }; + + // New size as suggested by Windows. + let suggested_rect = unsafe { *(lparam as *const RECT) }; + + // The window rect provided is the window's outer size, not it's inner size. However, + // win32 doesn't provide an `UnadjustWindowRectEx` function to get the client rect from + // the outer rect, so we instead adjust the window rect to get the decoration margins + // and remove them from the outer size. + let margin_left: i32; + let margin_top: i32; + // let margin_right: i32; + // let margin_bottom: i32; + { + let adjusted_rect = window_flags + .adjust_rect(window, suggested_rect) + .unwrap_or(suggested_rect); + margin_left = suggested_rect.left - adjusted_rect.left; + margin_top = suggested_rect.top - adjusted_rect.top; + // margin_right = adjusted_rect.right - suggested_rect.right; + // margin_bottom = adjusted_rect.bottom - suggested_rect.bottom; + } + + let old_physical_inner_rect = util::WindowArea::Inner + .get_rect(window) + .expect("failed to query (old) inner window area"); + let old_physical_inner_size = PhysicalSize::new( + (old_physical_inner_rect.right - old_physical_inner_rect.left) as u32, + (old_physical_inner_rect.bottom - old_physical_inner_rect.top) as u32, + ); + + // `allow_resize` prevents us from re-applying DPI adjustment to the restored size after + // exiting fullscreen (the restored size is already DPI adjusted). + let new_physical_inner_size = match allow_resize { + // We calculate our own size because the default suggested rect doesn't do a great + // job of preserving the window's logical size. + true => old_physical_inner_size + .to_logical::(old_scale_factor) + .to_physical::(new_scale_factor), + false => old_physical_inner_size, + }; + + let new_inner_size = Arc::new(Mutex::new(new_physical_inner_size)); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ScaleFactorChanged { + scale_factor: new_scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade( + &new_inner_size, + )), + }, + }); + + let new_physical_inner_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); + + let dragging_window: bool; + + { + let window_state = userdata.window_state_lock(); + dragging_window = window_state + .window_flags() + .contains(WindowFlags::MARKER_IN_SIZE_MOVE); + // Unset maximized if we're changing the window's size. + if new_physical_inner_size != old_physical_inner_size { + WindowState::set_window_flags(window_state, window, |f| { + f.set(WindowFlags::MAXIMIZED, false) + }); + } + } + + let new_outer_rect: RECT; + { + let suggested_ul = ( + suggested_rect.left + margin_left, + suggested_rect.top + margin_top, + ); + + let mut conservative_rect = RECT { + left: suggested_ul.0, + top: suggested_ul.1, + right: suggested_ul.0 + new_physical_inner_size.width as i32, + bottom: suggested_ul.1 + new_physical_inner_size.height as i32, + }; + + conservative_rect = window_flags + .adjust_rect(window, conservative_rect) + .unwrap_or(conservative_rect); + + // If we're dragging the window, offset the window so that the cursor's + // relative horizontal position in the title bar is preserved. + if dragging_window { + let bias = { + let cursor_pos = { + let mut pos = unsafe { mem::zeroed() }; + unsafe { GetCursorPos(&mut pos) }; + pos + }; + let suggested_cursor_horizontal_ratio = + (cursor_pos.x - suggested_rect.left) as f64 + / (suggested_rect.right - suggested_rect.left) as f64; + + (cursor_pos.x + - (suggested_cursor_horizontal_ratio + * (conservative_rect.right - conservative_rect.left) + as f64) as i32) + - conservative_rect.left + }; + conservative_rect.left += bias; + conservative_rect.right += bias; + } + + // Check to see if the new window rect is on the monitor with the new DPI factor. + // If it isn't, offset the window so that it is. + let new_dpi_monitor = + unsafe { MonitorFromWindow(window, MONITOR_DEFAULTTONULL) }; + let conservative_rect_monitor = + unsafe { MonitorFromRect(&conservative_rect, MONITOR_DEFAULTTONULL) }; + new_outer_rect = if conservative_rect_monitor == new_dpi_monitor { + conservative_rect + } else { + let get_monitor_rect = |monitor| { + let mut monitor_info = MONITORINFO { + cbSize: mem::size_of::() as _, + ..unsafe { mem::zeroed() } + }; + unsafe { GetMonitorInfoW(monitor, &mut monitor_info) }; + monitor_info.rcMonitor + }; + let wrong_monitor = conservative_rect_monitor; + let wrong_monitor_rect = get_monitor_rect(wrong_monitor); + let new_monitor_rect = get_monitor_rect(new_dpi_monitor); + + // The direction to nudge the window in to get the window onto the monitor with + // the new DPI factor. We calculate this by seeing which monitor edges are + // shared and nudging away from the wrong monitor based on those. + #[allow(clippy::bool_to_int_with_if)] + let delta_nudge_to_dpi_monitor = ( + if wrong_monitor_rect.left == new_monitor_rect.right { + -1 + } else if wrong_monitor_rect.right == new_monitor_rect.left { + 1 + } else { + 0 + }, + if wrong_monitor_rect.bottom == new_monitor_rect.top { + 1 + } else if wrong_monitor_rect.top == new_monitor_rect.bottom { + -1 + } else { + 0 + }, + ); + + let abort_after_iterations = new_monitor_rect.right + - new_monitor_rect.left + + new_monitor_rect.bottom + - new_monitor_rect.top; + for _ in 0..abort_after_iterations { + conservative_rect.left += delta_nudge_to_dpi_monitor.0; + conservative_rect.right += delta_nudge_to_dpi_monitor.0; + conservative_rect.top += delta_nudge_to_dpi_monitor.1; + conservative_rect.bottom += delta_nudge_to_dpi_monitor.1; + + if unsafe { + MonitorFromRect(&conservative_rect, MONITOR_DEFAULTTONULL) + } == new_dpi_monitor + { + break; + } + } + + conservative_rect + }; + } + + unsafe { + SetWindowPos( + window, + 0, + new_outer_rect.left, + new_outer_rect.top, + new_outer_rect.right - new_outer_rect.left, + new_outer_rect.bottom - new_outer_rect.top, + SWP_NOZORDER | SWP_NOACTIVATE, + ) + }; + + result = ProcResult::Value(0); + } + + WM_SETTINGCHANGE => { + use crate::event::WindowEvent::ThemeChanged; + + let preferred_theme = userdata.window_state_lock().preferred_theme; + + if preferred_theme.is_none() { + let new_theme = try_theme(window, preferred_theme); + let mut window_state = userdata.window_state_lock(); + + if window_state.current_theme != new_theme { + window_state.current_theme = new_theme; + drop(window_state); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ThemeChanged(new_theme), + }); + } + } + result = ProcResult::DefWindowProc(wparam); + } + + _ => { + if msg == DESTROY_MSG_ID.get() { + unsafe { DestroyWindow(window) }; + result = ProcResult::Value(0); + } else if msg == SET_RETAIN_STATE_ON_SIZE_MSG_ID.get() { + let mut window_state = userdata.window_state_lock(); + window_state.set_window_flags_in_place(|f| { + f.set(WindowFlags::MARKER_RETAIN_STATE_ON_SIZE, wparam != 0) + }); + result = ProcResult::Value(0); + } else if msg == TASKBAR_CREATED.get() { + let window_state = userdata.window_state_lock(); + unsafe { set_skip_taskbar(window, window_state.skip_taskbar) }; + result = ProcResult::DefWindowProc(wparam); + } else { + result = ProcResult::DefWindowProc(wparam); + } + } + }; + + userdata + .event_loop_runner + .catch_unwind(callback) + .unwrap_or_else(|| result = ProcResult::Value(-1)); + + match result { + ProcResult::DefWindowProc(wparam) => unsafe { + DefWindowProcW(window, msg, wparam, lparam) + }, + ProcResult::Value(val) => val, + } +} + +unsafe extern "system" fn thread_event_target_callback( + window: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + let userdata_ptr = unsafe { super::get_window_long(window, GWL_USERDATA) } + as *mut ThreadMsgTargetData; + if userdata_ptr.is_null() { + // `userdata_ptr` will always be null for the first `WM_GETMINMAXINFO`, as well as + // `WM_NCCREATE` and `WM_CREATE`. + return unsafe { DefWindowProcW(window, msg, wparam, lparam) }; + } + let userdata = unsafe { Box::from_raw(userdata_ptr) }; + + if msg != WM_PAINT { + unsafe { RedrawWindow(window, ptr::null(), 0, RDW_INTERNALPAINT) }; + } + + let mut userdata_removed = false; + + // I decided to bind the closure to `callback` and pass it to catch_unwind rather than passing + // the closure to catch_unwind directly so that the match body indentation wouldn't change and + // the git blame and history would be preserved. + let callback = || match msg { + WM_NCDESTROY => { + unsafe { super::set_window_long(window, GWL_USERDATA, 0) }; + userdata_removed = true; + 0 + } + WM_PAINT => unsafe { + ValidateRect(window, ptr::null()); + // Default WM_PAINT behaviour. This makes sure modals and popups are shown immediately + // when opening them. + DefWindowProcW(window, msg, wparam, lparam) + }, + + WM_INPUT_DEVICE_CHANGE => { + let event = match wparam as u32 { + GIDC_ARRIVAL => DeviceEvent::Added, + GIDC_REMOVAL => DeviceEvent::Removed, + _ => unreachable!(), + }; + + userdata.send_event(Event::DeviceEvent { + device_id: wrap_device_id(lparam as u32), + event, + }); + + 0 + } + + WM_INPUT => { + if let Some(data) = raw_input::get_raw_input_data(lparam as _) { + unsafe { handle_raw_input(&userdata, data) }; + } + + unsafe { DefWindowProcW(window, msg, wparam, lparam) } + } + + _ if msg == USER_EVENT_MSG_ID.get() => { + // synthesis a placeholder UserEvent, so that if the callback is + // re-entered it can be buffered for later delivery. the real + // user event is still in the mpsc channel and will be pulled + // once the placeholder event is delivered to the wrapper + // `event_handler` + userdata.send_event(Event::UserEvent(UserEventPlaceholder)); + 0 + } + _ if msg == EXEC_MSG_ID.get() => { + let mut function: ThreadExecFn = unsafe { Box::from_raw(wparam as *mut _) }; + function(); + 0 + } + _ => unsafe { DefWindowProcW(window, msg, wparam, lparam) }, + }; + + let result = userdata + .event_loop_runner + .catch_unwind(callback) + .unwrap_or(-1); + if userdata_removed { + drop(userdata); + } else { + Box::leak(userdata); + } + result +} + +unsafe fn handle_raw_input(userdata: &ThreadMsgTargetData, data: RAWINPUT) { + use crate::event::DeviceEvent::{Button, Key, Motion, MouseMotion, MouseWheel}; + use crate::event::ElementState::{Pressed, Released}; + use crate::event::MouseScrollDelta::LineDelta; + + let device_id = wrap_device_id(data.header.hDevice as _); + + if data.header.dwType == RIM_TYPEMOUSE { + let mouse = unsafe { data.data.mouse }; + + if util::has_flag(mouse.usFlags as u32, MOUSE_MOVE_RELATIVE) { + let x = mouse.lLastX as f64; + let y = mouse.lLastY as f64; + + if x != 0.0 { + userdata.send_event(Event::DeviceEvent { + device_id, + event: Motion { axis: 0, value: x }, + }); + } + + if y != 0.0 { + userdata.send_event(Event::DeviceEvent { + device_id, + event: Motion { axis: 1, value: y }, + }); + } + + if x != 0.0 || y != 0.0 { + userdata.send_event(Event::DeviceEvent { + device_id, + event: MouseMotion { delta: (x, y) }, + }); + } + } + + let button_flags = unsafe { mouse.Anonymous.Anonymous.usButtonFlags }; + if util::has_flag(button_flags as u32, RI_MOUSE_WHEEL) { + let button_data = unsafe { mouse.Anonymous.Anonymous.usButtonData } as i16; + let delta = button_data as f32 / WHEEL_DELTA as f32; + userdata.send_event(Event::DeviceEvent { + device_id, + event: MouseWheel { + delta: LineDelta(0.0, delta), + }, + }); + } + if util::has_flag(button_flags as u32, RI_MOUSE_HWHEEL) { + let button_data = unsafe { mouse.Anonymous.Anonymous.usButtonData } as i16; + let delta = -button_data as f32 / WHEEL_DELTA as f32; + userdata.send_event(Event::DeviceEvent { + device_id, + event: MouseWheel { + delta: LineDelta(delta, 0.0), + }, + }); + } + + let button_state = raw_input::get_raw_mouse_button_state(button_flags as u32); + for (button, state) in button_state.iter().enumerate() { + if let Some(state) = *state { + userdata.send_event(Event::DeviceEvent { + device_id, + event: Button { + button: button as _, + state, + }, + }); + } + } + } else if data.header.dwType == RIM_TYPEKEYBOARD { + let keyboard = unsafe { data.data.keyboard }; + + let pressed = keyboard.Message == WM_KEYDOWN || keyboard.Message == WM_SYSKEYDOWN; + let released = keyboard.Message == WM_KEYUP || keyboard.Message == WM_SYSKEYUP; + + if !pressed && !released { + return; + } + + if let Some(physical_key) = raw_input::get_keyboard_physical_key(keyboard) { + let state = if pressed { Pressed } else { Released }; + + userdata.send_event(Event::DeviceEvent { + device_id, + event: Key(RawKeyEvent { + physical_key, + state, + }), + }); + } + } +} + +enum PointerMoveKind { + /// Pointer enterd to the window. + Enter, + /// Pointer leaved the window client area. + Leave, + /// Pointer is inside the window or `GetClientRect` failed. + None, +} + +fn get_pointer_move_kind( + window: HWND, + mouse_was_inside_window: bool, + x: i32, + y: i32, +) -> PointerMoveKind { + let rect: RECT = unsafe { + let mut rect: RECT = mem::zeroed(); + if GetClientRect(window, &mut rect) == false.into() { + return PointerMoveKind::None; // exit early if GetClientRect failed + } + rect + }; + + let x = (rect.left..rect.right).contains(&x); + let y = (rect.top..rect.bottom).contains(&y); + + if !mouse_was_inside_window && x && y { + PointerMoveKind::Enter + } else if mouse_was_inside_window && !(x && y) { + PointerMoveKind::Leave + } else { + PointerMoveKind::None + } +} diff --git a/rio-window/src/platform_impl/windows/event_loop/runner.rs b/rio-window/src/platform_impl/windows/event_loop/runner.rs new file mode 100644 index 00000000..7bbae2c9 --- /dev/null +++ b/rio-window/src/platform_impl/windows/event_loop/runner.rs @@ -0,0 +1,428 @@ +use std::any::Any; +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use std::time::Instant; +use std::{mem, panic}; + +use windows_sys::Win32::Foundation::HWND; + +use crate::dpi::PhysicalSize; +use crate::event::{Event, InnerSizeWriter, StartCause, WindowEvent}; +use crate::platform_impl::platform::event_loop::{WindowData, GWL_USERDATA}; +use crate::platform_impl::platform::get_window_long; +use crate::window::WindowId; + +use super::ControlFlow; + +pub(crate) type EventLoopRunnerShared = Rc>; + +type EventHandler = Cell)>>>; + +pub(crate) struct EventLoopRunner { + // The event loop's win32 handles + pub(super) thread_msg_target: HWND, + + // Setting this will ensure pump_events will return to the external + // loop asap. E.g. set after each RedrawRequested to ensure pump_events + // can't stall an external loop beyond a frame + pub(super) interrupt_msg_dispatch: Cell, + + control_flow: Cell, + exit: Cell>, + runner_state: Cell, + last_events_cleared: Cell, + event_handler: EventHandler, + event_buffer: RefCell>>, + + panic_error: Cell>, +} + +pub type PanicError = Box; + +/// See `move_state_to` function for details on how the state loop works. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum RunnerState { + /// The event loop has just been created, and an `Init` event must be sent. + Uninitialized, + /// The event loop is idling. + Idle, + /// The event loop is handling the OS's events and sending them to the user's callback. + /// `NewEvents` has been sent, and `AboutToWait` hasn't. + HandlingMainEvents, + /// The event loop has been destroyed. No other events will be emitted. + Destroyed, +} + +enum BufferedEvent { + Event(Event), + ScaleFactorChanged(WindowId, f64, PhysicalSize), +} + +impl EventLoopRunner { + pub(crate) fn new(thread_msg_target: HWND) -> EventLoopRunner { + EventLoopRunner { + thread_msg_target, + interrupt_msg_dispatch: Cell::new(false), + runner_state: Cell::new(RunnerState::Uninitialized), + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(None), + panic_error: Cell::new(None), + last_events_cleared: Cell::new(Instant::now()), + event_handler: Cell::new(None), + event_buffer: RefCell::new(VecDeque::new()), + } + } + + /// Associate the application's event handler with the runner + /// + /// # Safety + /// This is ignoring the lifetime of the application handler (which may not + /// outlive the EventLoopRunner) and can lead to undefined behaviour if + /// the handler is not cleared before the end of real lifetime. + /// + /// All public APIs that take an event handler (`run`, `run_on_demand`, + /// `pump_events`) _must_ pair a call to `set_event_handler` with + /// a call to `clear_event_handler` before returning to avoid + /// undefined behaviour. + pub(crate) unsafe fn set_event_handler(&self, f: F) + where + F: FnMut(Event), + { + // Erase closure lifetime. + // SAFETY: Caller upholds that the lifetime of the closure is upheld. + let f = unsafe { + mem::transmute::)>, Box)>>( + Box::new(f), + ) + }; + let old_event_handler = self.event_handler.replace(Some(f)); + assert!(old_event_handler.is_none()); + } + + pub(crate) fn clear_event_handler(&self) { + self.event_handler.set(None); + } + + pub(crate) fn reset_runner(&self) { + let EventLoopRunner { + thread_msg_target: _, + interrupt_msg_dispatch, + runner_state, + panic_error, + control_flow: _, + exit, + last_events_cleared: _, + event_handler, + event_buffer: _, + } = self; + interrupt_msg_dispatch.set(false); + runner_state.set(RunnerState::Uninitialized); + panic_error.set(None); + exit.set(None); + event_handler.set(None); + } +} + +/// State retrieval functions. +impl EventLoopRunner { + #[allow(unused)] + pub fn thread_msg_target(&self) -> HWND { + self.thread_msg_target + } + + pub fn take_panic_error(&self) -> Result<(), PanicError> { + match self.panic_error.take() { + Some(err) => Err(err), + None => Ok(()), + } + } + + pub fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub fn set_exit_code(&self, code: i32) { + self.exit.set(Some(code)) + } + + pub fn exit_code(&self) -> Option { + self.exit.get() + } + + pub fn clear_exit(&self) { + self.exit.set(None); + } + + pub fn should_buffer(&self) -> bool { + let handler = self.event_handler.take(); + let should_buffer = handler.is_none(); + self.event_handler.set(handler); + should_buffer + } +} + +/// Misc. functions +impl EventLoopRunner { + pub fn catch_unwind(&self, f: impl FnOnce() -> R) -> Option { + let panic_error = self.panic_error.take(); + if panic_error.is_none() { + let result = panic::catch_unwind(panic::AssertUnwindSafe(f)); + + // Check to see if the panic error was set in a re-entrant call to catch_unwind inside + // of `f`. If it was, that error takes priority. If it wasn't, check if our call to + // catch_unwind caught any panics and set panic_error appropriately. + match self.panic_error.take() { + None => match result { + Ok(r) => Some(r), + Err(e) => { + self.panic_error.set(Some(e)); + None + } + }, + Some(e) => { + self.panic_error.set(Some(e)); + None + } + } + } else { + self.panic_error.set(panic_error); + None + } + } +} + +/// Event dispatch functions. +impl EventLoopRunner { + pub(crate) fn prepare_wait(&self) { + self.move_state_to(RunnerState::Idle); + } + + pub(crate) fn wakeup(&self) { + self.move_state_to(RunnerState::HandlingMainEvents); + } + + pub(crate) fn send_event(&self, event: Event) { + if let Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } = event + { + self.call_event_handler(event); + // As a rule, to ensure that `pump_events` can't block an external event loop + // for too long, we always guarantee that `pump_events` will return control to + // the external loop asap after a `RedrawRequested` event is dispatched. + self.interrupt_msg_dispatch.set(true); + } else if self.should_buffer() { + // If the runner is already borrowed, we're in the middle of an event loop invocation. + // Add the event to a buffer to be processed later. + self.event_buffer + .borrow_mut() + .push_back(BufferedEvent::from_event(event)) + } else { + self.call_event_handler(event); + self.dispatch_buffered_events(); + } + } + + pub(crate) fn loop_destroyed(&self) { + self.move_state_to(RunnerState::Destroyed); + } + + fn call_event_handler(&self, event: Event) { + self.catch_unwind(|| { + let mut event_handler = self.event_handler.take().expect( + "either event handler is re-entrant (likely), or no event handler is registered \ + (very unlikely)", + ); + + event_handler(event); + + assert!(self.event_handler.replace(Some(event_handler)).is_none()); + }); + } + + fn dispatch_buffered_events(&self) { + loop { + // We do this instead of using a `while let` loop because if we use a `while let` + // loop the reference returned `borrow_mut()` doesn't get dropped until the end + // of the loop's body and attempts to add events to the event buffer while in + // `process_event` will fail. + let buffered_event_opt = self.event_buffer.borrow_mut().pop_front(); + match buffered_event_opt { + Some(e) => e.dispatch_event(|e| self.call_event_handler(e)), + None => break, + } + } + } + + /// Dispatch control flow events (`NewEvents`, `AboutToWait`, and + /// `LoopExiting`) as necessary to bring the internal `RunnerState` to the + /// new runner state. + /// + /// The state transitions are defined as follows: + /// + /// ```text + /// Uninitialized + /// | + /// V + /// Idle + /// ^ | + /// | V + /// HandlingMainEvents + /// | + /// V + /// Destroyed + /// ``` + /// + /// Attempting to transition back to `Uninitialized` will result in a panic. Attempting to + /// transition *from* `Destroyed` will also result in a panic. Transitioning to the current + /// state is a no-op. Even if the `new_runner_state` isn't the immediate next state in the + /// runner state machine (e.g. `self.runner_state == HandlingMainEvents` and + /// `new_runner_state == Idle`), the intermediate state transitions will still be executed. + fn move_state_to(&self, new_runner_state: RunnerState) { + use RunnerState::{Destroyed, HandlingMainEvents, Idle, Uninitialized}; + + match ( + self.runner_state.replace(new_runner_state), + new_runner_state, + ) { + (Uninitialized, Uninitialized) + | (Idle, Idle) + | (HandlingMainEvents, HandlingMainEvents) + | (Destroyed, Destroyed) => (), + + // State transitions that initialize the event loop. + (Uninitialized, HandlingMainEvents) => { + self.call_new_events(true); + } + (Uninitialized, Idle) => { + self.call_new_events(true); + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); + } + (Uninitialized, Destroyed) => { + self.call_new_events(true); + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); + self.call_event_handler(Event::LoopExiting); + } + (_, Uninitialized) => panic!("cannot move state to Uninitialized"), + + // State transitions that start the event handling process. + (Idle, HandlingMainEvents) => { + self.call_new_events(false); + } + (Idle, Destroyed) => { + self.call_event_handler(Event::LoopExiting); + } + + (HandlingMainEvents, Idle) => { + // This is always the last event we dispatch before waiting for new events + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); + } + (HandlingMainEvents, Destroyed) => { + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); + self.call_event_handler(Event::LoopExiting); + } + + (Destroyed, _) => panic!("cannot move state from Destroyed"), + } + } + + fn call_new_events(&self, init: bool) { + let start_cause = match (init, self.control_flow(), self.exit.get()) { + (true, ..) => StartCause::Init, + (false, ControlFlow::Poll, None) => StartCause::Poll, + (false, _, Some(_)) | (false, ControlFlow::Wait, None) => { + StartCause::WaitCancelled { + requested_resume: None, + start: self.last_events_cleared.get(), + } + } + (false, ControlFlow::WaitUntil(requested_resume), None) => { + if Instant::now() < requested_resume { + StartCause::WaitCancelled { + requested_resume: Some(requested_resume), + start: self.last_events_cleared.get(), + } + } else { + StartCause::ResumeTimeReached { + requested_resume, + start: self.last_events_cleared.get(), + } + } + } + }; + self.call_event_handler(Event::NewEvents(start_cause)); + // NB: For consistency all platforms must emit a 'resumed' event even though Windows + // applications don't themselves have a formal suspend/resume lifecycle. + if init { + self.call_event_handler(Event::Resumed); + } + self.dispatch_buffered_events(); + } +} + +impl BufferedEvent { + pub fn from_event(event: Event) -> BufferedEvent { + match event { + Event::WindowEvent { + event: + WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer, + }, + window_id, + } => BufferedEvent::ScaleFactorChanged( + window_id, + scale_factor, + *inner_size_writer + .new_inner_size + .upgrade() + .unwrap() + .lock() + .unwrap(), + ), + event => BufferedEvent::Event(event), + } + } + + pub fn dispatch_event(self, dispatch: impl FnOnce(Event)) { + match self { + Self::Event(event) => dispatch(event), + Self::ScaleFactorChanged(window_id, scale_factor, new_inner_size) => { + let user_new_innner_size = Arc::new(Mutex::new(new_inner_size)); + dispatch(Event::WindowEvent { + window_id, + event: WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade( + &user_new_innner_size, + )), + }, + }); + let inner_size = *user_new_innner_size.lock().unwrap(); + + drop(user_new_innner_size); + + if inner_size != new_inner_size { + let window_flags = unsafe { + let userdata = get_window_long(window_id.0.into(), GWL_USERDATA) + as *mut WindowData; + (*userdata).window_state_lock().window_flags + }; + + window_flags.set_size((window_id.0).0, inner_size); + } + } + } + } +} diff --git a/rio-window/src/platform_impl/windows/icon.rs b/rio-window/src/platform_impl/windows/icon.rs new file mode 100644 index 00000000..c7ae6935 --- /dev/null +++ b/rio-window/src/platform_impl/windows/icon.rs @@ -0,0 +1,256 @@ +use std::ffi::c_void; +use std::path::Path; +use std::sync::Arc; +use std::{fmt, io, mem}; + +use cursor_icon::CursorIcon; +use windows_sys::core::PCWSTR; +use windows_sys::Win32::Foundation::HWND; +use windows_sys::Win32::Graphics::Gdi::{ + CreateBitmap, CreateCompatibleBitmap, DeleteObject, GetDC, ReleaseDC, SetBitmapBits, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + CreateIcon, CreateIconIndirect, DestroyCursor, DestroyIcon, LoadImageW, SendMessageW, + HCURSOR, HICON, ICONINFO, ICON_BIG, ICON_SMALL, IMAGE_ICON, LR_DEFAULTSIZE, + LR_LOADFROMFILE, WM_SETICON, +}; + +use crate::cursor::CursorImage; +use crate::dpi::PhysicalSize; +use crate::icon::*; + +use super::util; + +impl Pixel { + fn convert_to_bgra(&mut self) { + mem::swap(&mut self.r, &mut self.b); + } +} + +impl RgbaIcon { + fn into_windows_icon(self) -> Result { + let rgba = self.rgba; + let pixel_count = rgba.len() / PIXEL_SIZE; + let mut and_mask = Vec::with_capacity(pixel_count); + let pixels = unsafe { + std::slice::from_raw_parts_mut(rgba.as_ptr() as *mut Pixel, pixel_count) + }; + for pixel in pixels { + and_mask.push(pixel.a.wrapping_sub(u8::MAX)); // invert alpha channel + pixel.convert_to_bgra(); + } + assert_eq!(and_mask.len(), pixel_count); + let handle = unsafe { + CreateIcon( + 0, + self.width as i32, + self.height as i32, + 1, + (PIXEL_SIZE * 8) as u8, + and_mask.as_ptr(), + rgba.as_ptr(), + ) + }; + if handle != 0 { + Ok(WinIcon::from_handle(handle)) + } else { + Err(BadIcon::OsError(io::Error::last_os_error())) + } + } +} + +#[derive(Debug)] +pub enum IconType { + Small = ICON_SMALL as isize, + Big = ICON_BIG as isize, +} + +#[derive(Debug)] +struct RaiiIcon { + handle: HICON, +} + +#[derive(Clone)] +pub struct WinIcon { + inner: Arc, +} + +unsafe impl Send for WinIcon {} + +impl WinIcon { + pub fn as_raw_handle(&self) -> HICON { + self.inner.handle + } + + pub fn from_path>( + path: P, + size: Option>, + ) -> Result { + // width / height of 0 along with LR_DEFAULTSIZE tells windows to load the default icon size + let (width, height) = size.map(Into::into).unwrap_or((0, 0)); + + let wide_path = util::encode_wide(path.as_ref()); + + let handle = unsafe { + LoadImageW( + 0, + wide_path.as_ptr(), + IMAGE_ICON, + width, + height, + LR_DEFAULTSIZE | LR_LOADFROMFILE, + ) + }; + if handle != 0 { + Ok(WinIcon::from_handle(handle as HICON)) + } else { + Err(BadIcon::OsError(io::Error::last_os_error())) + } + } + + pub fn from_resource( + resource_id: u16, + size: Option>, + ) -> Result { + // width / height of 0 along with LR_DEFAULTSIZE tells windows to load the default icon size + let (width, height) = size.map(Into::into).unwrap_or((0, 0)); + let handle = unsafe { + LoadImageW( + util::get_instance_handle(), + resource_id as PCWSTR, + IMAGE_ICON, + width, + height, + LR_DEFAULTSIZE, + ) + }; + if handle != 0 { + Ok(WinIcon::from_handle(handle as HICON)) + } else { + Err(BadIcon::OsError(io::Error::last_os_error())) + } + } + + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + let rgba_icon = RgbaIcon::from_rgba(rgba, width, height)?; + rgba_icon.into_windows_icon() + } + + pub fn set_for_window(&self, hwnd: HWND, icon_type: IconType) { + unsafe { + SendMessageW(hwnd, WM_SETICON, icon_type as usize, self.as_raw_handle()); + } + } + + fn from_handle(handle: HICON) -> Self { + Self { + inner: Arc::new(RaiiIcon { handle }), + } + } +} + +impl Drop for RaiiIcon { + fn drop(&mut self) { + unsafe { DestroyIcon(self.handle) }; + } +} + +impl fmt::Debug for WinIcon { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + (*self.inner).fmt(formatter) + } +} + +pub fn unset_for_window(hwnd: HWND, icon_type: IconType) { + unsafe { + SendMessageW(hwnd, WM_SETICON, icon_type as usize, 0); + } +} + +#[derive(Debug, Clone)] +pub enum SelectedCursor { + Named(CursorIcon), + Custom(Arc), +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Named(Default::default()) + } +} + +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub enum WinCursor { + Cursor(Arc), + Failed, +} + +impl WinCursor { + pub(crate) fn new(image: &CursorImage) -> Result { + let mut bgra = image.rgba.clone(); + bgra.chunks_exact_mut(4).for_each(|chunk| chunk.swap(0, 2)); + + let w = image.width as i32; + let h = image.height as i32; + + unsafe { + let hdc_screen = GetDC(0); + if hdc_screen == 0 { + return Err(io::Error::last_os_error()); + } + let hbm_color = CreateCompatibleBitmap(hdc_screen, w, h); + ReleaseDC(0, hdc_screen); + if hbm_color == 0 { + return Err(io::Error::last_os_error()); + } + if SetBitmapBits(hbm_color, bgra.len() as u32, bgra.as_ptr() as *const c_void) + == 0 + { + DeleteObject(hbm_color); + return Err(io::Error::last_os_error()); + }; + + // Mask created according to https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createbitmap#parameters + let mask_bits: Vec = vec![0xff; ((((w + 15) >> 4) << 1) * h) as usize]; + let hbm_mask = CreateBitmap(w, h, 1, 1, mask_bits.as_ptr() as *const _); + if hbm_mask == 0 { + DeleteObject(hbm_color); + return Err(io::Error::last_os_error()); + } + + let icon_info = ICONINFO { + fIcon: 0, + xHotspot: image.hotspot_x as u32, + yHotspot: image.hotspot_y as u32, + hbmMask: hbm_mask, + hbmColor: hbm_color, + }; + + let handle = CreateIconIndirect(&icon_info as *const _); + DeleteObject(hbm_color); + DeleteObject(hbm_mask); + if handle == 0 { + return Err(io::Error::last_os_error()); + } + + Ok(Self::Cursor(Arc::new(RaiiCursor { handle }))) + } + } +} + +#[derive(Debug, Hash, Eq, PartialEq)] +pub struct RaiiCursor { + handle: HCURSOR, +} + +impl Drop for RaiiCursor { + fn drop(&mut self) { + unsafe { DestroyCursor(self.handle) }; + } +} + +impl RaiiCursor { + pub fn as_raw_handle(&self) -> HICON { + self.handle + } +} diff --git a/rio-window/src/platform_impl/windows/ime.rs b/rio-window/src/platform_impl/windows/ime.rs new file mode 100644 index 00000000..9bf56568 --- /dev/null +++ b/rio-window/src/platform_impl/windows/ime.rs @@ -0,0 +1,170 @@ +use std::ffi::{c_void, OsString}; +use std::os::windows::prelude::OsStringExt; +use std::ptr::null_mut; + +use windows_sys::Win32::Foundation::{POINT, RECT}; +use windows_sys::Win32::Globalization::HIMC; +use windows_sys::Win32::UI::Input::Ime::{ + ImmAssociateContextEx, ImmGetCompositionStringW, ImmGetContext, ImmReleaseContext, + ImmSetCandidateWindow, ImmSetCompositionWindow, ATTR_TARGET_CONVERTED, + ATTR_TARGET_NOTCONVERTED, CANDIDATEFORM, CFS_EXCLUDE, CFS_POINT, COMPOSITIONFORM, + GCS_COMPATTR, GCS_COMPSTR, GCS_CURSORPOS, GCS_RESULTSTR, IACE_CHILDREN, IACE_DEFAULT, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_IMMENABLED}; + +use crate::dpi::{Position, Size}; +use crate::platform::windows::HWND; + +pub struct ImeContext { + hwnd: HWND, + himc: HIMC, +} + +impl ImeContext { + pub unsafe fn current(hwnd: HWND) -> Self { + let himc = unsafe { ImmGetContext(hwnd) }; + ImeContext { hwnd, himc } + } + + pub unsafe fn get_composing_text_and_cursor( + &self, + ) -> Option<(String, Option, Option)> { + let text = unsafe { self.get_composition_string(GCS_COMPSTR) }?; + let attrs = + unsafe { self.get_composition_data(GCS_COMPATTR) }.unwrap_or_default(); + + let mut first = None; + let mut last = None; + let mut boundary_before_char = 0; + + for (attr, chr) in attrs.into_iter().zip(text.chars()) { + let char_is_targeted = attr as u32 == ATTR_TARGET_CONVERTED + || attr as u32 == ATTR_TARGET_NOTCONVERTED; + + if first.is_none() && char_is_targeted { + first = Some(boundary_before_char); + } else if first.is_some() && last.is_none() && !char_is_targeted { + last = Some(boundary_before_char); + } + + boundary_before_char += chr.len_utf8(); + } + + if first.is_some() && last.is_none() { + last = Some(text.len()); + } else if first.is_none() { + // IME haven't split words and select any clause yet, so trying to retrieve normal + // cursor. + let cursor = unsafe { self.get_composition_cursor(&text) }; + first = cursor; + last = cursor; + } + + Some((text, first, last)) + } + + pub unsafe fn get_composed_text(&self) -> Option { + unsafe { self.get_composition_string(GCS_RESULTSTR) } + } + + unsafe fn get_composition_cursor(&self, text: &str) -> Option { + let cursor = + unsafe { ImmGetCompositionStringW(self.himc, GCS_CURSORPOS, null_mut(), 0) }; + (cursor >= 0).then(|| text.chars().take(cursor as _).map(|c| c.len_utf8()).sum()) + } + + unsafe fn get_composition_string(&self, gcs_mode: u32) -> Option { + let data = unsafe { self.get_composition_data(gcs_mode) }?; + let (prefix, shorts, suffix) = unsafe { data.align_to::() }; + if prefix.is_empty() && suffix.is_empty() { + OsString::from_wide(shorts).into_string().ok() + } else { + None + } + } + + unsafe fn get_composition_data(&self, gcs_mode: u32) -> Option> { + let size = + match unsafe { ImmGetCompositionStringW(self.himc, gcs_mode, null_mut(), 0) } + { + 0 => return Some(Vec::new()), + size if size < 0 => return None, + size => size, + }; + + let mut buf = Vec::::with_capacity(size as _); + let size = unsafe { + ImmGetCompositionStringW( + self.himc, + gcs_mode, + buf.as_mut_ptr() as *mut c_void, + size as _, + ) + }; + + if size < 0 { + None + } else { + unsafe { buf.set_len(size as _) }; + Some(buf) + } + } + + pub unsafe fn set_ime_cursor_area( + &self, + spot: Position, + size: Size, + scale_factor: f64, + ) { + if !unsafe { ImeContext::system_has_ime() } { + return; + } + + let (x, y) = spot.to_physical::(scale_factor).into(); + let (width, height): (i32, i32) = size.to_physical::(scale_factor).into(); + let rc_area = RECT { + left: x, + top: y, + right: x + width, + bottom: y + height, + }; + let candidate_form = CANDIDATEFORM { + dwIndex: 0, + dwStyle: CFS_EXCLUDE, + ptCurrentPos: POINT { x, y }, + rcArea: rc_area, + }; + let composition_form = COMPOSITIONFORM { + dwStyle: CFS_POINT, + ptCurrentPos: POINT { x, y: y + height }, + rcArea: rc_area, + }; + + unsafe { + ImmSetCompositionWindow(self.himc, &composition_form); + ImmSetCandidateWindow(self.himc, &candidate_form); + } + } + + pub unsafe fn set_ime_allowed(hwnd: HWND, allowed: bool) { + if !unsafe { ImeContext::system_has_ime() } { + return; + } + + if allowed { + unsafe { ImmAssociateContextEx(hwnd, 0, IACE_DEFAULT) }; + } else { + unsafe { ImmAssociateContextEx(hwnd, 0, IACE_CHILDREN) }; + } + } + + unsafe fn system_has_ime() -> bool { + unsafe { GetSystemMetrics(SM_IMMENABLED) != 0 } + } +} + +impl Drop for ImeContext { + fn drop(&mut self) { + unsafe { ImmReleaseContext(self.hwnd, self.himc) }; + } +} diff --git a/rio-window/src/platform_impl/windows/keyboard.rs b/rio-window/src/platform_impl/windows/keyboard.rs new file mode 100644 index 00000000..b6433328 --- /dev/null +++ b/rio-window/src/platform_impl/windows/keyboard.rs @@ -0,0 +1,1319 @@ +use std::char; +use std::ffi::OsString; +use std::mem::MaybeUninit; +use std::os::windows::ffi::OsStringExt; +use std::sync::atomic::AtomicU32; +use std::sync::atomic::Ordering::Relaxed; +use std::sync::{Mutex, MutexGuard}; + +use windows_sys::Win32::Foundation::{HWND, LPARAM, WPARAM}; +use windows_sys::Win32::System::SystemServices::LANG_KOREAN; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + GetAsyncKeyState, GetKeyState, GetKeyboardLayout, GetKeyboardState, MapVirtualKeyExW, + MAPVK_VK_TO_VSC_EX, MAPVK_VSC_TO_VK_EX, VIRTUAL_KEY, VK_ABNT_C2, VK_ADD, VK_CAPITAL, + VK_CLEAR, VK_CONTROL, VK_DECIMAL, VK_DELETE, VK_DIVIDE, VK_DOWN, VK_END, VK_F4, + VK_HOME, VK_INSERT, VK_LCONTROL, VK_LEFT, VK_LMENU, VK_LSHIFT, VK_LWIN, VK_MENU, + VK_MULTIPLY, VK_NEXT, VK_NUMLOCK, VK_NUMPAD0, VK_NUMPAD1, VK_NUMPAD2, VK_NUMPAD3, + VK_NUMPAD4, VK_NUMPAD5, VK_NUMPAD6, VK_NUMPAD7, VK_NUMPAD8, VK_NUMPAD9, VK_PRIOR, + VK_RCONTROL, VK_RETURN, VK_RIGHT, VK_RMENU, VK_RSHIFT, VK_RWIN, VK_SCROLL, VK_SHIFT, + VK_SUBTRACT, VK_UP, +}; +use windows_sys::Win32::UI::TextServices::HKL; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + PeekMessageW, MSG, PM_NOREMOVE, WM_CHAR, WM_DEADCHAR, WM_KEYDOWN, WM_KEYFIRST, + WM_KEYLAST, WM_KEYUP, WM_KILLFOCUS, WM_SETFOCUS, WM_SYSCHAR, WM_SYSDEADCHAR, + WM_SYSKEYDOWN, WM_SYSKEYUP, +}; + +use smol_str::SmolStr; +use tracing::{trace, warn}; +use unicode_segmentation::UnicodeSegmentation; + +use crate::event::{ElementState, KeyEvent}; +use crate::keyboard::{ + Key, KeyCode, KeyLocation, NamedKey, NativeKey, NativeKeyCode, PhysicalKey, +}; +use crate::platform_impl::platform::event_loop::ProcResult; +use crate::platform_impl::platform::keyboard_layout::{ + Layout, LayoutCache, WindowsModifiers, LAYOUT_CACHE, +}; +use crate::platform_impl::platform::{loword, primarylangid, KeyEventExtra}; + +pub type ExScancode = u16; + +pub struct MessageAsKeyEvent { + pub event: KeyEvent, + pub is_synthetic: bool, +} + +/// Stores information required to make `KeyEvent`s. +/// +/// A single Winit `KeyEvent` contains information which the Windows API passes to the application +/// in multiple window messages. In other words: a Winit `KeyEvent` cannot be built from a single +/// window message. Therefore, this type keeps track of certain information from previous events so +/// that a `KeyEvent` can be constructed when the last event related to a keypress is received. +/// +/// `PeekMessage` is sometimes used to determine whether the next window message still belongs to +/// the current keypress. If it doesn't and the current state represents a key event waiting to be +/// dispatched, then said event is considered complete and is dispatched. +/// +/// The sequence of window messages for a key press event is the following: +/// - Exactly one WM_KEYDOWN / WM_SYSKEYDOWN +/// - Zero or one WM_DEADCHAR / WM_SYSDEADCHAR +/// - Zero or more WM_CHAR / WM_SYSCHAR. These messages each come with a UTF-16 code unit which when +/// put together in the sequence they arrived in, forms the text which is the result of pressing +/// the key. +/// +/// Key release messages are a bit different due to the fact that they don't contribute to +/// text input. The "sequence" only consists of one WM_KEYUP / WM_SYSKEYUP event. +pub struct KeyEventBuilder { + event_info: Mutex>, + pending: PendingEventQueue, +} +impl Default for KeyEventBuilder { + fn default() -> Self { + KeyEventBuilder { + event_info: Mutex::new(None), + pending: Default::default(), + } + } +} +impl KeyEventBuilder { + /// Call this function for every window message. + /// Returns Some() if this window message completes a KeyEvent. + /// Returns None otherwise. + pub(crate) fn process_message( + &self, + hwnd: HWND, + msg_kind: u32, + wparam: WPARAM, + lparam: LPARAM, + result: &mut ProcResult, + ) -> Vec { + enum MatchResult { + Nothing, + TokenToRemove(PendingMessageToken), + MessagesToDispatch(Vec), + } + + let mut matcher = || -> MatchResult { + match msg_kind { + WM_SETFOCUS => { + // synthesize keydown events + let kbd_state = get_async_kbd_state(); + let key_events = + Self::synthesize_kbd_state(ElementState::Pressed, &kbd_state); + MatchResult::MessagesToDispatch( + self.pending.complete_multi(key_events), + ) + } + WM_KILLFOCUS => { + // sythesize keyup events + let kbd_state = get_kbd_state(); + let key_events = + Self::synthesize_kbd_state(ElementState::Released, &kbd_state); + MatchResult::MessagesToDispatch( + self.pending.complete_multi(key_events), + ) + } + WM_KEYDOWN | WM_SYSKEYDOWN => { + if msg_kind == WM_SYSKEYDOWN && wparam as VIRTUAL_KEY == VK_F4 { + // Don't dispatch Alt+F4 to the application. + // This is handled in `event_loop.rs` + return MatchResult::Nothing; + } + let pending_token = self.pending.add_pending(); + *result = ProcResult::Value(0); + + let next_msg = next_kbd_msg(hwnd); + + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + let mut finished_event_info = + Some(PartialKeyEventInfo::from_message( + wparam, + lparam, + ElementState::Pressed, + &mut layouts, + )); + let mut event_info = self.event_info.lock().unwrap(); + *event_info = None; + if let Some(next_msg) = next_msg { + let next_msg_kind = next_msg.message; + let next_belongs_to_this = !matches!( + next_msg_kind, + WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP + ); + if next_belongs_to_this { + // The next OS event belongs to this Winit event, so let's just + // store the partial information, and add to it in the upcoming events + *event_info = finished_event_info.take(); + } else { + let (_, layout) = layouts.get_current_layout(); + let is_fake = { + let curr_event = finished_event_info.as_ref().unwrap(); + is_current_fake(curr_event, next_msg, layout) + }; + if is_fake { + finished_event_info = None; + } + } + } + if let Some(event_info) = finished_event_info { + let ev = event_info.finalize(); + return MatchResult::MessagesToDispatch( + self.pending.complete_pending( + pending_token, + MessageAsKeyEvent { + event: ev, + is_synthetic: false, + }, + ), + ); + } + MatchResult::TokenToRemove(pending_token) + } + WM_DEADCHAR | WM_SYSDEADCHAR => { + let pending_token = self.pending.add_pending(); + *result = ProcResult::Value(0); + // At this point, we know that there isn't going to be any more events related + // to this key press + let event_info = self.event_info.lock().unwrap().take().unwrap(); + let ev = event_info.finalize(); + MatchResult::MessagesToDispatch(self.pending.complete_pending( + pending_token, + MessageAsKeyEvent { + event: ev, + is_synthetic: false, + }, + )) + } + WM_CHAR | WM_SYSCHAR => { + let mut event_info = self.event_info.lock().unwrap(); + if event_info.is_none() { + trace!( + "Received a CHAR message but no `event_info` was available. The \ + message is probably IME, returning." + ); + return MatchResult::Nothing; + } + let pending_token = self.pending.add_pending(); + *result = ProcResult::Value(0); + let is_high_surrogate = (0xd800..=0xdbff).contains(&wparam); + let is_low_surrogate = (0xdc00..=0xdfff).contains(&wparam); + + let is_utf16 = is_high_surrogate || is_low_surrogate; + + if is_utf16 { + if let Some(ev_info) = event_info.as_mut() { + ev_info.utf16parts.push(wparam as u16); + } + } else { + // In this case, wparam holds a UTF-32 character. + // Let's encode it as UTF-16 and append it to the end of `utf16parts` + let utf16parts = match event_info.as_mut() { + Some(ev_info) => &mut ev_info.utf16parts, + None => { + warn!("The event_info was None when it was expected to be some"); + return MatchResult::TokenToRemove(pending_token); + } + }; + let start_offset = utf16parts.len(); + let new_size = utf16parts.len() + 2; + utf16parts.resize(new_size, 0); + if let Some(ch) = char::from_u32(wparam as u32) { + let encode_len = + ch.encode_utf16(&mut utf16parts[start_offset..]).len(); + let new_size = start_offset + encode_len; + utf16parts.resize(new_size, 0); + } + } + // It's important that we unlock the mutex, and create the pending event token + // before calling `next_msg` + std::mem::drop(event_info); + let next_msg = next_kbd_msg(hwnd); + let more_char_coming = next_msg + .map(|m| matches!(m.message, WM_CHAR | WM_SYSCHAR)) + .unwrap_or(false); + if more_char_coming { + // No need to produce an event just yet, because there are still more + // characters that need to appended to this keyobard + // event + MatchResult::TokenToRemove(pending_token) + } else { + let mut event_info = self.event_info.lock().unwrap(); + let mut event_info = match event_info.take() { + Some(ev_info) => ev_info, + None => { + warn!("The event_info was None when it was expected to be some"); + return MatchResult::TokenToRemove(pending_token); + } + }; + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + // It's okay to call `ToUnicode` here, because at this point the dead key + // is already consumed by the character. + let kbd_state = get_kbd_state(); + let mod_state = WindowsModifiers::active_modifiers(&kbd_state); + + let (_, layout) = layouts.get_current_layout(); + let ctrl_on = if layout.has_alt_graph { + let alt_on = mod_state.contains(WindowsModifiers::ALT); + !alt_on && mod_state.contains(WindowsModifiers::CONTROL) + } else { + mod_state.contains(WindowsModifiers::CONTROL) + }; + + // If Ctrl is not pressed, just use the text with all + // modifiers because that already consumed the dead key. Otherwise, + // we would interpret the character incorrectly, missing the dead key. + if !ctrl_on { + event_info.text = + PartialText::System(event_info.utf16parts.clone()); + } else { + let mod_no_ctrl = mod_state.remove_only_ctrl(); + let num_lock_on = kbd_state[VK_NUMLOCK as usize] & 1 != 0; + let vkey = event_info.vkey; + let physical_key = &event_info.physical_key; + let key = layout.get_key( + mod_no_ctrl, + num_lock_on, + vkey, + physical_key, + ); + event_info.text = + PartialText::Text(key.to_text().map(SmolStr::new)); + } + let ev = event_info.finalize(); + MatchResult::MessagesToDispatch(self.pending.complete_pending( + pending_token, + MessageAsKeyEvent { + event: ev, + is_synthetic: false, + }, + )) + } + } + WM_KEYUP | WM_SYSKEYUP => { + let pending_token = self.pending.add_pending(); + *result = ProcResult::Value(0); + + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + let event_info = PartialKeyEventInfo::from_message( + wparam, + lparam, + ElementState::Released, + &mut layouts, + ); + // We MUST release the layout lock before calling `next_kbd_msg`, otherwise it + // may deadlock + drop(layouts); + // It's important that we create the pending token before reading the next + // message. + let next_msg = next_kbd_msg(hwnd); + let mut valid_event_info = Some(event_info); + if let Some(next_msg) = next_msg { + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + let (_, layout) = layouts.get_current_layout(); + let is_fake = { + let event_info = valid_event_info.as_ref().unwrap(); + is_current_fake(event_info, next_msg, layout) + }; + if is_fake { + valid_event_info = None; + } + } + if let Some(event_info) = valid_event_info { + let event = event_info.finalize(); + return MatchResult::MessagesToDispatch( + self.pending.complete_pending( + pending_token, + MessageAsKeyEvent { + event, + is_synthetic: false, + }, + ), + ); + } + MatchResult::TokenToRemove(pending_token) + } + _ => MatchResult::Nothing, + } + }; + let matcher_result = matcher(); + match matcher_result { + MatchResult::TokenToRemove(t) => self.pending.remove_pending(t), + MatchResult::MessagesToDispatch(m) => m, + MatchResult::Nothing => Vec::new(), + } + } + + // Allowing nominimal_bool lint because the `is_key_pressed` macro triggers this warning + // and I don't know of another way to resolve it and also keeping the macro + #[allow(clippy::nonminimal_bool)] + fn synthesize_kbd_state( + key_state: ElementState, + kbd_state: &[u8; 256], + ) -> Vec { + let mut key_events = Vec::new(); + + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + let (locale_id, _) = layouts.get_current_layout(); + + macro_rules! is_key_pressed { + ($vk:expr) => { + kbd_state[$vk as usize] & 0x80 != 0 + }; + } + + // Is caps-lock active? Note that this is different from caps-lock + // being held down. + let caps_lock_on = kbd_state[VK_CAPITAL as usize] & 1 != 0; + let num_lock_on = kbd_state[VK_NUMLOCK as usize] & 1 != 0; + + // We are synthesizing the press event for caps-lock first for the following reasons: + // 1. If caps-lock is *not* held down but *is* active, then we have to synthesize all + // printable keys, respecting the caps-lock state. + // 2. If caps-lock is held down, we could choose to sythesize its keypress after every other + // key, in which case all other keys *must* be sythesized as if the caps-lock state was + // be the opposite of what it currently is. + // -- + // For the sake of simplicity we are choosing to always sythesize + // caps-lock first, and always use the current caps-lock state + // to determine the produced text + if is_key_pressed!(VK_CAPITAL) { + let event = Self::create_synthetic( + VK_CAPITAL, + key_state, + caps_lock_on, + num_lock_on, + locale_id as HKL, + &mut layouts, + ); + if let Some(event) = event { + key_events.push(event); + } + } + let do_non_modifier = |key_events: &mut Vec<_>, layouts: &mut _| { + for vk in 0..256 { + match vk { + VK_CONTROL | VK_LCONTROL | VK_RCONTROL | VK_SHIFT | VK_LSHIFT + | VK_RSHIFT | VK_MENU | VK_LMENU | VK_RMENU | VK_CAPITAL => continue, + _ => (), + } + if !is_key_pressed!(vk) { + continue; + } + let event = Self::create_synthetic( + vk, + key_state, + caps_lock_on, + num_lock_on, + locale_id as HKL, + layouts, + ); + if let Some(event) = event { + key_events.push(event); + } + } + }; + let do_modifier = |key_events: &mut Vec<_>, layouts: &mut _| { + const CLEAR_MODIFIER_VKS: [VIRTUAL_KEY; 6] = [ + VK_LCONTROL, + VK_LSHIFT, + VK_LMENU, + VK_RCONTROL, + VK_RSHIFT, + VK_RMENU, + ]; + for vk in CLEAR_MODIFIER_VKS.iter() { + if is_key_pressed!(*vk) { + let event = Self::create_synthetic( + *vk, + key_state, + caps_lock_on, + num_lock_on, + locale_id as HKL, + layouts, + ); + if let Some(event) = event { + key_events.push(event); + } + } + } + }; + + // Be cheeky and sequence modifier and non-modifier + // key events such that non-modifier keys are not affected + // by modifiers (except for caps-lock) + match key_state { + ElementState::Pressed => { + do_non_modifier(&mut key_events, &mut layouts); + do_modifier(&mut key_events, &mut layouts); + } + ElementState::Released => { + do_modifier(&mut key_events, &mut layouts); + do_non_modifier(&mut key_events, &mut layouts); + } + } + + key_events + } + + fn create_synthetic( + vk: VIRTUAL_KEY, + key_state: ElementState, + caps_lock_on: bool, + num_lock_on: bool, + locale_id: HKL, + layouts: &mut MutexGuard<'_, LayoutCache>, + ) -> Option { + let scancode = + unsafe { MapVirtualKeyExW(vk as u32, MAPVK_VK_TO_VSC_EX, locale_id) }; + if scancode == 0 { + return None; + } + let scancode = scancode as ExScancode; + let physical_key = scancode_to_physicalkey(scancode as u32); + let mods = if caps_lock_on { + WindowsModifiers::CAPS_LOCK + } else { + WindowsModifiers::empty() + }; + let layout = layouts.layouts.get(&(locale_id as u64)).unwrap(); + let logical_key = layout.get_key(mods, num_lock_on, vk, &physical_key); + let key_without_modifiers = + layout.get_key(WindowsModifiers::empty(), false, vk, &physical_key); + let text = if key_state == ElementState::Pressed { + logical_key.to_text().map(SmolStr::new) + } else { + None + }; + let event_info = PartialKeyEventInfo { + vkey: vk, + logical_key: PartialLogicalKey::This(logical_key.clone()), + key_without_modifiers, + key_state, + is_repeat: false, + physical_key, + location: get_location(scancode, locale_id), + utf16parts: Vec::with_capacity(8), + text: PartialText::Text(text.clone()), + }; + + let mut event = event_info.finalize(); + event.logical_key = logical_key; + event.platform_specific.text_with_all_modifiers = text; + Some(MessageAsKeyEvent { + event, + is_synthetic: true, + }) + } +} + +enum PartialText { + // Unicode + System(Vec), + Text(Option), +} + +enum PartialLogicalKey { + /// Use the text provided by the WM_CHAR messages and report that as a `Character` variant. If + /// the text consists of multiple grapheme clusters (user-precieved characters) that means that + /// dead key could not be combined with the second input, and in that case we should fall back + /// to using what would have without a dead-key input. + TextOr(Key), + + /// Use the value directly provided by this variant + This(Key), +} + +struct PartialKeyEventInfo { + vkey: VIRTUAL_KEY, + key_state: ElementState, + is_repeat: bool, + physical_key: PhysicalKey, + location: KeyLocation, + logical_key: PartialLogicalKey, + + key_without_modifiers: Key, + + /// The UTF-16 code units of the text that was produced by the keypress event. + /// This take all modifiers into account. Including CTRL + utf16parts: Vec, + + text: PartialText, +} + +impl PartialKeyEventInfo { + fn from_message( + wparam: WPARAM, + lparam: LPARAM, + state: ElementState, + layouts: &mut MutexGuard<'_, LayoutCache>, + ) -> Self { + const NO_MODS: WindowsModifiers = WindowsModifiers::empty(); + + let (_, layout) = layouts.get_current_layout(); + let lparam_struct = destructure_key_lparam(lparam); + let vkey = wparam as VIRTUAL_KEY; + let scancode = if lparam_struct.scancode == 0 { + // In some cases (often with media keys) the device reports a scancode of 0 but a + // valid virtual key. In these cases we obtain the scancode from the virtual key. + unsafe { + MapVirtualKeyExW(vkey as u32, MAPVK_VK_TO_VSC_EX, layout.hkl as HKL) + as u16 + } + } else { + new_ex_scancode(lparam_struct.scancode, lparam_struct.extended) + }; + let physical_key = scancode_to_physicalkey(scancode as u32); + let location = get_location(scancode, layout.hkl as HKL); + + let kbd_state = get_kbd_state(); + let mods = WindowsModifiers::active_modifiers(&kbd_state); + let mods_without_ctrl = mods.remove_only_ctrl(); + let num_lock_on = kbd_state[VK_NUMLOCK as usize] & 1 != 0; + + // On Windows Ctrl+NumLock = Pause (and apparently Ctrl+Pause -> NumLock). In these cases + // the KeyCode still stores the real key, so in the name of consistency across platforms, we + // circumvent this mapping and force the key values to match the keycode. + // For more on this, read the article by Raymond Chen, titled: + // "Why does Ctrl+ScrollLock cancel dialogs?" + // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 + let code_as_key = if mods.contains(WindowsModifiers::CONTROL) { + match physical_key { + PhysicalKey::Code(KeyCode::NumLock) => { + Some(Key::Named(NamedKey::NumLock)) + } + PhysicalKey::Code(KeyCode::Pause) => Some(Key::Named(NamedKey::Pause)), + _ => None, + } + } else { + None + }; + + let preliminary_logical_key = + layout.get_key(mods_without_ctrl, num_lock_on, vkey, &physical_key); + let key_is_char = matches!(preliminary_logical_key, Key::Character(_)); + let is_pressed = state == ElementState::Pressed; + + let logical_key = if let Some(key) = code_as_key.clone() { + PartialLogicalKey::This(key) + } else if is_pressed && key_is_char && !mods.contains(WindowsModifiers::CONTROL) { + // In some cases we want to use the UNICHAR text for logical_key in order to allow + // dead keys to have an effect on the character reported by `logical_key`. + PartialLogicalKey::TextOr(preliminary_logical_key) + } else { + PartialLogicalKey::This(preliminary_logical_key) + }; + let key_without_modifiers = if let Some(key) = code_as_key { + key + } else { + match layout.get_key(NO_MODS, false, vkey, &physical_key) { + // We convert dead keys into their character. + // The reason for this is that `key_without_modifiers` is designed for key-bindings, + // but the US International layout treats `'` (apostrophe) as a dead key and the + // reguar US layout treats it a character. In order for a single binding + // configuration to work with both layouts, we forward each dead key as a character. + Key::Dead(k) => { + if let Some(ch) = k { + // I'm avoiding the heap allocation. I don't want to talk about it :( + let mut utf8 = [0; 4]; + let s = ch.encode_utf8(&mut utf8); + Key::Character(SmolStr::new(s)) + } else { + Key::Unidentified(NativeKey::Unidentified) + } + } + key => key, + } + }; + + PartialKeyEventInfo { + vkey, + key_state: state, + logical_key, + key_without_modifiers, + is_repeat: lparam_struct.is_repeat, + physical_key, + location, + utf16parts: Vec::with_capacity(8), + text: PartialText::System(Vec::new()), + } + } + + fn finalize(self) -> KeyEvent { + let mut char_with_all_modifiers = None; + if !self.utf16parts.is_empty() { + let os_string = OsString::from_wide(&self.utf16parts); + if let Ok(string) = os_string.into_string() { + char_with_all_modifiers = Some(SmolStr::new(string)); + } + } + + // The text without Ctrl + let mut text = None; + match self.text { + PartialText::System(wide) => { + if !wide.is_empty() { + let os_string = OsString::from_wide(&wide); + if let Ok(string) = os_string.into_string() { + text = Some(SmolStr::new(string)); + } + } + } + PartialText::Text(s) => { + text = s.map(SmolStr::new); + } + } + + let logical_key = match self.logical_key { + PartialLogicalKey::TextOr(fallback) => match text.as_ref() { + Some(s) => { + if s.grapheme_indices(true).count() > 1 { + fallback + } else { + Key::Character(s.clone()) + } + } + None => Key::Unidentified(NativeKey::Windows(self.vkey)), + }, + PartialLogicalKey::This(v) => v, + }; + + KeyEvent { + physical_key: self.physical_key, + logical_key, + text, + location: self.location, + state: self.key_state, + repeat: self.is_repeat, + platform_specific: KeyEventExtra { + text_with_all_modifiers: char_with_all_modifiers, + key_without_modifiers: self.key_without_modifiers, + }, + } + } +} + +#[derive(Debug, Copy, Clone)] +struct KeyLParam { + pub scancode: u8, + pub extended: bool, + + /// This is `previous_state XOR transition_state`. See the lParam for WM_KEYDOWN and WM_KEYUP + /// for further details. + pub is_repeat: bool, +} + +fn destructure_key_lparam(lparam: LPARAM) -> KeyLParam { + let previous_state = (lparam >> 30) & 0x01; + let transition_state = (lparam >> 31) & 0x01; + KeyLParam { + scancode: ((lparam >> 16) & 0xff) as u8, + extended: ((lparam >> 24) & 0x01) != 0, + is_repeat: (previous_state ^ transition_state) != 0, + } +} + +#[inline] +fn new_ex_scancode(scancode: u8, extended: bool) -> ExScancode { + (scancode as u16) | (if extended { 0xe000 } else { 0 }) +} + +#[inline] +fn ex_scancode_from_lparam(lparam: LPARAM) -> ExScancode { + let lparam = destructure_key_lparam(lparam); + new_ex_scancode(lparam.scancode, lparam.extended) +} + +/// Gets the keyboard state as reported by messages that have been removed from the event queue. +/// See also: get_async_kbd_state +fn get_kbd_state() -> [u8; 256] { + unsafe { + let mut kbd_state: MaybeUninit<[u8; 256]> = MaybeUninit::uninit(); + GetKeyboardState(kbd_state.as_mut_ptr() as *mut u8); + kbd_state.assume_init() + } +} + +/// Gets the current keyboard state regardless of whether the corresponding keyboard events have +/// been removed from the event queue. See also: get_kbd_state +#[allow(clippy::uninit_assumed_init)] +fn get_async_kbd_state() -> [u8; 256] { + unsafe { + let mut kbd_state: [u8; 256] = [0; 256]; + for (vk, state) in kbd_state.iter_mut().enumerate() { + let vk = vk as VIRTUAL_KEY; + let async_state = GetAsyncKeyState(vk as i32); + let is_down = (async_state & (1 << 15)) != 0; + *state = if is_down { 0x80 } else { 0 }; + + if matches!(vk, VK_CAPITAL | VK_NUMLOCK | VK_SCROLL) { + // Toggle states aren't reported by `GetAsyncKeyState` + let toggle_state = GetKeyState(vk as i32); + let is_active = (toggle_state & 1) != 0; + *state |= u8::from(is_active); + } + } + kbd_state + } +} + +/// On windows, AltGr == Ctrl + Alt +/// +/// Due to this equivalence, the system generates a fake Ctrl key-press (and key-release) preceding +/// every AltGr key-press (and key-release). We check if the current event is a Ctrl event and if +/// the next event is a right Alt (AltGr) event. If this is the case, the current event must be the +/// fake Ctrl event. +fn is_current_fake( + curr_info: &PartialKeyEventInfo, + next_msg: MSG, + layout: &Layout, +) -> bool { + let curr_is_ctrl = matches!( + curr_info.logical_key, + PartialLogicalKey::This(Key::Named(NamedKey::Control)) + ); + if layout.has_alt_graph { + let next_code = ex_scancode_from_lparam(next_msg.lParam); + let next_is_altgr = next_code == 0xe038; // 0xE038 is right alt + if curr_is_ctrl && next_is_altgr { + return true; + } + } + false +} + +enum PendingMessage { + Incomplete, + Complete(T), +} +struct IdentifiedPendingMessage { + token: PendingMessageToken, + msg: PendingMessage, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PendingMessageToken(u32); + +/// While processing keyboard events, we sometimes need +/// to call `PeekMessageW` (`next_msg`). But `PeekMessageW` +/// can also call the event handler, which means that the new event +/// gets processed before finishing to process the one that came before. +/// +/// This would mean that the application receives events in the wrong order. +/// To avoid this, we keep track whether we are in the middle of processing +/// an event. Such an event is an "incomplete pending event". A +/// "complete pending event" is one that has already finished processing, but +/// hasn't been dispatched to the application because there still are incomplete +/// pending events that came before it. +/// +/// When we finish processing an event, we call `complete_pending`, +/// which returns an empty array if there are incomplete pending events, but +/// if all pending events are complete, then it returns all pending events in +/// the order they were encountered. These can then be dispatched to the application +pub struct PendingEventQueue { + pending: Mutex>>, + next_id: AtomicU32, +} +impl PendingEventQueue { + /// Add a new pending event to the "pending queue" + pub fn add_pending(&self) -> PendingMessageToken { + let token = self.next_token(); + let mut pending = self.pending.lock().unwrap(); + pending.push(IdentifiedPendingMessage { + token, + msg: PendingMessage::Incomplete, + }); + token + } + + /// Returns all finished pending events + /// + /// If the return value is non empty, it's guaranteed to contain `msg` + /// + /// See also: `add_pending` + pub fn complete_pending(&self, token: PendingMessageToken, msg: T) -> Vec { + let mut pending = self.pending.lock().unwrap(); + let mut target_is_first = false; + for (i, pending_msg) in pending.iter_mut().enumerate() { + if pending_msg.token == token { + pending_msg.msg = PendingMessage::Complete(msg); + if i == 0 { + target_is_first = true; + } + break; + } + } + if target_is_first { + // If the message that we just finished was the first one in the pending queue, + // then we can empty the queue, and dispatch all of the messages. + Self::drain_pending(&mut *pending) + } else { + Vec::new() + } + } + + pub fn complete_multi(&self, msgs: Vec) -> Vec { + let mut pending = self.pending.lock().unwrap(); + if pending.is_empty() { + return msgs; + } + pending.reserve(msgs.len()); + for msg in msgs { + pending.push(IdentifiedPendingMessage { + token: self.next_token(), + msg: PendingMessage::Complete(msg), + }); + } + Vec::new() + } + + /// Returns all finished pending events + /// + /// It's safe to call this even if the element isn't in the list anymore + /// + /// See also: `add_pending` + pub fn remove_pending(&self, token: PendingMessageToken) -> Vec { + let mut pending = self.pending.lock().unwrap(); + let mut was_first = false; + if let Some(m) = pending.first() { + if m.token == token { + was_first = true; + } + } + pending.retain(|m| m.token != token); + if was_first { + Self::drain_pending(&mut *pending) + } else { + Vec::new() + } + } + + fn drain_pending(pending: &mut Vec>) -> Vec { + pending + .drain(..) + .map(|m| match m.msg { + PendingMessage::Complete(msg) => msg, + PendingMessage::Incomplete => { + panic!( + "Found an incomplete pending message when collecting messages. This \ + indicates a bug in winit." + ) + }, + }) + .collect() + } + + fn next_token(&self) -> PendingMessageToken { + // It's okay for the u32 to overflow here. Yes, that could mean + // that two different messages have the same token, + // but that would only happen after having about 4 billion + // messages sitting in the pending queue. + // + // In that case, having two identical tokens is the least of your concerns. + let id = self.next_id.fetch_add(1, Relaxed); + PendingMessageToken(id) + } +} +impl Default for PendingEventQueue { + fn default() -> Self { + PendingEventQueue { + pending: Mutex::new(Vec::new()), + next_id: AtomicU32::new(0), + } + } +} + +/// WARNING: Due to using PeekMessage, the event handler +/// function may get called during this function. +/// (Re-entrance to the event handler) +/// +/// This can cause a deadlock if calling this function +/// while having a mutex locked. +/// +/// It can also cause code to get executed in a surprising order. +pub fn next_kbd_msg(hwnd: HWND) -> Option { + unsafe { + let mut next_msg = MaybeUninit::uninit(); + let peek_retval = PeekMessageW( + next_msg.as_mut_ptr(), + hwnd, + WM_KEYFIRST, + WM_KEYLAST, + PM_NOREMOVE, + ); + (peek_retval != 0).then(|| next_msg.assume_init()) + } +} + +fn get_location(scancode: ExScancode, hkl: HKL) -> KeyLocation { + const ABNT_C2: VIRTUAL_KEY = VK_ABNT_C2 as VIRTUAL_KEY; + + let extension = 0xe000; + let extended = (scancode & extension) == extension; + let vkey = unsafe { + MapVirtualKeyExW(scancode as u32, MAPVK_VSC_TO_VK_EX, hkl) as VIRTUAL_KEY + }; + + // Use the native VKEY and the extended flag to cover most cases + // This is taken from the `druid` GUI library, specifically + // druid-shell/src/platform/windows/keyboard.rs + match vkey { + VK_LSHIFT | VK_LCONTROL | VK_LMENU | VK_LWIN => KeyLocation::Left, + VK_RSHIFT | VK_RCONTROL | VK_RMENU | VK_RWIN => KeyLocation::Right, + VK_RETURN if extended => KeyLocation::Numpad, + VK_INSERT | VK_DELETE | VK_END | VK_DOWN | VK_NEXT | VK_LEFT | VK_CLEAR + | VK_RIGHT | VK_HOME | VK_UP | VK_PRIOR => { + if extended { + KeyLocation::Standard + } else { + KeyLocation::Numpad + } + } + VK_NUMPAD0 | VK_NUMPAD1 | VK_NUMPAD2 | VK_NUMPAD3 | VK_NUMPAD4 | VK_NUMPAD5 + | VK_NUMPAD6 | VK_NUMPAD7 | VK_NUMPAD8 | VK_NUMPAD9 | VK_DECIMAL | VK_DIVIDE + | VK_MULTIPLY | VK_SUBTRACT | VK_ADD | ABNT_C2 => KeyLocation::Numpad, + _ => KeyLocation::Standard, + } +} + +pub(crate) fn physicalkey_to_scancode(physical_key: PhysicalKey) -> Option { + // See `scancode_to_physicalkey` for more info + + let hkl = unsafe { GetKeyboardLayout(0) }; + + let primary_lang_id = primarylangid(loword(hkl as u32)); + let is_korean = primary_lang_id as u32 == LANG_KOREAN; + + let code = match physical_key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(code) => { + return match code { + NativeKeyCode::Windows(scancode) => Some(scancode as u32), + _ => None, + }; + } + }; + + match code { + KeyCode::Backquote => Some(0x0029), + KeyCode::Backslash => Some(0x002b), + KeyCode::Backspace => Some(0x000e), + KeyCode::BracketLeft => Some(0x001a), + KeyCode::BracketRight => Some(0x001b), + KeyCode::Comma => Some(0x0033), + KeyCode::Digit0 => Some(0x000b), + KeyCode::Digit1 => Some(0x0002), + KeyCode::Digit2 => Some(0x0003), + KeyCode::Digit3 => Some(0x0004), + KeyCode::Digit4 => Some(0x0005), + KeyCode::Digit5 => Some(0x0006), + KeyCode::Digit6 => Some(0x0007), + KeyCode::Digit7 => Some(0x0008), + KeyCode::Digit8 => Some(0x0009), + KeyCode::Digit9 => Some(0x000a), + KeyCode::Equal => Some(0x000d), + KeyCode::IntlBackslash => Some(0x0056), + KeyCode::IntlRo => Some(0x0073), + KeyCode::IntlYen => Some(0x007d), + KeyCode::KeyA => Some(0x001e), + KeyCode::KeyB => Some(0x0030), + KeyCode::KeyC => Some(0x002e), + KeyCode::KeyD => Some(0x0020), + KeyCode::KeyE => Some(0x0012), + KeyCode::KeyF => Some(0x0021), + KeyCode::KeyG => Some(0x0022), + KeyCode::KeyH => Some(0x0023), + KeyCode::KeyI => Some(0x0017), + KeyCode::KeyJ => Some(0x0024), + KeyCode::KeyK => Some(0x0025), + KeyCode::KeyL => Some(0x0026), + KeyCode::KeyM => Some(0x0032), + KeyCode::KeyN => Some(0x0031), + KeyCode::KeyO => Some(0x0018), + KeyCode::KeyP => Some(0x0019), + KeyCode::KeyQ => Some(0x0010), + KeyCode::KeyR => Some(0x0013), + KeyCode::KeyS => Some(0x001f), + KeyCode::KeyT => Some(0x0014), + KeyCode::KeyU => Some(0x0016), + KeyCode::KeyV => Some(0x002f), + KeyCode::KeyW => Some(0x0011), + KeyCode::KeyX => Some(0x002d), + KeyCode::KeyY => Some(0x0015), + KeyCode::KeyZ => Some(0x002c), + KeyCode::Minus => Some(0x000c), + KeyCode::Period => Some(0x0034), + KeyCode::Quote => Some(0x0028), + KeyCode::Semicolon => Some(0x0027), + KeyCode::Slash => Some(0x0035), + KeyCode::AltLeft => Some(0x0038), + KeyCode::AltRight => Some(0xe038), + KeyCode::CapsLock => Some(0x003a), + KeyCode::ContextMenu => Some(0xe05d), + KeyCode::ControlLeft => Some(0x001d), + KeyCode::ControlRight => Some(0xe01d), + KeyCode::Enter => Some(0x001c), + KeyCode::SuperLeft => Some(0xe05b), + KeyCode::SuperRight => Some(0xe05c), + KeyCode::ShiftLeft => Some(0x002a), + KeyCode::ShiftRight => Some(0x0036), + KeyCode::Space => Some(0x0039), + KeyCode::Tab => Some(0x000f), + KeyCode::Convert => Some(0x0079), + KeyCode::Lang1 => { + if is_korean { + Some(0xe0f2) + } else { + Some(0x0072) + } + } + KeyCode::Lang2 => { + if is_korean { + Some(0xe0f1) + } else { + Some(0x0071) + } + } + KeyCode::KanaMode => Some(0x0070), + KeyCode::NonConvert => Some(0x007b), + KeyCode::Delete => Some(0xe053), + KeyCode::End => Some(0xe04f), + KeyCode::Home => Some(0xe047), + KeyCode::Insert => Some(0xe052), + KeyCode::PageDown => Some(0xe051), + KeyCode::PageUp => Some(0xe049), + KeyCode::ArrowDown => Some(0xe050), + KeyCode::ArrowLeft => Some(0xe04b), + KeyCode::ArrowRight => Some(0xe04d), + KeyCode::ArrowUp => Some(0xe048), + KeyCode::NumLock => Some(0xe045), + KeyCode::Numpad0 => Some(0x0052), + KeyCode::Numpad1 => Some(0x004f), + KeyCode::Numpad2 => Some(0x0050), + KeyCode::Numpad3 => Some(0x0051), + KeyCode::Numpad4 => Some(0x004b), + KeyCode::Numpad5 => Some(0x004c), + KeyCode::Numpad6 => Some(0x004d), + KeyCode::Numpad7 => Some(0x0047), + KeyCode::Numpad8 => Some(0x0048), + KeyCode::Numpad9 => Some(0x0049), + KeyCode::NumpadAdd => Some(0x004e), + KeyCode::NumpadComma => Some(0x007e), + KeyCode::NumpadDecimal => Some(0x0053), + KeyCode::NumpadDivide => Some(0xe035), + KeyCode::NumpadEnter => Some(0xe01c), + KeyCode::NumpadEqual => Some(0x0059), + KeyCode::NumpadMultiply => Some(0x0037), + KeyCode::NumpadSubtract => Some(0x004a), + KeyCode::Escape => Some(0x0001), + KeyCode::F1 => Some(0x003b), + KeyCode::F2 => Some(0x003c), + KeyCode::F3 => Some(0x003d), + KeyCode::F4 => Some(0x003e), + KeyCode::F5 => Some(0x003f), + KeyCode::F6 => Some(0x0040), + KeyCode::F7 => Some(0x0041), + KeyCode::F8 => Some(0x0042), + KeyCode::F9 => Some(0x0043), + KeyCode::F10 => Some(0x0044), + KeyCode::F11 => Some(0x0057), + KeyCode::F12 => Some(0x0058), + KeyCode::F13 => Some(0x0064), + KeyCode::F14 => Some(0x0065), + KeyCode::F15 => Some(0x0066), + KeyCode::F16 => Some(0x0067), + KeyCode::F17 => Some(0x0068), + KeyCode::F18 => Some(0x0069), + KeyCode::F19 => Some(0x006a), + KeyCode::F20 => Some(0x006b), + KeyCode::F21 => Some(0x006c), + KeyCode::F22 => Some(0x006d), + KeyCode::F23 => Some(0x006e), + KeyCode::F24 => Some(0x0076), + KeyCode::PrintScreen => Some(0xe037), + // KeyCode::PrintScreen => Some(0x0054), // Alt + PrintScreen + KeyCode::ScrollLock => Some(0x0046), + KeyCode::Pause => Some(0x0045), + // KeyCode::Pause => Some(0xE046), // Ctrl + Pause + KeyCode::BrowserBack => Some(0xe06a), + KeyCode::BrowserFavorites => Some(0xe066), + KeyCode::BrowserForward => Some(0xe069), + KeyCode::BrowserHome => Some(0xe032), + KeyCode::BrowserRefresh => Some(0xe067), + KeyCode::BrowserSearch => Some(0xe065), + KeyCode::BrowserStop => Some(0xe068), + KeyCode::LaunchApp1 => Some(0xe06b), + KeyCode::LaunchApp2 => Some(0xe021), + KeyCode::LaunchMail => Some(0xe06c), + KeyCode::MediaPlayPause => Some(0xe022), + KeyCode::MediaSelect => Some(0xe06d), + KeyCode::MediaStop => Some(0xe024), + KeyCode::MediaTrackNext => Some(0xe019), + KeyCode::MediaTrackPrevious => Some(0xe010), + KeyCode::Power => Some(0xe05e), + KeyCode::AudioVolumeDown => Some(0xe02e), + KeyCode::AudioVolumeMute => Some(0xe020), + KeyCode::AudioVolumeUp => Some(0xe030), + _ => None, + } +} + +pub(crate) fn scancode_to_physicalkey(scancode: u32) -> PhysicalKey { + // See: https://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html + // and: https://www.w3.org/TR/uievents-code/ + // and: The widget/NativeKeyToDOMCodeName.h file in the firefox source + + PhysicalKey::Code(match scancode { + 0x0029 => KeyCode::Backquote, + 0x002b => KeyCode::Backslash, + 0x000e => KeyCode::Backspace, + 0x001a => KeyCode::BracketLeft, + 0x001b => KeyCode::BracketRight, + 0x0033 => KeyCode::Comma, + 0x000b => KeyCode::Digit0, + 0x0002 => KeyCode::Digit1, + 0x0003 => KeyCode::Digit2, + 0x0004 => KeyCode::Digit3, + 0x0005 => KeyCode::Digit4, + 0x0006 => KeyCode::Digit5, + 0x0007 => KeyCode::Digit6, + 0x0008 => KeyCode::Digit7, + 0x0009 => KeyCode::Digit8, + 0x000a => KeyCode::Digit9, + 0x000d => KeyCode::Equal, + 0x0056 => KeyCode::IntlBackslash, + 0x0073 => KeyCode::IntlRo, + 0x007d => KeyCode::IntlYen, + 0x001e => KeyCode::KeyA, + 0x0030 => KeyCode::KeyB, + 0x002e => KeyCode::KeyC, + 0x0020 => KeyCode::KeyD, + 0x0012 => KeyCode::KeyE, + 0x0021 => KeyCode::KeyF, + 0x0022 => KeyCode::KeyG, + 0x0023 => KeyCode::KeyH, + 0x0017 => KeyCode::KeyI, + 0x0024 => KeyCode::KeyJ, + 0x0025 => KeyCode::KeyK, + 0x0026 => KeyCode::KeyL, + 0x0032 => KeyCode::KeyM, + 0x0031 => KeyCode::KeyN, + 0x0018 => KeyCode::KeyO, + 0x0019 => KeyCode::KeyP, + 0x0010 => KeyCode::KeyQ, + 0x0013 => KeyCode::KeyR, + 0x001f => KeyCode::KeyS, + 0x0014 => KeyCode::KeyT, + 0x0016 => KeyCode::KeyU, + 0x002f => KeyCode::KeyV, + 0x0011 => KeyCode::KeyW, + 0x002d => KeyCode::KeyX, + 0x0015 => KeyCode::KeyY, + 0x002c => KeyCode::KeyZ, + 0x000c => KeyCode::Minus, + 0x0034 => KeyCode::Period, + 0x0028 => KeyCode::Quote, + 0x0027 => KeyCode::Semicolon, + 0x0035 => KeyCode::Slash, + 0x0038 => KeyCode::AltLeft, + 0xe038 => KeyCode::AltRight, + 0x003a => KeyCode::CapsLock, + 0xe05d => KeyCode::ContextMenu, + 0x001d => KeyCode::ControlLeft, + 0xe01d => KeyCode::ControlRight, + 0x001c => KeyCode::Enter, + 0xe05b => KeyCode::SuperLeft, + 0xe05c => KeyCode::SuperRight, + 0x002a => KeyCode::ShiftLeft, + 0x0036 => KeyCode::ShiftRight, + 0x0039 => KeyCode::Space, + 0x000f => KeyCode::Tab, + 0x0079 => KeyCode::Convert, + 0x0072 => KeyCode::Lang1, // for non-Korean layout + 0xe0f2 => KeyCode::Lang1, // for Korean layout + 0x0071 => KeyCode::Lang2, // for non-Korean layout + 0xe0f1 => KeyCode::Lang2, // for Korean layout + 0x0070 => KeyCode::KanaMode, + 0x007b => KeyCode::NonConvert, + 0xe053 => KeyCode::Delete, + 0xe04f => KeyCode::End, + 0xe047 => KeyCode::Home, + 0xe052 => KeyCode::Insert, + 0xe051 => KeyCode::PageDown, + 0xe049 => KeyCode::PageUp, + 0xe050 => KeyCode::ArrowDown, + 0xe04b => KeyCode::ArrowLeft, + 0xe04d => KeyCode::ArrowRight, + 0xe048 => KeyCode::ArrowUp, + 0xe045 => KeyCode::NumLock, + 0x0052 => KeyCode::Numpad0, + 0x004f => KeyCode::Numpad1, + 0x0050 => KeyCode::Numpad2, + 0x0051 => KeyCode::Numpad3, + 0x004b => KeyCode::Numpad4, + 0x004c => KeyCode::Numpad5, + 0x004d => KeyCode::Numpad6, + 0x0047 => KeyCode::Numpad7, + 0x0048 => KeyCode::Numpad8, + 0x0049 => KeyCode::Numpad9, + 0x004e => KeyCode::NumpadAdd, + 0x007e => KeyCode::NumpadComma, + 0x0053 => KeyCode::NumpadDecimal, + 0xe035 => KeyCode::NumpadDivide, + 0xe01c => KeyCode::NumpadEnter, + 0x0059 => KeyCode::NumpadEqual, + 0x0037 => KeyCode::NumpadMultiply, + 0x004a => KeyCode::NumpadSubtract, + 0x0001 => KeyCode::Escape, + 0x003b => KeyCode::F1, + 0x003c => KeyCode::F2, + 0x003d => KeyCode::F3, + 0x003e => KeyCode::F4, + 0x003f => KeyCode::F5, + 0x0040 => KeyCode::F6, + 0x0041 => KeyCode::F7, + 0x0042 => KeyCode::F8, + 0x0043 => KeyCode::F9, + 0x0044 => KeyCode::F10, + 0x0057 => KeyCode::F11, + 0x0058 => KeyCode::F12, + 0x0064 => KeyCode::F13, + 0x0065 => KeyCode::F14, + 0x0066 => KeyCode::F15, + 0x0067 => KeyCode::F16, + 0x0068 => KeyCode::F17, + 0x0069 => KeyCode::F18, + 0x006a => KeyCode::F19, + 0x006b => KeyCode::F20, + 0x006c => KeyCode::F21, + 0x006d => KeyCode::F22, + 0x006e => KeyCode::F23, + 0x0076 => KeyCode::F24, + 0xe037 => KeyCode::PrintScreen, + 0x0054 => KeyCode::PrintScreen, // Alt + PrintScreen + 0x0046 => KeyCode::ScrollLock, + 0x0045 => KeyCode::Pause, + 0xe046 => KeyCode::Pause, // Ctrl + Pause + 0xe06a => KeyCode::BrowserBack, + 0xe066 => KeyCode::BrowserFavorites, + 0xe069 => KeyCode::BrowserForward, + 0xe032 => KeyCode::BrowserHome, + 0xe067 => KeyCode::BrowserRefresh, + 0xe065 => KeyCode::BrowserSearch, + 0xe068 => KeyCode::BrowserStop, + 0xe06b => KeyCode::LaunchApp1, + 0xe021 => KeyCode::LaunchApp2, + 0xe06c => KeyCode::LaunchMail, + 0xe022 => KeyCode::MediaPlayPause, + 0xe06d => KeyCode::MediaSelect, + 0xe024 => KeyCode::MediaStop, + 0xe019 => KeyCode::MediaTrackNext, + 0xe010 => KeyCode::MediaTrackPrevious, + 0xe05e => KeyCode::Power, + 0xe02e => KeyCode::AudioVolumeDown, + 0xe020 => KeyCode::AudioVolumeMute, + 0xe030 => KeyCode::AudioVolumeUp, + _ => return PhysicalKey::Unidentified(NativeKeyCode::Windows(scancode as u16)), + }) +} diff --git a/rio-window/src/platform_impl/windows/keyboard_layout.rs b/rio-window/src/platform_impl/windows/keyboard_layout.rs new file mode 100644 index 00000000..cdf26c38 --- /dev/null +++ b/rio-window/src/platform_impl/windows/keyboard_layout.rs @@ -0,0 +1,1022 @@ +use std::collections::hash_map::Entry; +use std::collections::{HashMap, HashSet}; +use std::ffi::OsString; +use std::os::windows::ffi::OsStringExt; +use std::sync::Mutex; + +use crate::utils::Lazy; +use smol_str::SmolStr; +use windows_sys::Win32::System::SystemServices::{LANG_JAPANESE, LANG_KOREAN}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + GetKeyState, GetKeyboardLayout, MapVirtualKeyExW, ToUnicodeEx, MAPVK_VK_TO_VSC_EX, + VIRTUAL_KEY, VK_ACCEPT, VK_ADD, VK_APPS, VK_ATTN, VK_BACK, VK_BROWSER_BACK, + VK_BROWSER_FAVORITES, VK_BROWSER_FORWARD, VK_BROWSER_HOME, VK_BROWSER_REFRESH, + VK_BROWSER_SEARCH, VK_BROWSER_STOP, VK_CANCEL, VK_CAPITAL, VK_CLEAR, VK_CONTROL, + VK_CONVERT, VK_CRSEL, VK_DECIMAL, VK_DELETE, VK_DIVIDE, VK_DOWN, VK_END, VK_EREOF, + VK_ESCAPE, VK_EXECUTE, VK_EXSEL, VK_F1, VK_F10, VK_F11, VK_F12, VK_F13, VK_F14, + VK_F15, VK_F16, VK_F17, VK_F18, VK_F19, VK_F2, VK_F20, VK_F21, VK_F22, VK_F23, + VK_F24, VK_F3, VK_F4, VK_F5, VK_F6, VK_F7, VK_F8, VK_F9, VK_FINAL, VK_GAMEPAD_A, + VK_GAMEPAD_B, VK_GAMEPAD_DPAD_DOWN, VK_GAMEPAD_DPAD_LEFT, VK_GAMEPAD_DPAD_RIGHT, + VK_GAMEPAD_DPAD_UP, VK_GAMEPAD_LEFT_SHOULDER, VK_GAMEPAD_LEFT_THUMBSTICK_BUTTON, + VK_GAMEPAD_LEFT_THUMBSTICK_DOWN, VK_GAMEPAD_LEFT_THUMBSTICK_LEFT, + VK_GAMEPAD_LEFT_THUMBSTICK_RIGHT, VK_GAMEPAD_LEFT_THUMBSTICK_UP, + VK_GAMEPAD_LEFT_TRIGGER, VK_GAMEPAD_MENU, VK_GAMEPAD_RIGHT_SHOULDER, + VK_GAMEPAD_RIGHT_THUMBSTICK_BUTTON, VK_GAMEPAD_RIGHT_THUMBSTICK_DOWN, + VK_GAMEPAD_RIGHT_THUMBSTICK_LEFT, VK_GAMEPAD_RIGHT_THUMBSTICK_RIGHT, + VK_GAMEPAD_RIGHT_THUMBSTICK_UP, VK_GAMEPAD_RIGHT_TRIGGER, VK_GAMEPAD_VIEW, + VK_GAMEPAD_X, VK_GAMEPAD_Y, VK_HANGUL, VK_HANJA, VK_HELP, VK_HOME, VK_ICO_00, + VK_ICO_CLEAR, VK_ICO_HELP, VK_INSERT, VK_JUNJA, VK_KANA, VK_KANJI, VK_LAUNCH_APP1, + VK_LAUNCH_APP2, VK_LAUNCH_MAIL, VK_LAUNCH_MEDIA_SELECT, VK_LBUTTON, VK_LCONTROL, + VK_LEFT, VK_LMENU, VK_LSHIFT, VK_LWIN, VK_MBUTTON, VK_MEDIA_NEXT_TRACK, + VK_MEDIA_PLAY_PAUSE, VK_MEDIA_PREV_TRACK, VK_MEDIA_STOP, VK_MENU, VK_MODECHANGE, + VK_MULTIPLY, VK_NAVIGATION_ACCEPT, VK_NAVIGATION_CANCEL, VK_NAVIGATION_DOWN, + VK_NAVIGATION_LEFT, VK_NAVIGATION_MENU, VK_NAVIGATION_RIGHT, VK_NAVIGATION_UP, + VK_NAVIGATION_VIEW, VK_NEXT, VK_NONAME, VK_NONCONVERT, VK_NUMLOCK, VK_NUMPAD0, + VK_NUMPAD1, VK_NUMPAD2, VK_NUMPAD3, VK_NUMPAD4, VK_NUMPAD5, VK_NUMPAD6, VK_NUMPAD7, + VK_NUMPAD8, VK_NUMPAD9, VK_OEM_1, VK_OEM_102, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, + VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_ATTN, VK_OEM_AUTO, VK_OEM_AX, VK_OEM_BACKTAB, + VK_OEM_CLEAR, VK_OEM_COMMA, VK_OEM_COPY, VK_OEM_CUSEL, VK_OEM_ENLW, VK_OEM_FINISH, + VK_OEM_FJ_LOYA, VK_OEM_FJ_MASSHOU, VK_OEM_FJ_ROYA, VK_OEM_FJ_TOUROKU, VK_OEM_JUMP, + VK_OEM_MINUS, VK_OEM_NEC_EQUAL, VK_OEM_PA1, VK_OEM_PA2, VK_OEM_PA3, VK_OEM_PERIOD, + VK_OEM_PLUS, VK_OEM_RESET, VK_OEM_WSCTRL, VK_PA1, VK_PACKET, VK_PAUSE, VK_PLAY, + VK_PRINT, VK_PRIOR, VK_PROCESSKEY, VK_RBUTTON, VK_RCONTROL, VK_RETURN, VK_RIGHT, + VK_RMENU, VK_RSHIFT, VK_RWIN, VK_SCROLL, VK_SELECT, VK_SEPARATOR, VK_SHIFT, VK_SLEEP, + VK_SNAPSHOT, VK_SPACE, VK_SUBTRACT, VK_TAB, VK_UP, VK_VOLUME_DOWN, VK_VOLUME_MUTE, + VK_VOLUME_UP, VK_XBUTTON1, VK_XBUTTON2, VK_ZOOM, +}; +use windows_sys::Win32::UI::TextServices::HKL; + +use crate::keyboard::{Key, KeyCode, ModifiersState, NamedKey, NativeKey, PhysicalKey}; +use crate::platform_impl::{loword, primarylangid, scancode_to_physicalkey}; + +pub(crate) static LAYOUT_CACHE: Lazy> = + Lazy::new(|| Mutex::new(LayoutCache::default())); + +fn key_pressed(vkey: VIRTUAL_KEY) -> bool { + unsafe { (GetKeyState(vkey as i32) & (1 << 15)) == (1 << 15) } +} + +const NUMPAD_VKEYS: [VIRTUAL_KEY; 16] = [ + VK_NUMPAD0, + VK_NUMPAD1, + VK_NUMPAD2, + VK_NUMPAD3, + VK_NUMPAD4, + VK_NUMPAD5, + VK_NUMPAD6, + VK_NUMPAD7, + VK_NUMPAD8, + VK_NUMPAD9, + VK_MULTIPLY, + VK_ADD, + VK_SEPARATOR, + VK_SUBTRACT, + VK_DECIMAL, + VK_DIVIDE, +]; + +static NUMPAD_KEYCODES: Lazy> = Lazy::new(|| { + let mut keycodes = HashSet::new(); + keycodes.insert(KeyCode::Numpad0); + keycodes.insert(KeyCode::Numpad1); + keycodes.insert(KeyCode::Numpad2); + keycodes.insert(KeyCode::Numpad3); + keycodes.insert(KeyCode::Numpad4); + keycodes.insert(KeyCode::Numpad5); + keycodes.insert(KeyCode::Numpad6); + keycodes.insert(KeyCode::Numpad7); + keycodes.insert(KeyCode::Numpad8); + keycodes.insert(KeyCode::Numpad9); + keycodes.insert(KeyCode::NumpadMultiply); + keycodes.insert(KeyCode::NumpadAdd); + keycodes.insert(KeyCode::NumpadComma); + keycodes.insert(KeyCode::NumpadSubtract); + keycodes.insert(KeyCode::NumpadDecimal); + keycodes.insert(KeyCode::NumpadDivide); + keycodes +}); + +bitflags::bitflags! { + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct WindowsModifiers : u8 { + const SHIFT = 1 << 0; + const CONTROL = 1 << 1; + const ALT = 1 << 2; + const CAPS_LOCK = 1 << 3; + const FLAGS_END = 1 << 4; + } +} + +impl WindowsModifiers { + pub fn active_modifiers(key_state: &[u8; 256]) -> WindowsModifiers { + let shift = key_state[VK_SHIFT as usize] & 0x80 != 0; + let lshift = key_state[VK_LSHIFT as usize] & 0x80 != 0; + let rshift = key_state[VK_RSHIFT as usize] & 0x80 != 0; + + let control = key_state[VK_CONTROL as usize] & 0x80 != 0; + let lcontrol = key_state[VK_LCONTROL as usize] & 0x80 != 0; + let rcontrol = key_state[VK_RCONTROL as usize] & 0x80 != 0; + + let alt = key_state[VK_MENU as usize] & 0x80 != 0; + let lalt = key_state[VK_LMENU as usize] & 0x80 != 0; + let ralt = key_state[VK_RMENU as usize] & 0x80 != 0; + + let caps = key_state[VK_CAPITAL as usize] & 0x01 != 0; + + let mut result = WindowsModifiers::empty(); + if shift || lshift || rshift { + result.insert(WindowsModifiers::SHIFT); + } + if control || lcontrol || rcontrol { + result.insert(WindowsModifiers::CONTROL); + } + if alt || lalt || ralt { + result.insert(WindowsModifiers::ALT); + } + if caps { + result.insert(WindowsModifiers::CAPS_LOCK); + } + + result + } + + pub fn apply_to_kbd_state(self, key_state: &mut [u8; 256]) { + if self.intersects(Self::SHIFT) { + key_state[VK_SHIFT as usize] |= 0x80; + } else { + key_state[VK_SHIFT as usize] &= !0x80; + key_state[VK_LSHIFT as usize] &= !0x80; + key_state[VK_RSHIFT as usize] &= !0x80; + } + if self.intersects(Self::CONTROL) { + key_state[VK_CONTROL as usize] |= 0x80; + } else { + key_state[VK_CONTROL as usize] &= !0x80; + key_state[VK_LCONTROL as usize] &= !0x80; + key_state[VK_RCONTROL as usize] &= !0x80; + } + if self.intersects(Self::ALT) { + key_state[VK_MENU as usize] |= 0x80; + } else { + key_state[VK_MENU as usize] &= !0x80; + key_state[VK_LMENU as usize] &= !0x80; + key_state[VK_RMENU as usize] &= !0x80; + } + if self.intersects(Self::CAPS_LOCK) { + key_state[VK_CAPITAL as usize] |= 0x01; + } else { + key_state[VK_CAPITAL as usize] &= !0x01; + } + } + + /// Removes the control modifier if the alt modifier is not present. + /// This is useful because on Windows: (Control + Alt) == AltGr + /// but we don't want to interfere with the AltGr state. + pub fn remove_only_ctrl(mut self) -> WindowsModifiers { + if !self.contains(WindowsModifiers::ALT) { + self.remove(WindowsModifiers::CONTROL); + } + self + } +} + +pub(crate) struct Layout { + pub hkl: u64, + + /// Maps numpad keys from Windows virtual key to a `Key`. + /// + /// This is useful because some numpad keys generate different characters based on the locale. + /// For example `VK_DECIMAL` is sometimes "." and sometimes ",". Note: numpad-specific virtual + /// keys are only produced by Windows when the NumLock is active. + /// + /// Making this field separate from the `keys` field saves having to add NumLock as a modifier + /// to `WindowsModifiers`, which would double the number of items in keys. + pub numlock_on_keys: HashMap, + /// Like `numlock_on_keys` but this will map to the key that would be produced if numlock was + /// off. The keys of this map are identical to the keys of `numlock_on_keys`. + pub numlock_off_keys: HashMap, + + /// Maps a modifier state to group of key strings + /// We're not using `ModifiersState` here because that object cannot express caps lock, + /// but we need to handle caps lock too. + /// + /// This map shouldn't need to exist. + /// However currently this seems to be the only good way + /// of getting the label for the pressed key. Note that calling `ToUnicode` + /// just when the key is pressed/released would be enough if `ToUnicode` wouldn't + /// change the keyboard state (it clears the dead key). There is a flag to prevent + /// changing the state, but that flag requires Windows 10, version 1607 or newer) + pub keys: HashMap>, + pub has_alt_graph: bool, +} + +impl Layout { + pub fn get_key( + &self, + mods: WindowsModifiers, + num_lock_on: bool, + vkey: VIRTUAL_KEY, + physical_key: &PhysicalKey, + ) -> Key { + let native_code = NativeKey::Windows(vkey); + + let unknown_alt = vkey == VK_MENU; + if !unknown_alt { + // Here we try using the virtual key directly but if the virtual key doesn't distinguish + // between left and right alt, we can't report AltGr. Therefore, we only do this if the + // key is not the "unknown alt" key. + // + // The reason for using the virtual key directly is that `MapVirtualKeyExW` (used when + // building the keys map) sometimes maps virtual keys to odd scancodes that don't match + // the scancode coming from the KEYDOWN message for the same key. For example: `VK_LEFT` + // is mapped to `0x004B`, but the scancode for the left arrow is `0xE04B`. + let key_from_vkey = vkey_to_non_char_key( + vkey, + native_code.clone(), + self.hkl, + self.has_alt_graph, + ); + + if !matches!(key_from_vkey, Key::Unidentified(_)) { + return key_from_vkey; + } + } + if num_lock_on { + if let Some(key) = self.numlock_on_keys.get(&vkey) { + return key.clone(); + } + } else if let Some(key) = self.numlock_off_keys.get(&vkey) { + return key.clone(); + } + if let PhysicalKey::Code(code) = physical_key { + if let Some(keys) = self.keys.get(&mods) { + if let Some(key) = keys.get(code) { + return key.clone(); + } + } + } + Key::Unidentified(native_code) + } +} + +#[derive(Default)] +pub(crate) struct LayoutCache { + /// Maps locale identifiers (HKL) to layouts + pub layouts: HashMap, +} + +impl LayoutCache { + /// Checks whether the current layout is already known and + /// prepares the layout if it isn't known. + /// The current layout is then returned. + pub fn get_current_layout(&mut self) -> (u64, &Layout) { + let locale_id = unsafe { GetKeyboardLayout(0) } as u64; + match self.layouts.entry(locale_id) { + Entry::Occupied(entry) => (locale_id, entry.into_mut()), + Entry::Vacant(entry) => { + let layout = Self::prepare_layout(locale_id); + (locale_id, entry.insert(layout)) + } + } + } + + pub fn get_agnostic_mods(&mut self) -> ModifiersState { + let (_, layout) = self.get_current_layout(); + let filter_out_altgr = layout.has_alt_graph && key_pressed(VK_RMENU); + let mut mods = ModifiersState::empty(); + mods.set(ModifiersState::SHIFT, key_pressed(VK_SHIFT)); + mods.set( + ModifiersState::CONTROL, + key_pressed(VK_CONTROL) && !filter_out_altgr, + ); + mods.set( + ModifiersState::ALT, + key_pressed(VK_MENU) && !filter_out_altgr, + ); + mods.set( + ModifiersState::SUPER, + key_pressed(VK_LWIN) || key_pressed(VK_RWIN), + ); + mods + } + + fn prepare_layout(locale_id: u64) -> Layout { + let mut layout = Layout { + hkl: locale_id, + numlock_on_keys: Default::default(), + numlock_off_keys: Default::default(), + keys: Default::default(), + has_alt_graph: false, + }; + + // We initialize the keyboard state with all zeros to + // simulate a scenario when no modifier is active. + let mut key_state = [0u8; 256]; + + // `MapVirtualKeyExW` maps (non-numpad-specific) virtual keys to scancodes as if numlock + // was off. We rely on this behavior to find all virtual keys which are not numpad-specific + // but map to the numpad. + // + // src_vkey: VK ==> scancode: u16 (on the numpad) + // + // Then we convert the source virtual key into a `Key` and the scancode into a virtual key + // to get the reverse mapping. + // + // src_vkey: VK ==> scancode: u16 (on the numpad) + // || || + // \/ \/ + // map_value: Key <- map_vkey: VK + layout.numlock_off_keys.reserve(NUMPAD_KEYCODES.len()); + for vk in 0..256 { + let scancode = + unsafe { MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC_EX, locale_id as HKL) }; + if scancode == 0 { + continue; + } + let keycode = match scancode_to_physicalkey(scancode) { + PhysicalKey::Code(code) => code, + // TODO: validate that we can skip on unidentified keys (probably never occurs?) + _ => continue, + }; + if !is_numpad_specific(vk as VIRTUAL_KEY) + && NUMPAD_KEYCODES.contains(&keycode) + { + let native_code = NativeKey::Windows(vk as VIRTUAL_KEY); + let map_vkey = keycode_to_vkey(keycode, locale_id); + if map_vkey == 0 { + continue; + } + let map_value = vkey_to_non_char_key( + vk as VIRTUAL_KEY, + native_code, + locale_id, + false, + ); + if matches!(map_value, Key::Unidentified(_)) { + continue; + } + layout.numlock_off_keys.insert(map_vkey, map_value); + } + } + + layout.numlock_on_keys.reserve(NUMPAD_VKEYS.len()); + for vk in NUMPAD_VKEYS.iter() { + let vk = (*vk) as u32; + let scancode = + unsafe { MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC_EX, locale_id as HKL) }; + let unicode = Self::to_unicode_string(&key_state, vk, scancode, locale_id); + if let ToUnicodeResult::Str(s) = unicode { + layout + .numlock_on_keys + .insert(vk as VIRTUAL_KEY, Key::Character(SmolStr::new(s))); + } + } + + // Iterate through every combination of modifiers + let mods_end = WindowsModifiers::FLAGS_END.bits(); + for mod_state in 0..mods_end { + let mut keys_for_this_mod = HashMap::with_capacity(256); + + let mod_state = WindowsModifiers::from_bits_retain(mod_state); + mod_state.apply_to_kbd_state(&mut key_state); + + // Virtual key values are in the domain [0, 255]. + // This is reinforced by the fact that the keyboard state array has 256 + // elements. This array is allowed to be indexed by virtual key values + // giving the key state for the virtual key used for indexing. + for vk in 0..256 { + let scancode = + unsafe { MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC_EX, locale_id as HKL) }; + if scancode == 0 { + continue; + } + + let native_code = NativeKey::Windows(vk as VIRTUAL_KEY); + let key_code = match scancode_to_physicalkey(scancode) { + PhysicalKey::Code(code) => code, + // TODO: validate that we can skip on unidentified keys (probably never occurs?) + _ => continue, + }; + // Let's try to get the key from just the scancode and vk + // We don't necessarily know yet if AltGraph is present on this layout so we'll + // assume it isn't. Then we'll do a second pass where we set the "AltRight" keys to + // "AltGr" in case we find out that there's an AltGraph. + let preliminary_key = vkey_to_non_char_key( + vk as VIRTUAL_KEY, + native_code, + locale_id, + false, + ); + match preliminary_key { + Key::Unidentified(_) => (), + _ => { + keys_for_this_mod.insert(key_code, preliminary_key); + continue; + } + } + + let unicode = + Self::to_unicode_string(&key_state, vk, scancode, locale_id); + let key = match unicode { + ToUnicodeResult::Str(str) => Key::Character(SmolStr::new(str)), + ToUnicodeResult::Dead(dead_char) => { + // println!("{:?} - {:?} produced dead {:?}", key_code, mod_state, + // dead_char); + Key::Dead(dead_char) + } + ToUnicodeResult::None => { + let has_alt = mod_state.contains(WindowsModifiers::ALT); + let has_ctrl = mod_state.contains(WindowsModifiers::CONTROL); + // HACK: `ToUnicodeEx` seems to fail getting the string for the numpad + // divide key, so we handle that explicitly here + if !has_alt && !has_ctrl && key_code == KeyCode::NumpadDivide { + Key::Character(SmolStr::new("/")) + } else { + // Just use the unidentified key, we got earlier + preliminary_key + } + } + }; + + // Check for alt graph. + // The logic is that if a key pressed with no modifier produces + // a different `Character` from when it's pressed with CTRL+ALT then the layout + // has AltGr. + let ctrl_alt: WindowsModifiers = + WindowsModifiers::CONTROL | WindowsModifiers::ALT; + let is_in_ctrl_alt = mod_state == ctrl_alt; + if !layout.has_alt_graph && is_in_ctrl_alt { + // Unwrapping here because if we are in the ctrl+alt modifier state + // then the alt modifier state must have come before. + let simple_keys = + layout.keys.get(&WindowsModifiers::empty()).unwrap(); + if let Some(Key::Character(key_no_altgr)) = simple_keys.get(&key_code) + { + if let Key::Character(key) = &key { + layout.has_alt_graph = key != key_no_altgr; + } + } + } + + keys_for_this_mod.insert(key_code, key); + } + layout.keys.insert(mod_state, keys_for_this_mod); + } + + // Second pass: replace right alt keys with AltGr if the layout has alt graph + if layout.has_alt_graph { + for mod_state in 0..mods_end { + let mod_state = WindowsModifiers::from_bits_retain(mod_state); + if let Some(keys) = layout.keys.get_mut(&mod_state) { + if let Some(key) = keys.get_mut(&KeyCode::AltRight) { + *key = Key::Named(NamedKey::AltGraph); + } + } + } + } + + layout + } + + fn to_unicode_string( + key_state: &[u8; 256], + vkey: u32, + scancode: u32, + locale_id: u64, + ) -> ToUnicodeResult { + unsafe { + let mut label_wide = [0u16; 8]; + let mut wide_len = ToUnicodeEx( + vkey, + scancode, + (&key_state[0]) as *const _, + (&mut label_wide[0]) as *mut _, + label_wide.len() as i32, + 0, + locale_id as HKL, + ); + if wide_len < 0 { + // If it's dead, we run `ToUnicode` again to consume the dead-key + wide_len = ToUnicodeEx( + vkey, + scancode, + (&key_state[0]) as *const _, + (&mut label_wide[0]) as *mut _, + label_wide.len() as i32, + 0, + locale_id as HKL, + ); + if wide_len > 0 { + let os_string = + OsString::from_wide(&label_wide[0..wide_len as usize]); + if let Ok(label_str) = os_string.into_string() { + if let Some(ch) = label_str.chars().next() { + return ToUnicodeResult::Dead(Some(ch)); + } + } + } + return ToUnicodeResult::Dead(None); + } + if wide_len > 0 { + let os_string = OsString::from_wide(&label_wide[0..wide_len as usize]); + if let Ok(label_str) = os_string.into_string() { + return ToUnicodeResult::Str(label_str); + } + } + } + ToUnicodeResult::None + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +enum ToUnicodeResult { + Str(String), + Dead(Option), + None, +} + +fn is_numpad_specific(vk: VIRTUAL_KEY) -> bool { + matches!( + vk, + VK_NUMPAD0 + | VK_NUMPAD1 + | VK_NUMPAD2 + | VK_NUMPAD3 + | VK_NUMPAD4 + | VK_NUMPAD5 + | VK_NUMPAD6 + | VK_NUMPAD7 + | VK_NUMPAD8 + | VK_NUMPAD9 + | VK_ADD + | VK_SUBTRACT + | VK_DIVIDE + | VK_DECIMAL + | VK_SEPARATOR + ) +} + +fn keycode_to_vkey(keycode: KeyCode, hkl: u64) -> VIRTUAL_KEY { + let primary_lang_id = primarylangid(loword(hkl as u32)); + let is_korean = primary_lang_id as u32 == LANG_KOREAN; + let is_japanese = primary_lang_id as u32 == LANG_JAPANESE; + + match keycode { + KeyCode::Backquote => 0, + KeyCode::Backslash => 0, + KeyCode::BracketLeft => 0, + KeyCode::BracketRight => 0, + KeyCode::Comma => 0, + KeyCode::Digit0 => 0, + KeyCode::Digit1 => 0, + KeyCode::Digit2 => 0, + KeyCode::Digit3 => 0, + KeyCode::Digit4 => 0, + KeyCode::Digit5 => 0, + KeyCode::Digit6 => 0, + KeyCode::Digit7 => 0, + KeyCode::Digit8 => 0, + KeyCode::Digit9 => 0, + KeyCode::Equal => 0, + KeyCode::IntlBackslash => 0, + KeyCode::IntlRo => 0, + KeyCode::IntlYen => 0, + KeyCode::KeyA => 0, + KeyCode::KeyB => 0, + KeyCode::KeyC => 0, + KeyCode::KeyD => 0, + KeyCode::KeyE => 0, + KeyCode::KeyF => 0, + KeyCode::KeyG => 0, + KeyCode::KeyH => 0, + KeyCode::KeyI => 0, + KeyCode::KeyJ => 0, + KeyCode::KeyK => 0, + KeyCode::KeyL => 0, + KeyCode::KeyM => 0, + KeyCode::KeyN => 0, + KeyCode::KeyO => 0, + KeyCode::KeyP => 0, + KeyCode::KeyQ => 0, + KeyCode::KeyR => 0, + KeyCode::KeyS => 0, + KeyCode::KeyT => 0, + KeyCode::KeyU => 0, + KeyCode::KeyV => 0, + KeyCode::KeyW => 0, + KeyCode::KeyX => 0, + KeyCode::KeyY => 0, + KeyCode::KeyZ => 0, + KeyCode::Minus => 0, + KeyCode::Period => 0, + KeyCode::Quote => 0, + KeyCode::Semicolon => 0, + KeyCode::Slash => 0, + KeyCode::AltLeft => VK_LMENU, + KeyCode::AltRight => VK_RMENU, + KeyCode::Backspace => VK_BACK, + KeyCode::CapsLock => VK_CAPITAL, + KeyCode::ContextMenu => VK_APPS, + KeyCode::ControlLeft => VK_LCONTROL, + KeyCode::ControlRight => VK_RCONTROL, + KeyCode::Enter => VK_RETURN, + KeyCode::SuperLeft => VK_LWIN, + KeyCode::SuperRight => VK_RWIN, + KeyCode::ShiftLeft => VK_RSHIFT, + KeyCode::ShiftRight => VK_LSHIFT, + KeyCode::Space => VK_SPACE, + KeyCode::Tab => VK_TAB, + KeyCode::Convert => VK_CONVERT, + KeyCode::KanaMode => VK_KANA, + KeyCode::Lang1 if is_korean => VK_HANGUL, + KeyCode::Lang1 if is_japanese => VK_KANA, + KeyCode::Lang2 if is_korean => VK_HANJA, + KeyCode::Lang2 if is_japanese => 0, + KeyCode::Lang3 if is_japanese => VK_OEM_FINISH, + KeyCode::Lang4 if is_japanese => 0, + KeyCode::Lang5 if is_japanese => 0, + KeyCode::NonConvert => VK_NONCONVERT, + KeyCode::Delete => VK_DELETE, + KeyCode::End => VK_END, + KeyCode::Help => VK_HELP, + KeyCode::Home => VK_HOME, + KeyCode::Insert => VK_INSERT, + KeyCode::PageDown => VK_NEXT, + KeyCode::PageUp => VK_PRIOR, + KeyCode::ArrowDown => VK_DOWN, + KeyCode::ArrowLeft => VK_LEFT, + KeyCode::ArrowRight => VK_RIGHT, + KeyCode::ArrowUp => VK_UP, + KeyCode::NumLock => VK_NUMLOCK, + KeyCode::Numpad0 => VK_NUMPAD0, + KeyCode::Numpad1 => VK_NUMPAD1, + KeyCode::Numpad2 => VK_NUMPAD2, + KeyCode::Numpad3 => VK_NUMPAD3, + KeyCode::Numpad4 => VK_NUMPAD4, + KeyCode::Numpad5 => VK_NUMPAD5, + KeyCode::Numpad6 => VK_NUMPAD6, + KeyCode::Numpad7 => VK_NUMPAD7, + KeyCode::Numpad8 => VK_NUMPAD8, + KeyCode::Numpad9 => VK_NUMPAD9, + KeyCode::NumpadAdd => VK_ADD, + KeyCode::NumpadBackspace => VK_BACK, + KeyCode::NumpadClear => VK_CLEAR, + KeyCode::NumpadClearEntry => 0, + KeyCode::NumpadComma => VK_SEPARATOR, + KeyCode::NumpadDecimal => VK_DECIMAL, + KeyCode::NumpadDivide => VK_DIVIDE, + KeyCode::NumpadEnter => VK_RETURN, + KeyCode::NumpadEqual => 0, + KeyCode::NumpadHash => 0, + KeyCode::NumpadMemoryAdd => 0, + KeyCode::NumpadMemoryClear => 0, + KeyCode::NumpadMemoryRecall => 0, + KeyCode::NumpadMemoryStore => 0, + KeyCode::NumpadMemorySubtract => 0, + KeyCode::NumpadMultiply => VK_MULTIPLY, + KeyCode::NumpadParenLeft => 0, + KeyCode::NumpadParenRight => 0, + KeyCode::NumpadStar => 0, + KeyCode::NumpadSubtract => VK_SUBTRACT, + KeyCode::Escape => VK_ESCAPE, + KeyCode::Fn => 0, + KeyCode::FnLock => 0, + KeyCode::PrintScreen => VK_SNAPSHOT, + KeyCode::ScrollLock => VK_SCROLL, + KeyCode::Pause => VK_PAUSE, + KeyCode::BrowserBack => VK_BROWSER_BACK, + KeyCode::BrowserFavorites => VK_BROWSER_FAVORITES, + KeyCode::BrowserForward => VK_BROWSER_FORWARD, + KeyCode::BrowserHome => VK_BROWSER_HOME, + KeyCode::BrowserRefresh => VK_BROWSER_REFRESH, + KeyCode::BrowserSearch => VK_BROWSER_SEARCH, + KeyCode::BrowserStop => VK_BROWSER_STOP, + KeyCode::Eject => 0, + KeyCode::LaunchApp1 => VK_LAUNCH_APP1, + KeyCode::LaunchApp2 => VK_LAUNCH_APP2, + KeyCode::LaunchMail => VK_LAUNCH_MAIL, + KeyCode::MediaPlayPause => VK_MEDIA_PLAY_PAUSE, + KeyCode::MediaSelect => VK_LAUNCH_MEDIA_SELECT, + KeyCode::MediaStop => VK_MEDIA_STOP, + KeyCode::MediaTrackNext => VK_MEDIA_NEXT_TRACK, + KeyCode::MediaTrackPrevious => VK_MEDIA_PREV_TRACK, + KeyCode::Power => 0, + KeyCode::Sleep => 0, + KeyCode::AudioVolumeDown => VK_VOLUME_DOWN, + KeyCode::AudioVolumeMute => VK_VOLUME_MUTE, + KeyCode::AudioVolumeUp => VK_VOLUME_UP, + KeyCode::WakeUp => 0, + KeyCode::Hyper => 0, + KeyCode::Turbo => 0, + KeyCode::Abort => 0, + KeyCode::Resume => 0, + KeyCode::Suspend => 0, + KeyCode::Again => 0, + KeyCode::Copy => 0, + KeyCode::Cut => 0, + KeyCode::Find => 0, + KeyCode::Open => 0, + KeyCode::Paste => 0, + KeyCode::Props => 0, + KeyCode::Select => VK_SELECT, + KeyCode::Undo => 0, + KeyCode::Hiragana => 0, + KeyCode::Katakana => 0, + KeyCode::F1 => VK_F1, + KeyCode::F2 => VK_F2, + KeyCode::F3 => VK_F3, + KeyCode::F4 => VK_F4, + KeyCode::F5 => VK_F5, + KeyCode::F6 => VK_F6, + KeyCode::F7 => VK_F7, + KeyCode::F8 => VK_F8, + KeyCode::F9 => VK_F9, + KeyCode::F10 => VK_F10, + KeyCode::F11 => VK_F11, + KeyCode::F12 => VK_F12, + KeyCode::F13 => VK_F13, + KeyCode::F14 => VK_F14, + KeyCode::F15 => VK_F15, + KeyCode::F16 => VK_F16, + KeyCode::F17 => VK_F17, + KeyCode::F18 => VK_F18, + KeyCode::F19 => VK_F19, + KeyCode::F20 => VK_F20, + KeyCode::F21 => VK_F21, + KeyCode::F22 => VK_F22, + KeyCode::F23 => VK_F23, + KeyCode::F24 => VK_F24, + KeyCode::F25 => 0, + KeyCode::F26 => 0, + KeyCode::F27 => 0, + KeyCode::F28 => 0, + KeyCode::F29 => 0, + KeyCode::F30 => 0, + KeyCode::F31 => 0, + KeyCode::F32 => 0, + KeyCode::F33 => 0, + KeyCode::F34 => 0, + KeyCode::F35 => 0, + _ => 0, + } +} + +/// This converts virtual keys to `Key`s. Only virtual keys which can be unambiguously converted to +/// a `Key`, with only the information passed in as arguments, are converted. +/// +/// In other words: this function does not need to "prepare" the current layout in order to do +/// the conversion, but as such it cannot convert certain keys, like language-specific character +/// keys. +/// +/// The result includes all non-character keys defined within `Key` plus characters from numpad +/// keys. For example, backspace and tab are included. +fn vkey_to_non_char_key( + vkey: VIRTUAL_KEY, + native_code: NativeKey, + hkl: u64, + has_alt_graph: bool, +) -> Key { + // List of the Web key names and their corresponding platform-native key names: + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + + let primary_lang_id = primarylangid(loword(hkl as u32)); + let is_korean = primary_lang_id as u32 == LANG_KOREAN; + let is_japanese = primary_lang_id as u32 == LANG_JAPANESE; + + match vkey { + VK_LBUTTON => Key::Unidentified(NativeKey::Unidentified), // Mouse + VK_RBUTTON => Key::Unidentified(NativeKey::Unidentified), // Mouse + + // I don't think this can be represented with a Key + VK_CANCEL => Key::Unidentified(native_code), + + VK_MBUTTON => Key::Unidentified(NativeKey::Unidentified), // Mouse + VK_XBUTTON1 => Key::Unidentified(NativeKey::Unidentified), // Mouse + VK_XBUTTON2 => Key::Unidentified(NativeKey::Unidentified), // Mouse + VK_BACK => Key::Named(NamedKey::Backspace), + VK_TAB => Key::Named(NamedKey::Tab), + VK_CLEAR => Key::Named(NamedKey::Clear), + VK_RETURN => Key::Named(NamedKey::Enter), + VK_SHIFT => Key::Named(NamedKey::Shift), + VK_CONTROL => Key::Named(NamedKey::Control), + VK_MENU => Key::Named(NamedKey::Alt), + VK_PAUSE => Key::Named(NamedKey::Pause), + VK_CAPITAL => Key::Named(NamedKey::CapsLock), + + // VK_HANGEUL => Key::Named(NamedKey::HangulMode), // Deprecated in favour of VK_HANGUL + + // VK_HANGUL and VK_KANA are defined as the same constant, therefore + // we use appropriate conditions to differentiate between them + VK_HANGUL if is_korean => Key::Named(NamedKey::HangulMode), + VK_KANA if is_japanese => Key::Named(NamedKey::KanaMode), + + VK_JUNJA => Key::Named(NamedKey::JunjaMode), + VK_FINAL => Key::Named(NamedKey::FinalMode), + + // VK_HANJA and VK_KANJI are defined as the same constant, therefore + // we use appropriate conditions to differentiate between them + VK_HANJA if is_korean => Key::Named(NamedKey::HanjaMode), + VK_KANJI if is_japanese => Key::Named(NamedKey::KanjiMode), + + VK_ESCAPE => Key::Named(NamedKey::Escape), + VK_CONVERT => Key::Named(NamedKey::Convert), + VK_NONCONVERT => Key::Named(NamedKey::NonConvert), + VK_ACCEPT => Key::Named(NamedKey::Accept), + VK_MODECHANGE => Key::Named(NamedKey::ModeChange), + VK_SPACE => Key::Named(NamedKey::Space), + VK_PRIOR => Key::Named(NamedKey::PageUp), + VK_NEXT => Key::Named(NamedKey::PageDown), + VK_END => Key::Named(NamedKey::End), + VK_HOME => Key::Named(NamedKey::Home), + VK_LEFT => Key::Named(NamedKey::ArrowLeft), + VK_UP => Key::Named(NamedKey::ArrowUp), + VK_RIGHT => Key::Named(NamedKey::ArrowRight), + VK_DOWN => Key::Named(NamedKey::ArrowDown), + VK_SELECT => Key::Named(NamedKey::Select), + VK_PRINT => Key::Named(NamedKey::Print), + VK_EXECUTE => Key::Named(NamedKey::Execute), + VK_SNAPSHOT => Key::Named(NamedKey::PrintScreen), + VK_INSERT => Key::Named(NamedKey::Insert), + VK_DELETE => Key::Named(NamedKey::Delete), + VK_HELP => Key::Named(NamedKey::Help), + VK_LWIN => Key::Named(NamedKey::Super), + VK_RWIN => Key::Named(NamedKey::Super), + VK_APPS => Key::Named(NamedKey::ContextMenu), + VK_SLEEP => Key::Named(NamedKey::Standby), + + // Numpad keys produce characters + VK_NUMPAD0 => Key::Unidentified(native_code), + VK_NUMPAD1 => Key::Unidentified(native_code), + VK_NUMPAD2 => Key::Unidentified(native_code), + VK_NUMPAD3 => Key::Unidentified(native_code), + VK_NUMPAD4 => Key::Unidentified(native_code), + VK_NUMPAD5 => Key::Unidentified(native_code), + VK_NUMPAD6 => Key::Unidentified(native_code), + VK_NUMPAD7 => Key::Unidentified(native_code), + VK_NUMPAD8 => Key::Unidentified(native_code), + VK_NUMPAD9 => Key::Unidentified(native_code), + VK_MULTIPLY => Key::Unidentified(native_code), + VK_ADD => Key::Unidentified(native_code), + VK_SEPARATOR => Key::Unidentified(native_code), + VK_SUBTRACT => Key::Unidentified(native_code), + VK_DECIMAL => Key::Unidentified(native_code), + VK_DIVIDE => Key::Unidentified(native_code), + + VK_F1 => Key::Named(NamedKey::F1), + VK_F2 => Key::Named(NamedKey::F2), + VK_F3 => Key::Named(NamedKey::F3), + VK_F4 => Key::Named(NamedKey::F4), + VK_F5 => Key::Named(NamedKey::F5), + VK_F6 => Key::Named(NamedKey::F6), + VK_F7 => Key::Named(NamedKey::F7), + VK_F8 => Key::Named(NamedKey::F8), + VK_F9 => Key::Named(NamedKey::F9), + VK_F10 => Key::Named(NamedKey::F10), + VK_F11 => Key::Named(NamedKey::F11), + VK_F12 => Key::Named(NamedKey::F12), + VK_F13 => Key::Named(NamedKey::F13), + VK_F14 => Key::Named(NamedKey::F14), + VK_F15 => Key::Named(NamedKey::F15), + VK_F16 => Key::Named(NamedKey::F16), + VK_F17 => Key::Named(NamedKey::F17), + VK_F18 => Key::Named(NamedKey::F18), + VK_F19 => Key::Named(NamedKey::F19), + VK_F20 => Key::Named(NamedKey::F20), + VK_F21 => Key::Named(NamedKey::F21), + VK_F22 => Key::Named(NamedKey::F22), + VK_F23 => Key::Named(NamedKey::F23), + VK_F24 => Key::Named(NamedKey::F24), + VK_NAVIGATION_VIEW => Key::Unidentified(native_code), + VK_NAVIGATION_MENU => Key::Unidentified(native_code), + VK_NAVIGATION_UP => Key::Unidentified(native_code), + VK_NAVIGATION_DOWN => Key::Unidentified(native_code), + VK_NAVIGATION_LEFT => Key::Unidentified(native_code), + VK_NAVIGATION_RIGHT => Key::Unidentified(native_code), + VK_NAVIGATION_ACCEPT => Key::Unidentified(native_code), + VK_NAVIGATION_CANCEL => Key::Unidentified(native_code), + VK_NUMLOCK => Key::Named(NamedKey::NumLock), + VK_SCROLL => Key::Named(NamedKey::ScrollLock), + VK_OEM_NEC_EQUAL => Key::Unidentified(native_code), + // VK_OEM_FJ_JISHO => Key::Unidentified(native_code), // Conflicts with `VK_OEM_NEC_EQUAL` + VK_OEM_FJ_MASSHOU => Key::Unidentified(native_code), + VK_OEM_FJ_TOUROKU => Key::Unidentified(native_code), + VK_OEM_FJ_LOYA => Key::Unidentified(native_code), + VK_OEM_FJ_ROYA => Key::Unidentified(native_code), + VK_LSHIFT => Key::Named(NamedKey::Shift), + VK_RSHIFT => Key::Named(NamedKey::Shift), + VK_LCONTROL => Key::Named(NamedKey::Control), + VK_RCONTROL => Key::Named(NamedKey::Control), + VK_LMENU => Key::Named(NamedKey::Alt), + VK_RMENU => { + if has_alt_graph { + Key::Named(NamedKey::AltGraph) + } else { + Key::Named(NamedKey::Alt) + } + } + VK_BROWSER_BACK => Key::Named(NamedKey::BrowserBack), + VK_BROWSER_FORWARD => Key::Named(NamedKey::BrowserForward), + VK_BROWSER_REFRESH => Key::Named(NamedKey::BrowserRefresh), + VK_BROWSER_STOP => Key::Named(NamedKey::BrowserStop), + VK_BROWSER_SEARCH => Key::Named(NamedKey::BrowserSearch), + VK_BROWSER_FAVORITES => Key::Named(NamedKey::BrowserFavorites), + VK_BROWSER_HOME => Key::Named(NamedKey::BrowserHome), + VK_VOLUME_MUTE => Key::Named(NamedKey::AudioVolumeMute), + VK_VOLUME_DOWN => Key::Named(NamedKey::AudioVolumeDown), + VK_VOLUME_UP => Key::Named(NamedKey::AudioVolumeUp), + VK_MEDIA_NEXT_TRACK => Key::Named(NamedKey::MediaTrackNext), + VK_MEDIA_PREV_TRACK => Key::Named(NamedKey::MediaTrackPrevious), + VK_MEDIA_STOP => Key::Named(NamedKey::MediaStop), + VK_MEDIA_PLAY_PAUSE => Key::Named(NamedKey::MediaPlayPause), + VK_LAUNCH_MAIL => Key::Named(NamedKey::LaunchMail), + VK_LAUNCH_MEDIA_SELECT => Key::Named(NamedKey::LaunchMediaPlayer), + VK_LAUNCH_APP1 => Key::Named(NamedKey::LaunchApplication1), + VK_LAUNCH_APP2 => Key::Named(NamedKey::LaunchApplication2), + + // This function only converts "non-printable" + VK_OEM_1 => Key::Unidentified(native_code), + VK_OEM_PLUS => Key::Unidentified(native_code), + VK_OEM_COMMA => Key::Unidentified(native_code), + VK_OEM_MINUS => Key::Unidentified(native_code), + VK_OEM_PERIOD => Key::Unidentified(native_code), + VK_OEM_2 => Key::Unidentified(native_code), + VK_OEM_3 => Key::Unidentified(native_code), + + VK_GAMEPAD_A => Key::Unidentified(native_code), + VK_GAMEPAD_B => Key::Unidentified(native_code), + VK_GAMEPAD_X => Key::Unidentified(native_code), + VK_GAMEPAD_Y => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_SHOULDER => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_SHOULDER => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_TRIGGER => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_TRIGGER => Key::Unidentified(native_code), + VK_GAMEPAD_DPAD_UP => Key::Unidentified(native_code), + VK_GAMEPAD_DPAD_DOWN => Key::Unidentified(native_code), + VK_GAMEPAD_DPAD_LEFT => Key::Unidentified(native_code), + VK_GAMEPAD_DPAD_RIGHT => Key::Unidentified(native_code), + VK_GAMEPAD_MENU => Key::Unidentified(native_code), + VK_GAMEPAD_VIEW => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_THUMBSTICK_BUTTON => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_THUMBSTICK_BUTTON => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_THUMBSTICK_UP => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_THUMBSTICK_DOWN => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_THUMBSTICK_RIGHT => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_THUMBSTICK_LEFT => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_THUMBSTICK_UP => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_THUMBSTICK_DOWN => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_THUMBSTICK_RIGHT => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_THUMBSTICK_LEFT => Key::Unidentified(native_code), + + // This function only converts "non-printable" + VK_OEM_4 => Key::Unidentified(native_code), + VK_OEM_5 => Key::Unidentified(native_code), + VK_OEM_6 => Key::Unidentified(native_code), + VK_OEM_7 => Key::Unidentified(native_code), + VK_OEM_8 => Key::Unidentified(native_code), + VK_OEM_AX => Key::Unidentified(native_code), + VK_OEM_102 => Key::Unidentified(native_code), + + VK_ICO_HELP => Key::Unidentified(native_code), + VK_ICO_00 => Key::Unidentified(native_code), + + VK_PROCESSKEY => Key::Named(NamedKey::Process), + + VK_ICO_CLEAR => Key::Unidentified(native_code), + VK_PACKET => Key::Unidentified(native_code), + VK_OEM_RESET => Key::Unidentified(native_code), + VK_OEM_JUMP => Key::Unidentified(native_code), + VK_OEM_PA1 => Key::Unidentified(native_code), + VK_OEM_PA2 => Key::Unidentified(native_code), + VK_OEM_PA3 => Key::Unidentified(native_code), + VK_OEM_WSCTRL => Key::Unidentified(native_code), + VK_OEM_CUSEL => Key::Unidentified(native_code), + + VK_OEM_ATTN => Key::Named(NamedKey::Attn), + VK_OEM_FINISH => { + if is_japanese { + Key::Named(NamedKey::Katakana) + } else { + // This matches IE and Firefox behaviour according to + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + // At the time of writing, there is no `NamedKey::Finish` variant as + // Finish is not mentioned at https://w3c.github.io/uievents-key/ + // Also see: https://github.com/pyfisch/keyboard-types/issues/9 + Key::Unidentified(native_code) + } + } + VK_OEM_COPY => Key::Named(NamedKey::Copy), + VK_OEM_AUTO => Key::Named(NamedKey::Hankaku), + VK_OEM_ENLW => Key::Named(NamedKey::Zenkaku), + VK_OEM_BACKTAB => Key::Named(NamedKey::Romaji), + VK_ATTN => Key::Named(NamedKey::KanaMode), + VK_CRSEL => Key::Named(NamedKey::CrSel), + VK_EXSEL => Key::Named(NamedKey::ExSel), + VK_EREOF => Key::Named(NamedKey::EraseEof), + VK_PLAY => Key::Named(NamedKey::Play), + VK_ZOOM => Key::Named(NamedKey::ZoomToggle), + VK_NONAME => Key::Unidentified(native_code), + VK_PA1 => Key::Unidentified(native_code), + VK_OEM_CLEAR => Key::Named(NamedKey::Clear), + _ => Key::Unidentified(native_code), + } +} diff --git a/rio-window/src/platform_impl/windows/minimal_ime.rs b/rio-window/src/platform_impl/windows/minimal_ime.rs new file mode 100644 index 00000000..71600abb --- /dev/null +++ b/rio-window/src/platform_impl/windows/minimal_ime.rs @@ -0,0 +1,67 @@ +use std::sync::{ + atomic::{AtomicBool, Ordering::Relaxed}, + Mutex, +}; + +use winapi::{ + shared::{ + minwindef::{LPARAM, WPARAM}, + windef::HWND, + }, + um::winuser, +}; + +use crate::platform_impl::platform::{event_loop::ProcResult, keyboard::next_kbd_msg}; + +pub struct MinimalIme { + // True if we're currently receiving messages belonging to a finished IME session. + getting_ime_text: AtomicBool, + + utf16parts: Mutex>, +} +impl Default for MinimalIme { + fn default() -> Self { + MinimalIme { + getting_ime_text: AtomicBool::new(false), + utf16parts: Mutex::new(Vec::with_capacity(16)), + } + } +} +impl MinimalIme { + pub(crate) fn process_message( + &self, + hwnd: HWND, + msg_kind: u32, + wparam: WPARAM, + _lparam: LPARAM, + result: &mut ProcResult, + ) -> Option { + match msg_kind { + winuser::WM_IME_ENDCOMPOSITION => { + self.getting_ime_text.store(true, Relaxed); + } + winuser::WM_CHAR | winuser::WM_SYSCHAR => { + if self.getting_ime_text.load(Relaxed) { + *result = ProcResult::Value(0); + self.utf16parts.lock().unwrap().push(wparam as u16); + // It's important that we push the new character and release the lock + // before getting the next message + let next_msg = next_kbd_msg(hwnd); + let more_char_coming = next_msg + .map(|m| matches!(m.message, winuser::WM_CHAR | winuser::WM_SYSCHAR)) + .unwrap_or(false); + if !more_char_coming { + let mut utf16parts = self.utf16parts.lock().unwrap(); + let result = String::from_utf16(&utf16parts).ok(); + utf16parts.clear(); + self.getting_ime_text.store(false, Relaxed); + return result; + } + } + } + _ => (), + } + + None + } +} diff --git a/rio-window/src/platform_impl/windows/mod.rs b/rio-window/src/platform_impl/windows/mod.rs new file mode 100644 index 00000000..903545d9 --- /dev/null +++ b/rio-window/src/platform_impl/windows/mod.rs @@ -0,0 +1,209 @@ +#![cfg(windows_platform)] + +use smol_str::SmolStr; +use windows_sys::Win32::Foundation::{HANDLE, HWND}; +use windows_sys::Win32::UI::WindowsAndMessaging::{HMENU, WINDOW_LONG_PTR_INDEX}; + +pub(crate) use self::event_loop::{ + ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle, + PlatformSpecificEventLoopAttributes, +}; +pub(crate) use self::icon::{SelectedCursor, WinIcon}; +pub(crate) use self::keyboard::{physicalkey_to_scancode, scancode_to_physicalkey}; +pub(crate) use self::monitor::{MonitorHandle, VideoModeHandle}; +pub(crate) use self::window::Window; + +pub(crate) use self::icon::WinCursor as PlatformCustomCursor; +pub use self::icon::WinIcon as PlatformIcon; +pub(crate) use crate::cursor::OnlyCursorImageSource as PlatformCustomCursorSource; +use crate::platform_impl::Fullscreen; + +use crate::event::DeviceId as RootDeviceId; +use crate::icon::Icon; +use crate::keyboard::Key; +use crate::platform::windows::{BackdropType, Color, CornerPreference}; + +#[derive(Clone, Debug)] +pub struct PlatformSpecificWindowAttributes { + pub owner: Option, + pub menu: Option, + pub taskbar_icon: Option, + pub no_redirection_bitmap: bool, + pub drag_and_drop: bool, + pub skip_taskbar: bool, + pub class_name: String, + pub decoration_shadow: bool, + pub backdrop_type: BackdropType, + pub clip_children: bool, + pub border_color: Option, + pub title_background_color: Option, + pub title_text_color: Option, + pub corner_preference: Option, +} + +impl Default for PlatformSpecificWindowAttributes { + fn default() -> Self { + Self { + owner: None, + menu: None, + taskbar_icon: None, + no_redirection_bitmap: false, + drag_and_drop: true, + skip_taskbar: false, + class_name: "Window Class".to_string(), + decoration_shadow: false, + backdrop_type: BackdropType::default(), + clip_children: true, + border_color: None, + title_background_color: None, + title_text_color: None, + corner_preference: None, + } + } +} + +unsafe impl Send for PlatformSpecificWindowAttributes {} +unsafe impl Sync for PlatformSpecificWindowAttributes {} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(u32); + +impl DeviceId { + pub const unsafe fn dummy() -> Self { + DeviceId(0) + } +} + +impl DeviceId { + pub fn persistent_identifier(&self) -> Option { + if self.0 != 0 { + raw_input::get_raw_input_device_name(self.0 as HANDLE) + } else { + None + } + } +} + +// Constant device ID, to be removed when this backend is updated to report real device IDs. +const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId(0)); + +fn wrap_device_id(id: u32) -> RootDeviceId { + RootDeviceId(DeviceId(id)) +} + +pub type OsError = std::io::Error; + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifiers: Option, + pub key_without_modifiers: Key, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(HWND); +unsafe impl Send for WindowId {} +unsafe impl Sync for WindowId {} + +impl WindowId { + pub const unsafe fn dummy() -> Self { + WindowId(0) + } +} + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 as u64 + } +} + +impl From for HWND { + fn from(window_id: WindowId) -> Self { + window_id.0 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id as HWND) + } +} + +#[inline(always)] +const fn get_xbutton_wparam(x: u32) -> u16 { + hiword(x) +} + +#[inline(always)] +const fn get_x_lparam(x: u32) -> i16 { + loword(x) as _ +} + +#[inline(always)] +const fn get_y_lparam(x: u32) -> i16 { + hiword(x) as _ +} + +#[inline(always)] +pub(crate) const fn primarylangid(lgid: u16) -> u16 { + lgid & 0x3ff +} + +#[inline(always)] +pub(crate) const fn loword(x: u32) -> u16 { + (x & 0xffff) as u16 +} + +#[inline(always)] +const fn hiword(x: u32) -> u16 { + ((x >> 16) & 0xffff) as u16 +} + +#[inline(always)] +unsafe fn get_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX) -> isize { + #[cfg(target_pointer_width = "64")] + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW(hwnd, nindex) + }; + #[cfg(target_pointer_width = "32")] + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::GetWindowLongW(hwnd, nindex) as isize + }; +} + +#[inline(always)] +unsafe fn set_window_long( + hwnd: HWND, + nindex: WINDOW_LONG_PTR_INDEX, + dwnewlong: isize, +) -> isize { + #[cfg(target_pointer_width = "64")] + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW( + hwnd, nindex, dwnewlong, + ) + }; + #[cfg(target_pointer_width = "32")] + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::SetWindowLongW( + hwnd, + nindex, + dwnewlong as i32, + ) as isize + }; +} + +#[macro_use] +mod util; +mod dark_mode; +mod definitions; +mod dpi; +mod drop_handler; +mod event_loop; +mod icon; +mod ime; +mod keyboard; +mod keyboard_layout; +mod monitor; +mod raw_input; +mod window; +mod window_state; diff --git a/rio-window/src/platform_impl/windows/monitor.rs b/rio-window/src/platform_impl/windows/monitor.rs new file mode 100644 index 00000000..c05a64c9 --- /dev/null +++ b/rio-window/src/platform_impl/windows/monitor.rs @@ -0,0 +1,268 @@ +use std::collections::{BTreeSet, VecDeque}; +use std::hash::Hash; +use std::{io, mem, ptr}; + +use windows_sys::Win32::Foundation::{BOOL, HWND, LPARAM, POINT, RECT}; +use windows_sys::Win32::Graphics::Gdi::{ + EnumDisplayMonitors, EnumDisplaySettingsExW, GetMonitorInfoW, MonitorFromPoint, + MonitorFromWindow, DEVMODEW, DM_BITSPERPEL, DM_DISPLAYFREQUENCY, DM_PELSHEIGHT, + DM_PELSWIDTH, ENUM_CURRENT_SETTINGS, HDC, HMONITOR, MONITORINFO, MONITORINFOEXW, + MONITOR_DEFAULTTONEAREST, MONITOR_DEFAULTTOPRIMARY, +}; + +use super::util::decode_wide; +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::monitor::VideoModeHandle as RootVideoModeHandle; +use crate::platform_impl::platform::dpi::{dpi_to_scale_factor, get_monitor_dpi}; +use crate::platform_impl::platform::util::has_flag; +use crate::platform_impl::platform::window::Window; + +#[derive(Clone)] +pub struct VideoModeHandle { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate_millihertz: u32, + pub(crate) monitor: MonitorHandle, + // DEVMODEW is huge so we box it to avoid blowing up the size of winit::window::Fullscreen + pub(crate) native_video_mode: Box, +} + +impl PartialEq for VideoModeHandle { + fn eq(&self, other: &Self) -> bool { + self.size == other.size + && self.bit_depth == other.bit_depth + && self.refresh_rate_millihertz == other.refresh_rate_millihertz + && self.monitor == other.monitor + } +} + +impl Eq for VideoModeHandle {} + +impl std::hash::Hash for VideoModeHandle { + fn hash(&self, state: &mut H) { + self.size.hash(state); + self.bit_depth.hash(state); + self.refresh_rate_millihertz.hash(state); + self.monitor.hash(state); + } +} + +impl std::fmt::Debug for VideoModeHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VideoModeHandle") + .field("size", &self.size) + .field("bit_depth", &self.bit_depth) + .field("refresh_rate_millihertz", &self.refresh_rate_millihertz) + .field("monitor", &self.monitor) + .finish() + } +} + +impl VideoModeHandle { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)] +pub struct MonitorHandle(HMONITOR); + +// Send is not implemented for HMONITOR, we have to wrap it and implement it manually. +// For more info see: +// https://github.com/retep998/winapi-rs/issues/360 +// https://github.com/retep998/winapi-rs/issues/396 + +unsafe impl Send for MonitorHandle {} + +unsafe extern "system" fn monitor_enum_proc( + hmonitor: HMONITOR, + _hdc: HDC, + _place: *mut RECT, + data: LPARAM, +) -> BOOL { + let monitors = data as *mut VecDeque; + unsafe { (*monitors).push_back(MonitorHandle::new(hmonitor)) }; + true.into() // continue enumeration +} + +pub fn available_monitors() -> VecDeque { + let mut monitors: VecDeque = VecDeque::new(); + unsafe { + EnumDisplayMonitors( + 0, + ptr::null(), + Some(monitor_enum_proc), + &mut monitors as *mut _ as LPARAM, + ); + } + monitors +} + +pub fn primary_monitor() -> MonitorHandle { + const ORIGIN: POINT = POINT { x: 0, y: 0 }; + let hmonitor = unsafe { MonitorFromPoint(ORIGIN, MONITOR_DEFAULTTOPRIMARY) }; + MonitorHandle::new(hmonitor) +} + +pub fn current_monitor(hwnd: HWND) -> MonitorHandle { + let hmonitor = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }; + MonitorHandle::new(hmonitor) +} + +impl Window { + pub fn available_monitors(&self) -> VecDeque { + available_monitors() + } + + pub fn primary_monitor(&self) -> Option { + let monitor = primary_monitor(); + Some(monitor) + } +} + +pub(crate) fn get_monitor_info(hmonitor: HMONITOR) -> Result { + let mut monitor_info: MONITORINFOEXW = unsafe { mem::zeroed() }; + monitor_info.monitorInfo.cbSize = mem::size_of::() as u32; + let status = unsafe { + GetMonitorInfoW( + hmonitor, + &mut monitor_info as *mut MONITORINFOEXW as *mut MONITORINFO, + ) + }; + if status == false.into() { + Err(io::Error::last_os_error()) + } else { + Ok(monitor_info) + } +} + +impl MonitorHandle { + pub(crate) fn new(hmonitor: HMONITOR) -> Self { + MonitorHandle(hmonitor) + } + + #[inline] + pub fn name(&self) -> Option { + let monitor_info = get_monitor_info(self.0).unwrap(); + Some( + decode_wide(&monitor_info.szDevice) + .to_string_lossy() + .to_string(), + ) + } + + #[inline] + pub fn native_identifier(&self) -> String { + self.name().unwrap() + } + + #[inline] + pub fn hmonitor(&self) -> HMONITOR { + self.0 + } + + #[inline] + pub fn size(&self) -> PhysicalSize { + let rc_monitor = get_monitor_info(self.0).unwrap().monitorInfo.rcMonitor; + PhysicalSize { + width: (rc_monitor.right - rc_monitor.left) as u32, + height: (rc_monitor.bottom - rc_monitor.top) as u32, + } + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + let monitor_info = get_monitor_info(self.0).ok()?; + let device_name = monitor_info.szDevice.as_ptr(); + unsafe { + let mut mode: DEVMODEW = mem::zeroed(); + mode.dmSize = mem::size_of_val(&mode) as u16; + if EnumDisplaySettingsExW(device_name, ENUM_CURRENT_SETTINGS, &mut mode, 0) + == false.into() + { + None + } else { + Some(mode.dmDisplayFrequency * 1000) + } + } + } + + #[inline] + pub fn position(&self) -> PhysicalPosition { + get_monitor_info(self.0) + .map(|info| { + let rc_monitor = info.monitorInfo.rcMonitor; + PhysicalPosition { + x: rc_monitor.left, + y: rc_monitor.top, + } + }) + .unwrap_or(PhysicalPosition { x: 0, y: 0 }) + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + dpi_to_scale_factor(get_monitor_dpi(self.0).unwrap_or(96)) + } + + #[inline] + pub fn video_modes(&self) -> impl Iterator { + // EnumDisplaySettingsExW can return duplicate values (or some of the + // fields are probably changing, but we aren't looking at those fields + // anyway), so we're using a BTreeSet deduplicate + let mut modes = BTreeSet::::new(); + let mod_map = |mode: RootVideoModeHandle| mode.video_mode; + + let monitor_info = match get_monitor_info(self.0) { + Ok(monitor_info) => monitor_info, + Err(error) => { + tracing::warn!("Error from get_monitor_info: {error}"); + return modes.into_iter().map(mod_map); + } + }; + + let device_name = monitor_info.szDevice.as_ptr(); + + let mut i = 0; + loop { + let mut mode: DEVMODEW = unsafe { mem::zeroed() }; + mode.dmSize = mem::size_of_val(&mode) as u16; + if unsafe { EnumDisplaySettingsExW(device_name, i, &mut mode, 0) } + == false.into() + { + break; + } + + const REQUIRED_FIELDS: u32 = + DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY; + assert!(has_flag(mode.dmFields, REQUIRED_FIELDS)); + + // Use Ord impl of RootVideoModeHandle + modes.insert(RootVideoModeHandle { + video_mode: VideoModeHandle { + size: (mode.dmPelsWidth, mode.dmPelsHeight), + bit_depth: mode.dmBitsPerPel as u16, + refresh_rate_millihertz: mode.dmDisplayFrequency * 1000, + monitor: self.clone(), + native_video_mode: Box::new(mode), + }, + }); + + i += 1; + } + + modes.into_iter().map(mod_map) + } +} diff --git a/rio-window/src/platform_impl/windows/raw_input.rs b/rio-window/src/platform_impl/windows/raw_input.rs new file mode 100644 index 00000000..e206cbc6 --- /dev/null +++ b/rio-window/src/platform_impl/windows/raw_input.rs @@ -0,0 +1,349 @@ +use std::mem::{self, size_of}; +use std::ptr; + +use windows_sys::Win32::Devices::HumanInterfaceDevice::{ + HID_USAGE_GENERIC_KEYBOARD, HID_USAGE_GENERIC_MOUSE, HID_USAGE_PAGE_GENERIC, +}; +use windows_sys::Win32::Foundation::{HANDLE, HWND}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + MapVirtualKeyW, MAPVK_VK_TO_VSC_EX, VK_NUMLOCK, VK_SHIFT, +}; +use windows_sys::Win32::UI::Input::{ + GetRawInputData, GetRawInputDeviceInfoW, GetRawInputDeviceList, + RegisterRawInputDevices, HRAWINPUT, RAWINPUT, RAWINPUTDEVICE, RAWINPUTDEVICELIST, + RAWINPUTHEADER, RAWKEYBOARD, RIDEV_DEVNOTIFY, RIDEV_INPUTSINK, RIDEV_REMOVE, + RIDI_DEVICEINFO, RIDI_DEVICENAME, RID_DEVICE_INFO, RID_DEVICE_INFO_HID, + RID_DEVICE_INFO_KEYBOARD, RID_DEVICE_INFO_MOUSE, RID_INPUT, RIM_TYPEHID, + RIM_TYPEKEYBOARD, RIM_TYPEMOUSE, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + RI_KEY_E0, RI_KEY_E1, RI_MOUSE_BUTTON_1_DOWN, RI_MOUSE_BUTTON_1_UP, + RI_MOUSE_BUTTON_2_DOWN, RI_MOUSE_BUTTON_2_UP, RI_MOUSE_BUTTON_3_DOWN, + RI_MOUSE_BUTTON_3_UP, RI_MOUSE_BUTTON_4_DOWN, RI_MOUSE_BUTTON_4_UP, + RI_MOUSE_BUTTON_5_DOWN, RI_MOUSE_BUTTON_5_UP, +}; + +use super::scancode_to_physicalkey; +use crate::event::ElementState; +use crate::event_loop::DeviceEvents; +use crate::keyboard::{KeyCode, PhysicalKey}; +use crate::platform_impl::platform::util; + +#[allow(dead_code)] +pub fn get_raw_input_device_list() -> Option> { + let list_size = size_of::() as u32; + + let mut num_devices = 0; + let status = + unsafe { GetRawInputDeviceList(ptr::null_mut(), &mut num_devices, list_size) }; + + if status == u32::MAX { + return None; + } + + let mut buffer = Vec::with_capacity(num_devices as _); + + let num_stored = unsafe { + GetRawInputDeviceList(buffer.as_mut_ptr(), &mut num_devices, list_size) + }; + + if num_stored == u32::MAX { + return None; + } + + debug_assert_eq!(num_devices, num_stored); + + unsafe { buffer.set_len(num_devices as _) }; + + Some(buffer) +} + +#[allow(dead_code)] +pub enum RawDeviceInfo { + Mouse(RID_DEVICE_INFO_MOUSE), + Keyboard(RID_DEVICE_INFO_KEYBOARD), + Hid(RID_DEVICE_INFO_HID), +} + +impl From for RawDeviceInfo { + fn from(info: RID_DEVICE_INFO) -> Self { + unsafe { + match info.dwType { + RIM_TYPEMOUSE => RawDeviceInfo::Mouse(info.Anonymous.mouse), + RIM_TYPEKEYBOARD => RawDeviceInfo::Keyboard(info.Anonymous.keyboard), + RIM_TYPEHID => RawDeviceInfo::Hid(info.Anonymous.hid), + _ => unreachable!(), + } + } + } +} + +#[allow(dead_code)] +pub fn get_raw_input_device_info(handle: HANDLE) -> Option { + let mut info: RID_DEVICE_INFO = unsafe { mem::zeroed() }; + let info_size = size_of::() as u32; + + info.cbSize = info_size; + + let mut minimum_size = 0; + let status = unsafe { + GetRawInputDeviceInfoW( + handle, + RIDI_DEVICEINFO, + &mut info as *mut _ as _, + &mut minimum_size, + ) + }; + + if status == u32::MAX || status == 0 { + return None; + } + + debug_assert_eq!(info_size, status); + + Some(info.into()) +} + +pub fn get_raw_input_device_name(handle: HANDLE) -> Option { + let mut minimum_size = 0; + let status = unsafe { + GetRawInputDeviceInfoW( + handle, + RIDI_DEVICENAME, + ptr::null_mut(), + &mut minimum_size, + ) + }; + + if status != 0 { + return None; + } + + let mut name: Vec = Vec::with_capacity(minimum_size as _); + + let status = unsafe { + GetRawInputDeviceInfoW( + handle, + RIDI_DEVICENAME, + name.as_ptr() as _, + &mut minimum_size, + ) + }; + + if status == u32::MAX || status == 0 { + return None; + } + + debug_assert_eq!(minimum_size, status); + + unsafe { name.set_len(minimum_size as _) }; + + util::decode_wide(&name).into_string().ok() +} + +pub fn register_raw_input_devices(devices: &[RAWINPUTDEVICE]) -> bool { + let device_size = size_of::() as u32; + + unsafe { + RegisterRawInputDevices(devices.as_ptr(), devices.len() as u32, device_size) + == true.into() + } +} + +pub fn register_all_mice_and_keyboards_for_raw_input( + mut window_handle: HWND, + filter: DeviceEvents, +) -> bool { + // RIDEV_DEVNOTIFY: receive hotplug events + // RIDEV_INPUTSINK: receive events even if we're not in the foreground + // RIDEV_REMOVE: don't receive device events (requires NULL hwndTarget) + let flags = match filter { + DeviceEvents::Never => { + window_handle = 0; + RIDEV_REMOVE + } + DeviceEvents::WhenFocused => RIDEV_DEVNOTIFY, + DeviceEvents::Always => RIDEV_DEVNOTIFY | RIDEV_INPUTSINK, + }; + + let devices: [RAWINPUTDEVICE; 2] = [ + RAWINPUTDEVICE { + usUsagePage: HID_USAGE_PAGE_GENERIC, + usUsage: HID_USAGE_GENERIC_MOUSE, + dwFlags: flags, + hwndTarget: window_handle, + }, + RAWINPUTDEVICE { + usUsagePage: HID_USAGE_PAGE_GENERIC, + usUsage: HID_USAGE_GENERIC_KEYBOARD, + dwFlags: flags, + hwndTarget: window_handle, + }, + ]; + + register_raw_input_devices(&devices) +} + +pub fn get_raw_input_data(handle: HRAWINPUT) -> Option { + let mut data: RAWINPUT = unsafe { mem::zeroed() }; + let mut data_size = size_of::() as u32; + let header_size = size_of::() as u32; + + let status = unsafe { + GetRawInputData( + handle, + RID_INPUT, + &mut data as *mut _ as _, + &mut data_size, + header_size, + ) + }; + + if status == u32::MAX || status == 0 { + return None; + } + + Some(data) +} + +fn button_flags_to_element_state( + button_flags: u32, + down_flag: u32, + up_flag: u32, +) -> Option { + // We assume the same button won't be simultaneously pressed and released. + if util::has_flag(button_flags, down_flag) { + Some(ElementState::Pressed) + } else if util::has_flag(button_flags, up_flag) { + Some(ElementState::Released) + } else { + None + } +} + +pub fn get_raw_mouse_button_state(button_flags: u32) -> [Option; 5] { + [ + button_flags_to_element_state( + button_flags, + RI_MOUSE_BUTTON_1_DOWN, + RI_MOUSE_BUTTON_1_UP, + ), + button_flags_to_element_state( + button_flags, + RI_MOUSE_BUTTON_2_DOWN, + RI_MOUSE_BUTTON_2_UP, + ), + button_flags_to_element_state( + button_flags, + RI_MOUSE_BUTTON_3_DOWN, + RI_MOUSE_BUTTON_3_UP, + ), + button_flags_to_element_state( + button_flags, + RI_MOUSE_BUTTON_4_DOWN, + RI_MOUSE_BUTTON_4_UP, + ), + button_flags_to_element_state( + button_flags, + RI_MOUSE_BUTTON_5_DOWN, + RI_MOUSE_BUTTON_5_UP, + ), + ] +} + +pub fn get_keyboard_physical_key(keyboard: RAWKEYBOARD) -> Option { + let extension = { + if util::has_flag(keyboard.Flags, RI_KEY_E0 as _) { + 0xe000 + } else if util::has_flag(keyboard.Flags, RI_KEY_E1 as _) { + 0xe100 + } else { + 0x0000 + } + }; + let scancode = if keyboard.MakeCode == 0 { + // In some cases (often with media keys) the device reports a scancode of 0 but a + // valid virtual key. In these cases we obtain the scancode from the virtual key. + unsafe { MapVirtualKeyW(keyboard.VKey as u32, MAPVK_VK_TO_VSC_EX) as u16 } + } else { + keyboard.MakeCode | extension + }; + if scancode == 0xe11d || scancode == 0xe02a { + // At the hardware (or driver?) level, pressing the Pause key is equivalent to pressing + // Ctrl+NumLock. + // This equvalence means that if the user presses Pause, the keyboard will emit two + // subsequent keypresses: + // 1, 0xE11D - Which is a left Ctrl (0x1D) with an extension flag (0xE100) + // 2, 0x0045 - Which on its own can be interpreted as Pause + // + // There's another combination which isn't quite an equivalence: + // PrtSc used to be Shift+Asterisk. This means that on some keyboards, presssing + // PrtSc (print screen) produces the following sequence: + // 1, 0xE02A - Which is a left shift (0x2A) with an extension flag (0xE000) + // 2, 0xE037 - Which is a numpad multiply (0x37) with an exteion flag (0xE000). This on + // its own it can be interpreted as PrtSc + // + // For this reason, if we encounter the first keypress, we simply ignore it, trusting + // that there's going to be another event coming, from which we can extract the + // appropriate key. + // For more on this, read the article by Raymond Chen, titled: + // "Why does Ctrl+ScrollLock cancel dialogs?" + // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 + return None; + } + let physical_key = if keyboard.VKey == VK_NUMLOCK { + // Historically, the NumLock and the Pause key were one and the same physical key. + // The user could trigger Pause by pressing Ctrl+NumLock. + // Now these are often physically separate and the two keys can be differentiated by + // checking the extension flag of the scancode. NumLock is 0xE045, Pause is 0x0045. + // + // However in this event, both keys are reported as 0x0045 even on modern hardware. + // Therefore we use the virtual key instead to determine whether it's a NumLock and + // set the KeyCode accordingly. + // + // For more on this, read the article by Raymond Chen, titled: + // "Why does Ctrl+ScrollLock cancel dialogs?" + // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 + PhysicalKey::Code(KeyCode::NumLock) + } else { + scancode_to_physicalkey(scancode as u32) + }; + if keyboard.VKey == VK_SHIFT { + if let PhysicalKey::Code(code) = physical_key { + match code { + KeyCode::NumpadDecimal + | KeyCode::Numpad0 + | KeyCode::Numpad1 + | KeyCode::Numpad2 + | KeyCode::Numpad3 + | KeyCode::Numpad4 + | KeyCode::Numpad5 + | KeyCode::Numpad6 + | KeyCode::Numpad7 + | KeyCode::Numpad8 + | KeyCode::Numpad9 => { + // On Windows, holding the Shift key makes numpad keys behave as if NumLock + // wasn't active. The way this is exposed to applications by the system is that + // the application receives a fake key release event for the shift key at the + // moment when the numpad key is pressed, just before receiving the numpad key + // as well. + // + // The issue is that in the raw device event (here), the fake shift release + // event reports the numpad key as the scancode. Unfortunately, the event + // doesn't have any information to tell whether it's the + // left shift or the right shift that needs to get the fake + // release (or press) event so we don't forward this + // event to the application at all. + // + // For more on this, read the article by Raymond Chen, titled: + // "The shift key overrides NumLock" + // https://devblogs.microsoft.com/oldnewthing/20040906-00/?p=37953 + return None; + } + _ => (), + } + } + } + + Some(physical_key) +} diff --git a/rio-window/src/platform_impl/windows/util.rs b/rio-window/src/platform_impl/windows/util.rs new file mode 100644 index 00000000..71fd3c69 --- /dev/null +++ b/rio-window/src/platform_impl/windows/util.rs @@ -0,0 +1,286 @@ +use std::ffi::{c_void, OsStr, OsString}; +use std::iter::once; +use std::ops::BitAnd; +use std::os::windows::prelude::{OsStrExt, OsStringExt}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::{io, mem, ptr}; + +use crate::utils::Lazy; +use windows_sys::core::{HRESULT, PCWSTR}; +use windows_sys::Win32::Foundation::{BOOL, HANDLE, HMODULE, HWND, RECT}; +use windows_sys::Win32::Graphics::Gdi::{ClientToScreen, HMONITOR}; +use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA}; +use windows_sys::Win32::System::SystemServices::IMAGE_DOS_HEADER; +use windows_sys::Win32::UI::HiDpi::{ + DPI_AWARENESS_CONTEXT, MONITOR_DPI_TYPE, PROCESS_DPI_AWARENESS, +}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetActiveWindow; +use windows_sys::Win32::UI::Input::Pointer::{ + POINTER_INFO, POINTER_PEN_INFO, POINTER_TOUCH_INFO, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + ClipCursor, GetClientRect, GetClipCursor, GetSystemMetrics, GetWindowPlacement, + GetWindowRect, IsIconic, ShowCursor, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, + IDC_HELP, IDC_IBEAM, IDC_NO, IDC_SIZEALL, IDC_SIZENESW, IDC_SIZENS, IDC_SIZENWSE, + IDC_SIZEWE, IDC_WAIT, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, + SM_YVIRTUALSCREEN, SW_MAXIMIZE, WINDOWPLACEMENT, +}; + +use crate::window::CursorIcon; + +pub fn encode_wide(string: impl AsRef) -> Vec { + string.as_ref().encode_wide().chain(once(0)).collect() +} + +pub fn decode_wide(mut wide_c_string: &[u16]) -> OsString { + if let Some(null_pos) = wide_c_string.iter().position(|c| *c == 0) { + wide_c_string = &wide_c_string[..null_pos]; + } + + OsString::from_wide(wide_c_string) +} + +pub fn has_flag(bitset: T, flag: T) -> bool +where + T: Copy + PartialEq + BitAnd, +{ + bitset & flag == flag +} + +pub(crate) fn win_to_err(result: BOOL) -> Result<(), io::Error> { + if result != false.into() { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } +} + +pub enum WindowArea { + Outer, + Inner, +} + +impl WindowArea { + pub fn get_rect(self, hwnd: HWND) -> Result { + let mut rect = unsafe { mem::zeroed() }; + + match self { + WindowArea::Outer => { + win_to_err(unsafe { GetWindowRect(hwnd, &mut rect) })?; + } + WindowArea::Inner => unsafe { + let mut top_left = mem::zeroed(); + + win_to_err(ClientToScreen(hwnd, &mut top_left))?; + win_to_err(GetClientRect(hwnd, &mut rect))?; + rect.left += top_left.x; + rect.top += top_left.y; + rect.right += top_left.x; + rect.bottom += top_left.y; + }, + } + + Ok(rect) + } +} + +pub fn is_maximized(window: HWND) -> bool { + unsafe { + let mut placement: WINDOWPLACEMENT = mem::zeroed(); + placement.length = mem::size_of::() as u32; + GetWindowPlacement(window, &mut placement); + placement.showCmd == SW_MAXIMIZE as u32 + } +} + +pub fn set_cursor_hidden(hidden: bool) { + static HIDDEN: AtomicBool = AtomicBool::new(false); + let changed = HIDDEN.swap(hidden, Ordering::SeqCst) ^ hidden; + if changed { + unsafe { ShowCursor(BOOL::from(!hidden)) }; + } +} + +pub fn get_cursor_clip() -> Result { + unsafe { + let mut rect: RECT = mem::zeroed(); + win_to_err(GetClipCursor(&mut rect)).map(|_| rect) + } +} + +/// Sets the cursor's clip rect. +/// +/// Note that calling this will automatically dispatch a `WM_MOUSEMOVE` event. +pub fn set_cursor_clip(rect: Option) -> Result<(), io::Error> { + unsafe { + let rect_ptr = rect + .as_ref() + .map(|r| r as *const RECT) + .unwrap_or(ptr::null()); + win_to_err(ClipCursor(rect_ptr)) + } +} + +pub fn get_desktop_rect() -> RECT { + unsafe { + let left = GetSystemMetrics(SM_XVIRTUALSCREEN); + let top = GetSystemMetrics(SM_YVIRTUALSCREEN); + RECT { + left, + top, + right: left + GetSystemMetrics(SM_CXVIRTUALSCREEN), + bottom: top + GetSystemMetrics(SM_CYVIRTUALSCREEN), + } + } +} + +pub fn is_focused(window: HWND) -> bool { + window == unsafe { GetActiveWindow() } +} + +pub fn is_minimized(window: HWND) -> bool { + unsafe { IsIconic(window) != false.into() } +} + +pub fn get_instance_handle() -> HMODULE { + // Gets the instance handle by taking the address of the + // pseudo-variable created by the microsoft linker: + // https://devblogs.microsoft.com/oldnewthing/20041025-00/?p=37483 + + // This is preferred over GetModuleHandle(NULL) because it also works in DLLs: + // https://stackoverflow.com/questions/21718027/getmodulehandlenull-vs-hinstance + + extern "C" { + static __ImageBase: IMAGE_DOS_HEADER; + } + + unsafe { &__ImageBase as *const _ as _ } +} + +pub(crate) fn to_windows_cursor(cursor: CursorIcon) -> PCWSTR { + match cursor { + CursorIcon::Default => IDC_ARROW, + CursorIcon::Pointer => IDC_HAND, + CursorIcon::Crosshair => IDC_CROSS, + CursorIcon::Text | CursorIcon::VerticalText => IDC_IBEAM, + CursorIcon::NotAllowed | CursorIcon::NoDrop => IDC_NO, + CursorIcon::Grab + | CursorIcon::Grabbing + | CursorIcon::Move + | CursorIcon::AllScroll => IDC_SIZEALL, + CursorIcon::EResize + | CursorIcon::WResize + | CursorIcon::EwResize + | CursorIcon::ColResize => IDC_SIZEWE, + CursorIcon::NResize + | CursorIcon::SResize + | CursorIcon::NsResize + | CursorIcon::RowResize => IDC_SIZENS, + CursorIcon::NeResize | CursorIcon::SwResize | CursorIcon::NeswResize => { + IDC_SIZENESW + } + CursorIcon::NwResize | CursorIcon::SeResize | CursorIcon::NwseResize => { + IDC_SIZENWSE + } + CursorIcon::Wait => IDC_WAIT, + CursorIcon::Progress => IDC_APPSTARTING, + CursorIcon::Help => IDC_HELP, + _ => IDC_ARROW, // use arrow for the missing cases. + } +} + +// Helper function to dynamically load function pointer as some functions +// may not be available on all Windows platforms supported by winit. +// +// `library` and `function` must be zero-terminated. +pub(super) fn get_function_impl(library: &str, function: &str) -> Option<*const c_void> { + assert_eq!(library.chars().last(), Some('\0')); + assert_eq!(function.chars().last(), Some('\0')); + + // Library names we will use are ASCII so we can use the A version to avoid string conversion. + let module = unsafe { LoadLibraryA(library.as_ptr()) }; + if module == 0 { + return None; + } + + unsafe { GetProcAddress(module, function.as_ptr()) } + .map(|function_ptr| function_ptr as _) +} + +macro_rules! get_function { + ($lib:expr, $func:ident) => { + crate::platform_impl::platform::util::get_function_impl( + concat!($lib, '\0'), + concat!(stringify!($func), '\0'), + ) + .map(|f| unsafe { std::mem::transmute::<*const _, $func>(f) }) + }; +} + +pub type SetProcessDPIAware = unsafe extern "system" fn() -> BOOL; +pub type SetProcessDpiAwareness = + unsafe extern "system" fn(value: PROCESS_DPI_AWARENESS) -> HRESULT; +pub type SetProcessDpiAwarenessContext = + unsafe extern "system" fn(value: DPI_AWARENESS_CONTEXT) -> BOOL; +pub type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32; +pub type GetDpiForMonitor = unsafe extern "system" fn( + hmonitor: HMONITOR, + dpi_type: MONITOR_DPI_TYPE, + dpi_x: *mut u32, + dpi_y: *mut u32, +) -> HRESULT; +pub type EnableNonClientDpiScaling = unsafe extern "system" fn(hwnd: HWND) -> BOOL; +pub type AdjustWindowRectExForDpi = unsafe extern "system" fn( + rect: *mut RECT, + dwStyle: u32, + bMenu: BOOL, + dwExStyle: u32, + dpi: u32, +) -> BOOL; + +pub type GetPointerFrameInfoHistory = unsafe extern "system" fn( + pointerId: u32, + entriesCount: *mut u32, + pointerCount: *mut u32, + pointerInfo: *mut POINTER_INFO, +) -> BOOL; + +pub type SkipPointerFrameMessages = unsafe extern "system" fn(pointerId: u32) -> BOOL; +pub type GetPointerDeviceRects = unsafe extern "system" fn( + device: HANDLE, + pointerDeviceRect: *mut RECT, + displayRect: *mut RECT, +) -> BOOL; + +pub type GetPointerTouchInfo = + unsafe extern "system" fn(pointerId: u32, touchInfo: *mut POINTER_TOUCH_INFO) -> BOOL; + +pub type GetPointerPenInfo = + unsafe extern "system" fn(pointId: u32, penInfo: *mut POINTER_PEN_INFO) -> BOOL; + +pub(crate) static GET_DPI_FOR_WINDOW: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetDpiForWindow)); +pub(crate) static ADJUST_WINDOW_RECT_EX_FOR_DPI: Lazy> = + Lazy::new(|| get_function!("user32.dll", AdjustWindowRectExForDpi)); +pub(crate) static GET_DPI_FOR_MONITOR: Lazy> = + Lazy::new(|| get_function!("shcore.dll", GetDpiForMonitor)); +pub(crate) static ENABLE_NON_CLIENT_DPI_SCALING: Lazy> = + Lazy::new(|| get_function!("user32.dll", EnableNonClientDpiScaling)); +pub(crate) static SET_PROCESS_DPI_AWARENESS_CONTEXT: Lazy< + Option, +> = Lazy::new(|| get_function!("user32.dll", SetProcessDpiAwarenessContext)); +pub(crate) static SET_PROCESS_DPI_AWARENESS: Lazy> = + Lazy::new(|| get_function!("shcore.dll", SetProcessDpiAwareness)); +pub(crate) static SET_PROCESS_DPI_AWARE: Lazy> = + Lazy::new(|| get_function!("user32.dll", SetProcessDPIAware)); +pub(crate) static GET_POINTER_FRAME_INFO_HISTORY: Lazy< + Option, +> = Lazy::new(|| get_function!("user32.dll", GetPointerFrameInfoHistory)); +pub(crate) static SKIP_POINTER_FRAME_MESSAGES: Lazy> = + Lazy::new(|| get_function!("user32.dll", SkipPointerFrameMessages)); +pub(crate) static GET_POINTER_DEVICE_RECTS: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerDeviceRects)); +pub(crate) static GET_POINTER_TOUCH_INFO: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerTouchInfo)); +pub(crate) static GET_POINTER_PEN_INFO: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerPenInfo)); diff --git a/rio-window/src/platform_impl/windows/window.rs b/rio-window/src/platform_impl/windows/window.rs new file mode 100644 index 00000000..38007922 --- /dev/null +++ b/rio-window/src/platform_impl/windows/window.rs @@ -0,0 +1,1658 @@ +#![cfg(windows_platform)] + +use std::cell::Cell; +use std::ffi::c_void; +use std::mem::{self, MaybeUninit}; +use std::sync::mpsc::channel; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::{io, panic, ptr}; + +use windows_sys::Win32::Foundation::{ + HWND, LPARAM, OLE_E_WRONGCOMPOBJ, POINT, POINTS, RECT, RPC_E_CHANGED_MODE, S_OK, + WPARAM, +}; +use windows_sys::Win32::Graphics::Dwm::{ + DwmEnableBlurBehindWindow, DwmSetWindowAttribute, DWMWA_BORDER_COLOR, + DWMWA_CAPTION_COLOR, DWMWA_SYSTEMBACKDROP_TYPE, DWMWA_TEXT_COLOR, + DWMWA_WINDOW_CORNER_PREFERENCE, DWM_BB_BLURREGION, DWM_BB_ENABLE, DWM_BLURBEHIND, + DWM_SYSTEMBACKDROP_TYPE, DWM_WINDOW_CORNER_PREFERENCE, +}; +use windows_sys::Win32::Graphics::Gdi::{ + ChangeDisplaySettingsExW, ClientToScreen, CreateRectRgn, DeleteObject, InvalidateRgn, + RedrawWindow, CDS_FULLSCREEN, DISP_CHANGE_BADFLAGS, DISP_CHANGE_BADMODE, + DISP_CHANGE_BADPARAM, DISP_CHANGE_FAILED, DISP_CHANGE_SUCCESSFUL, RDW_INTERNALPAINT, +}; +use windows_sys::Win32::System::Com::{ + CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, + COINIT_APARTMENTTHREADED, +}; +use windows_sys::Win32::System::Ole::{OleInitialize, RegisterDragDrop}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + EnableWindow, GetActiveWindow, MapVirtualKeyW, ReleaseCapture, SendInput, ToUnicode, + INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP, + MAPVK_VK_TO_VSC, VIRTUAL_KEY, VK_LMENU, VK_MENU, VK_SPACE, +}; +use windows_sys::Win32::UI::Input::Touch::{RegisterTouchWindow, TWF_WANTPALM}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + CreateWindowExW, EnableMenuItem, FlashWindowEx, GetClientRect, GetCursorPos, + GetForegroundWindow, GetSystemMenu, GetSystemMetrics, GetWindowPlacement, + GetWindowTextLengthW, GetWindowTextW, IsWindowVisible, LoadCursorW, PeekMessageW, + PostMessageW, RegisterClassExW, SetCursor, SetCursorPos, SetForegroundWindow, + SetMenuDefaultItem, SetWindowDisplayAffinity, SetWindowPlacement, SetWindowPos, + SetWindowTextW, TrackPopupMenu, CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, FLASHWINFO, + FLASHW_ALL, FLASHW_STOP, FLASHW_TIMERNOFG, FLASHW_TRAY, GWLP_HINSTANCE, HTBOTTOM, + HTBOTTOMLEFT, HTBOTTOMRIGHT, HTCAPTION, HTLEFT, HTRIGHT, HTTOP, HTTOPLEFT, + HTTOPRIGHT, MENU_ITEM_STATE, MFS_DISABLED, MFS_ENABLED, MF_BYCOMMAND, NID_READY, + PM_NOREMOVE, SC_CLOSE, SC_MAXIMIZE, SC_MINIMIZE, SC_MOVE, SC_RESTORE, SC_SIZE, + SM_DIGITIZER, SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER, + TPM_LEFTALIGN, TPM_RETURNCMD, WDA_EXCLUDEFROMCAPTURE, WDA_NONE, WM_NCLBUTTONDOWN, + WM_SYSCOMMAND, WNDCLASSEXW, +}; + +use tracing::warn; + +use crate::cursor::Cursor; +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::icon::Icon; +use crate::platform::windows::{BackdropType, Color, CornerPreference}; +use crate::platform_impl::platform::dark_mode::try_theme; +use crate::platform_impl::platform::definitions::{ + CLSID_TaskbarList, IID_ITaskbarList, IID_ITaskbarList2, ITaskbarList, ITaskbarList2, +}; +use crate::platform_impl::platform::dpi::{ + dpi_to_scale_factor, enable_non_client_dpi_scaling, hwnd_dpi, +}; +use crate::platform_impl::platform::drop_handler::FileDropHandler; +use crate::platform_impl::platform::event_loop::{self, ActiveEventLoop, DESTROY_MSG_ID}; +use crate::platform_impl::platform::icon::{self, IconType, WinCursor}; +use crate::platform_impl::platform::ime::ImeContext; +use crate::platform_impl::platform::keyboard::KeyEventBuilder; +use crate::platform_impl::platform::monitor::{self, MonitorHandle}; +use crate::platform_impl::platform::window_state::{ + CursorFlags, SavedWindow, WindowFlags, WindowState, +}; +use crate::platform_impl::platform::{util, Fullscreen, SelectedCursor, WindowId}; +use crate::window::{ + CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowLevel, +}; + +/// The Win32 implementation of the main `Window` object. +pub(crate) struct Window { + /// Main handle for the window. + window: HWND, + + /// The current window state. + window_state: Arc>, + + // The events loop proxy. + thread_executor: event_loop::EventLoopThreadExecutor, +} + +impl Window { + pub(crate) fn new( + event_loop: &ActiveEventLoop, + w_attr: WindowAttributes, + ) -> Result { + // We dispatch an `init` function because of code style. + // First person to remove the need for cloning here gets a cookie! + // + // done. you owe me -- ossi + unsafe { init(w_attr, event_loop) } + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) { + // TODO: Use `thread_executor` here + f(self) + } + + pub(crate) fn maybe_wait_on_main( + &self, + f: impl FnOnce(&Self) -> R + Send, + ) -> R { + // TODO: Use `thread_executor` here + f(self) + } + + fn window_state_lock(&self) -> MutexGuard<'_, WindowState> { + self.window_state.lock().unwrap() + } + + pub fn set_title(&self, text: &str) { + let wide_text = util::encode_wide(text); + unsafe { + SetWindowTextW(self.hwnd(), wide_text.as_ptr()); + } + } + + pub fn set_transparent(&self, transparent: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::TRANSPARENT, transparent) + }); + }); + } + + pub fn set_blur(&self, _blur: bool) {} + + #[inline] + pub fn set_visible(&self, visible: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::VISIBLE, visible) + }); + }); + } + + #[inline] + pub fn is_visible(&self) -> Option { + Some(unsafe { IsWindowVisible(self.window) == 1 }) + } + + #[inline] + pub fn request_redraw(&self) { + // NOTE: mark that we requested a redraw to handle requests during `WM_PAINT` handling. + self.window_state.lock().unwrap().redraw_requested = true; + unsafe { + RedrawWindow(self.hwnd(), ptr::null(), 0, RDW_INTERNALPAINT); + } + } + + #[inline] + pub fn pre_present_notify(&self) {} + + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + util::WindowArea::Outer + .get_rect(self.hwnd()) + .map(|rect| Ok(PhysicalPosition::new(rect.left, rect.top))) + .expect( + "Unexpected GetWindowRect failure; please report this error to \ + rust-windowing/winit", + ) + } + + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + let mut position: POINT = unsafe { mem::zeroed() }; + if unsafe { ClientToScreen(self.hwnd(), &mut position) } == false.into() { + panic!( + "Unexpected ClientToScreen failure: please report this error to \ + rust-windowing/winit" + ) + } + Ok(PhysicalPosition::new(position.x, position.y)) + } + + #[inline] + pub fn set_outer_position(&self, position: Position) { + let (x, y): (i32, i32) = position.to_physical::(self.scale_factor()).into(); + + let window_state = Arc::clone(&self.window_state); + let window = self.window; + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MAXIMIZED, false) + }); + }); + + unsafe { + SetWindowPos( + self.hwnd(), + 0, + x, + y, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE, + ); + InvalidateRgn(self.hwnd(), 0, false.into()); + } + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let mut rect: RECT = unsafe { mem::zeroed() }; + if unsafe { GetClientRect(self.hwnd(), &mut rect) } == false.into() { + panic!( + "Unexpected GetClientRect failure: please report this error to \ + rust-windowing/winit" + ) + } + PhysicalSize::new( + (rect.right - rect.left) as u32, + (rect.bottom - rect.top) as u32, + ) + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + util::WindowArea::Outer + .get_rect(self.hwnd()) + .map(|rect| { + PhysicalSize::new( + (rect.right - rect.left) as u32, + (rect.bottom - rect.top) as u32, + ) + }) + .unwrap() + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let scale_factor = self.scale_factor(); + let physical_size = size.to_physical::(scale_factor); + + let window_flags = self.window_state_lock().window_flags; + window_flags.set_size(self.hwnd(), physical_size); + + if physical_size != self.inner_size() { + let window_state = Arc::clone(&self.window_state); + let window = self.window; + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags( + window_state.lock().unwrap(), + window, + |f| f.set(WindowFlags::MAXIMIZED, false), + ); + }); + } + + None + } + + #[inline] + pub fn set_min_inner_size(&self, size: Option) { + self.window_state_lock().min_size = size; + // Make windows re-check the window size bounds. + let size = self.inner_size(); + self.request_inner_size(size.into()); + } + + #[inline] + pub fn set_max_inner_size(&self, size: Option) { + self.window_state_lock().max_size = size; + // Make windows re-check the window size bounds. + let size = self.inner_size(); + self.request_inner_size(size.into()); + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + let w = self.window_state_lock(); + let scale_factor = w.scale_factor; + w.resize_increments + .map(|size| size.to_physical(scale_factor)) + } + + #[inline] + pub fn set_resize_increments(&self, increments: Option) { + self.window_state_lock().resize_increments = increments; + } + + #[inline] + pub fn set_resizable(&self, resizable: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::RESIZABLE, resizable) + }); + }); + } + + #[inline] + pub fn is_resizable(&self) -> bool { + let window_state = self.window_state_lock(); + window_state.window_flags.contains(WindowFlags::RESIZABLE) + } + + #[inline] + pub fn set_enabled_buttons(&self, buttons: WindowButtons) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set( + WindowFlags::MINIMIZABLE, + buttons.contains(WindowButtons::MINIMIZE), + ); + f.set( + WindowFlags::MAXIMIZABLE, + buttons.contains(WindowButtons::MAXIMIZE), + ); + f.set( + WindowFlags::CLOSABLE, + buttons.contains(WindowButtons::CLOSE), + ) + }); + }); + } + + pub fn enabled_buttons(&self) -> WindowButtons { + let mut buttons = WindowButtons::empty(); + let window_state = self.window_state_lock(); + if window_state.window_flags.contains(WindowFlags::MINIMIZABLE) { + buttons |= WindowButtons::MINIMIZE; + } + if window_state.window_flags.contains(WindowFlags::MAXIMIZABLE) { + buttons |= WindowButtons::MAXIMIZE; + } + if window_state.window_flags.contains(WindowFlags::CLOSABLE) { + buttons |= WindowButtons::CLOSE; + } + buttons + } + + /// Returns the `hwnd` of this window. + #[inline] + pub fn hwnd(&self) -> HWND { + self.window + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub unsafe fn rwh_06_no_thread_check( + &self, + ) -> Result { + let mut window_handle = rwh_06::Win32WindowHandle::new(unsafe { + // SAFETY: Handle will never be zero. + std::num::NonZeroIsize::new_unchecked(self.window) + }); + let hinstance = unsafe { super::get_window_long(self.hwnd(), GWLP_HINSTANCE) }; + window_handle.hinstance = std::num::NonZeroIsize::new(hinstance); + Ok(rwh_06::RawWindowHandle::Win32(window_handle)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06( + &self, + ) -> Result { + // TODO: Write a test once integration framework is ready to ensure that it holds. + // If we aren't in the GUI thread, we can't return the window. + if !self.thread_executor.in_event_loop_thread() { + tracing::error!("tried to access window handle outside of the main thread"); + return Err(rwh_06::HandleError::Unavailable); + } + + // SAFETY: We are on the correct thread. + unsafe { self.rwh_06_no_thread_check() } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Windows( + rwh_06::WindowsDisplayHandle::new(), + )) + } + + #[inline] + pub fn set_cursor(&self, cursor: Cursor) { + match cursor { + Cursor::Icon(icon) => { + self.window_state_lock().mouse.selected_cursor = + SelectedCursor::Named(icon); + self.thread_executor.execute_in_thread(move || unsafe { + let cursor = LoadCursorW(0, util::to_windows_cursor(icon)); + SetCursor(cursor); + }); + } + Cursor::Custom(cursor) => { + let new_cursor = match cursor.inner { + WinCursor::Cursor(cursor) => cursor, + WinCursor::Failed => { + warn!("Requested to apply failed cursor"); + return; + } + }; + self.window_state_lock().mouse.selected_cursor = + SelectedCursor::Custom(new_cursor.clone()); + self.thread_executor.execute_in_thread(move || unsafe { + SetCursor(new_cursor.as_raw_handle()); + }); + } + } + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let confine = match mode { + CursorGrabMode::None => false, + CursorGrabMode::Confined => true, + CursorGrabMode::Locked => { + return Err(ExternalError::NotSupported(NotSupportedError::new())) + } + }; + + let window = self.window; + let window_state = Arc::clone(&self.window_state); + let (tx, rx) = channel(); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + let result = window_state + .lock() + .unwrap() + .mouse + .set_cursor_flags(window, |f| f.set(CursorFlags::GRABBED, confine)) + .map_err(|e| ExternalError::Os(os_error!(e))); + let _ = tx.send(result); + }); + rx.recv().unwrap() + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + let (tx, rx) = channel(); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + let result = window_state + .lock() + .unwrap() + .mouse + .set_cursor_flags(window, |f| f.set(CursorFlags::HIDDEN, !visible)) + .map_err(|e| e.to_string()); + let _ = tx.send(result); + }); + rx.recv().unwrap().ok(); + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.window_state_lock().scale_factor + } + + #[inline] + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + let scale_factor = self.scale_factor(); + let (x, y) = position.to_physical::(scale_factor).into(); + + let mut point = POINT { x, y }; + unsafe { + if ClientToScreen(self.hwnd(), &mut point) == false.into() { + return Err(ExternalError::Os(os_error!(io::Error::last_os_error()))); + } + if SetCursorPos(point.x, point.y) == false.into() { + return Err(ExternalError::Os(os_error!(io::Error::last_os_error()))); + } + } + Ok(()) + } + + unsafe fn handle_os_dragging(&self, wparam: WPARAM) { + let window = self.window; + let window_state = self.window_state.clone(); + + self.thread_executor.execute_in_thread(move || { + { + let mut guard = window_state.lock().unwrap(); + if !guard.dragging { + guard.dragging = true; + } else { + return; + } + } + + let points = { + let mut pos = unsafe { mem::zeroed() }; + unsafe { GetCursorPos(&mut pos) }; + pos + }; + let points = POINTS { + x: points.x as i16, + y: points.y as i16, + }; + + // ReleaseCapture needs to execute on the main thread + unsafe { ReleaseCapture() }; + + unsafe { + PostMessageW( + window, + WM_NCLBUTTONDOWN, + wparam, + &points as *const _ as LPARAM, + ) + }; + }); + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + unsafe { + self.handle_os_dragging(HTCAPTION as WPARAM); + } + + Ok(()) + } + + #[inline] + pub fn drag_resize_window( + &self, + direction: ResizeDirection, + ) -> Result<(), ExternalError> { + unsafe { + self.handle_os_dragging(match direction { + ResizeDirection::East => HTRIGHT, + ResizeDirection::North => HTTOP, + ResizeDirection::NorthEast => HTTOPRIGHT, + ResizeDirection::NorthWest => HTTOPLEFT, + ResizeDirection::South => HTBOTTOM, + ResizeDirection::SouthEast => HTBOTTOMRIGHT, + ResizeDirection::SouthWest => HTBOTTOMLEFT, + ResizeDirection::West => HTLEFT, + } as WPARAM); + } + + Ok(()) + } + + unsafe fn handle_showing_window_menu(&self, position: Position) { + unsafe { + let point = { + let mut point = POINT { x: 0, y: 0 }; + let scale_factor = self.scale_factor(); + let (x, y) = position.to_physical::(scale_factor).into(); + point.x = x; + point.y = y; + if ClientToScreen(self.hwnd(), &mut point) == false.into() { + warn!( + "Can't convert client-area coordinates to screen coordinates when showing \ + window menu." + ); + return; + } + point + }; + + // get the current system menu + let h_menu = GetSystemMenu(self.hwnd(), 0); + if h_menu == 0 { + warn!("The corresponding window doesn't have a system menu"); + // This situation should not be treated as an error so just return without showing + // menu. + return; + } + + fn enable(b: bool) -> MENU_ITEM_STATE { + if b { + MFS_ENABLED + } else { + MFS_DISABLED + } + } + + // Change the menu items according to the current window status. + + let restore_btn = enable(self.is_maximized() && self.is_resizable()); + let size_btn = enable(!self.is_maximized() && self.is_resizable()); + let maximize_btn = enable(!self.is_maximized() && self.is_resizable()); + + EnableMenuItem(h_menu, SC_RESTORE, MF_BYCOMMAND | restore_btn); + EnableMenuItem(h_menu, SC_MOVE, MF_BYCOMMAND | enable(!self.is_maximized())); + EnableMenuItem(h_menu, SC_SIZE, MF_BYCOMMAND | size_btn); + EnableMenuItem(h_menu, SC_MINIMIZE, MF_BYCOMMAND | MFS_ENABLED); + EnableMenuItem(h_menu, SC_MAXIMIZE, MF_BYCOMMAND | maximize_btn); + EnableMenuItem(h_menu, SC_CLOSE, MF_BYCOMMAND | MFS_ENABLED); + + // Set the default menu item. + SetMenuDefaultItem(h_menu, SC_CLOSE, 0); + + // Popup the system menu at the position. + let result = TrackPopupMenu( + h_menu, + TPM_RETURNCMD | TPM_LEFTALIGN, /* for now im using LTR, but we have to use user + * layout direction */ + point.x, + point.y, + 0, + self.hwnd(), + std::ptr::null_mut(), + ); + + if result == 0 { + // User canceled the menu, no need to continue. + return; + } + + // Send the command that the user select to the corresponding window. + if PostMessageW(self.hwnd(), WM_SYSCOMMAND, result as _, 0) == 0 { + warn!("Can't post the system menu message to the window."); + } + } + } + + #[inline] + pub fn show_window_menu(&self, position: Position) { + unsafe { + self.handle_showing_window_menu(position); + } + } + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::IGNORE_CURSOR_EVENT, !hittest) + }); + }); + + Ok(()) + } + + #[inline] + pub fn id(&self) -> WindowId { + WindowId(self.hwnd()) + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + let is_minimized = util::is_minimized(self.hwnd()); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags_in_place( + &mut window_state.lock().unwrap(), + |f| f.set(WindowFlags::MINIMIZED, is_minimized), + ); + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MINIMIZED, minimized) + }); + }); + } + + #[inline] + pub fn is_minimized(&self) -> Option { + Some(util::is_minimized(self.hwnd())) + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MAXIMIZED, maximized) + }); + }); + } + + #[inline] + pub fn is_maximized(&self) -> bool { + let window_state = self.window_state_lock(); + window_state.window_flags.contains(WindowFlags::MAXIMIZED) + } + + #[inline] + pub fn fullscreen(&self) -> Option { + let window_state = self.window_state_lock(); + window_state.fullscreen.clone() + } + + #[inline] + pub fn set_fullscreen(&self, fullscreen: Option) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + let mut window_state_lock = window_state.lock().unwrap(); + let old_fullscreen = window_state_lock.fullscreen.clone(); + + match (&old_fullscreen, &fullscreen) { + // Return if we already are in the same fullscreen mode + _ if old_fullscreen == fullscreen => return, + // Return if saved Borderless(monitor) is the same as current monitor when requested + // fullscreen is Borderless(None) + ( + Some(Fullscreen::Borderless(Some(monitor))), + Some(Fullscreen::Borderless(None)), + ) if *monitor == monitor::current_monitor(window) => return, + _ => {} + } + + window_state_lock.fullscreen.clone_from(&fullscreen); + drop(window_state_lock); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + // Change video mode if we're transitioning to or from exclusive + // fullscreen + match (&old_fullscreen, &fullscreen) { + (_, Some(Fullscreen::Exclusive(video_mode))) => { + let monitor = video_mode.monitor(); + let monitor_info = + monitor::get_monitor_info(monitor.hmonitor()).unwrap(); + + let res = unsafe { + ChangeDisplaySettingsExW( + monitor_info.szDevice.as_ptr(), + &*video_mode.native_video_mode, + 0, + CDS_FULLSCREEN, + ptr::null(), + ) + }; + + debug_assert!(res != DISP_CHANGE_BADFLAGS); + debug_assert!(res != DISP_CHANGE_BADMODE); + debug_assert!(res != DISP_CHANGE_BADPARAM); + debug_assert!(res != DISP_CHANGE_FAILED); + assert_eq!(res, DISP_CHANGE_SUCCESSFUL); + } + (Some(Fullscreen::Exclusive(_)), _) => { + let res = unsafe { + ChangeDisplaySettingsExW( + ptr::null(), + ptr::null(), + 0, + CDS_FULLSCREEN, + ptr::null(), + ) + }; + + debug_assert!(res != DISP_CHANGE_BADFLAGS); + debug_assert!(res != DISP_CHANGE_BADMODE); + debug_assert!(res != DISP_CHANGE_BADPARAM); + debug_assert!(res != DISP_CHANGE_FAILED); + assert_eq!(res, DISP_CHANGE_SUCCESSFUL); + } + _ => (), + } + + unsafe { + // There are some scenarios where calling `ChangeDisplaySettingsExW` takes long + // enough to execute that the DWM thinks our program has frozen and takes over + // our program's window. When that happens, the `SetWindowPos` call below gets + // eaten and the window doesn't get set to the proper fullscreen position. + // + // Calling `PeekMessageW` here notifies Windows that our process is still running + // fine, taking control back from the DWM and ensuring that the `SetWindowPos` call + // below goes through. + let mut msg = mem::zeroed(); + PeekMessageW(&mut msg, 0, 0, 0, PM_NOREMOVE); + } + + // Update window style + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set( + WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN, + matches!(fullscreen, Some(Fullscreen::Exclusive(_))), + ); + f.set( + WindowFlags::MARKER_BORDERLESS_FULLSCREEN, + matches!(fullscreen, Some(Fullscreen::Borderless(_))), + ); + }); + + // Mark as fullscreen window wrt to z-order + // + // this needs to be called before the below fullscreen SetWindowPos as this itself + // will generate WM_SIZE messages of the old window size that can race with what we set + // below + unsafe { + taskbar_mark_fullscreen(window, fullscreen.is_some()); + } + + // Update window bounds + match &fullscreen { + Some(fullscreen) => { + // Save window bounds before entering fullscreen + let placement = unsafe { + let mut placement = mem::zeroed(); + GetWindowPlacement(window, &mut placement); + placement + }; + + window_state.lock().unwrap().saved_window = + Some(SavedWindow { placement }); + + let monitor = match &fullscreen { + Fullscreen::Exclusive(video_mode) => video_mode.monitor(), + Fullscreen::Borderless(Some(monitor)) => monitor.clone(), + Fullscreen::Borderless(None) => monitor::current_monitor(window), + }; + + let position: (i32, i32) = monitor.position().into(); + let size: (u32, u32) = monitor.size().into(); + + unsafe { + SetWindowPos( + window, + 0, + position.0, + position.1, + size.0 as i32, + size.1 as i32, + SWP_ASYNCWINDOWPOS | SWP_NOZORDER, + ); + InvalidateRgn(window, 0, false.into()); + } + } + None => { + let mut window_state_lock = window_state.lock().unwrap(); + if let Some(SavedWindow { placement }) = + window_state_lock.saved_window.take() + { + drop(window_state_lock); + unsafe { + SetWindowPlacement(window, &placement); + InvalidateRgn(window, 0, false.into()); + } + } + } + } + }); + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MARKER_DECORATIONS, decorations) + }); + }); + } + + #[inline] + pub fn is_decorated(&self) -> bool { + let window_state = self.window_state_lock(); + window_state + .window_flags + .contains(WindowFlags::MARKER_DECORATIONS) + } + + #[inline] + pub fn set_window_level(&self, level: WindowLevel) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set( + WindowFlags::ALWAYS_ON_TOP, + level == WindowLevel::AlwaysOnTop, + ); + f.set( + WindowFlags::ALWAYS_ON_BOTTOM, + level == WindowLevel::AlwaysOnBottom, + ); + }); + }); + } + + #[inline] + pub fn current_monitor(&self) -> Option { + Some(monitor::current_monitor(self.hwnd())) + } + + #[inline] + pub fn set_window_icon(&self, window_icon: Option) { + if let Some(ref window_icon) = window_icon { + window_icon + .inner + .set_for_window(self.hwnd(), IconType::Small); + } else { + icon::unset_for_window(self.hwnd(), IconType::Small); + } + self.window_state_lock().window_icon = window_icon; + } + + #[inline] + pub fn set_enable(&self, enabled: bool) { + unsafe { EnableWindow(self.hwnd(), enabled.into()) }; + } + + #[inline] + pub fn set_taskbar_icon(&self, taskbar_icon: Option) { + if let Some(ref taskbar_icon) = taskbar_icon { + taskbar_icon + .inner + .set_for_window(self.hwnd(), IconType::Big); + } else { + icon::unset_for_window(self.hwnd(), IconType::Big); + } + self.window_state_lock().taskbar_icon = taskbar_icon; + } + + #[inline] + pub fn set_ime_cursor_area(&self, spot: Position, size: Size) { + let window = self.window; + let state = self.window_state.clone(); + self.thread_executor.execute_in_thread(move || unsafe { + let scale_factor = state.lock().unwrap().scale_factor; + ImeContext::current(window).set_ime_cursor_area(spot, size, scale_factor); + }); + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + let window = self.window; + let state = self.window_state.clone(); + self.thread_executor.execute_in_thread(move || unsafe { + state.lock().unwrap().ime_allowed = allowed; + ImeContext::set_ime_allowed(window, allowed); + }) + } + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + + #[inline] + pub fn request_user_attention(&self, request_type: Option) { + let window = self.window; + let active_window_handle = unsafe { GetActiveWindow() }; + if window == active_window_handle { + return; + } + + self.thread_executor.execute_in_thread(move || unsafe { + let (flags, count) = request_type + .map(|ty| match ty { + UserAttentionType::Critical => { + (FLASHW_ALL | FLASHW_TIMERNOFG, u32::MAX) + } + UserAttentionType::Informational => { + (FLASHW_TRAY | FLASHW_TIMERNOFG, 0) + } + }) + .unwrap_or((FLASHW_STOP, 0)); + + let flash_info = FLASHWINFO { + cbSize: mem::size_of::() as u32, + hwnd: window, + dwFlags: flags, + uCount: count, + dwTimeout: 0, + }; + FlashWindowEx(&flash_info); + }); + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + try_theme(self.window, theme); + } + + #[inline] + pub fn theme(&self) -> Option { + Some(self.window_state_lock().current_theme) + } + + #[inline] + pub fn has_focus(&self) -> bool { + let window_state = self.window_state.lock().unwrap(); + window_state.has_active_focus() + } + + pub fn title(&self) -> String { + let len = unsafe { GetWindowTextLengthW(self.window) } + 1; + let mut buf = vec![0; len as usize]; + unsafe { GetWindowTextW(self.window, buf.as_mut_ptr(), len) }; + util::decode_wide(&buf).to_string_lossy().to_string() + } + + #[inline] + pub fn set_skip_taskbar(&self, skip: bool) { + self.window_state_lock().skip_taskbar = skip; + unsafe { set_skip_taskbar(self.hwnd(), skip) }; + } + + #[inline] + pub fn set_undecorated_shadow(&self, shadow: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MARKER_UNDECORATED_SHADOW, shadow) + }); + }); + } + + #[inline] + pub fn set_system_backdrop(&self, backdrop_type: BackdropType) { + unsafe { + DwmSetWindowAttribute( + self.hwnd(), + DWMWA_SYSTEMBACKDROP_TYPE as u32, + &(backdrop_type as i32) as *const _ as _, + mem::size_of::() as _, + ); + } + } + + #[inline] + pub fn focus_window(&self) { + let window_flags = self.window_state_lock().window_flags(); + + let is_visible = window_flags.contains(WindowFlags::VISIBLE); + let is_minimized = util::is_minimized(self.hwnd()); + let is_foreground = self.window == unsafe { GetForegroundWindow() }; + + if is_visible && !is_minimized && !is_foreground { + unsafe { force_window_active(self.window) }; + } + } + + #[inline] + pub fn set_content_protected(&self, protected: bool) { + unsafe { + SetWindowDisplayAffinity( + self.hwnd(), + if protected { + WDA_EXCLUDEFROMCAPTURE + } else { + WDA_NONE + }, + ) + }; + } + + #[inline] + pub fn reset_dead_keys(&self) { + // `ToUnicode` consumes the dead-key by default, so we are constructing a fake (but valid) + // key input which we can call `ToUnicode` with. + unsafe { + let vk = VK_SPACE as VIRTUAL_KEY; + let scancode = MapVirtualKeyW(vk as u32, MAPVK_VK_TO_VSC); + let kbd_state = [0; 256]; + let mut char_buff = [MaybeUninit::uninit(); 8]; + ToUnicode( + vk as u32, + scancode, + kbd_state.as_ptr(), + char_buff[0].as_mut_ptr(), + char_buff.len() as i32, + 0, + ); + } + } + + #[inline] + pub fn set_border_color(&self, color: Color) { + unsafe { + DwmSetWindowAttribute( + self.hwnd(), + DWMWA_BORDER_COLOR as u32, + &color as *const _ as _, + mem::size_of::() as _, + ); + } + } + + #[inline] + pub fn set_title_background_color(&self, color: Color) { + unsafe { + DwmSetWindowAttribute( + self.hwnd(), + DWMWA_CAPTION_COLOR as u32, + &color as *const _ as _, + mem::size_of::() as _, + ); + } + } + + #[inline] + pub fn set_title_text_color(&self, color: Color) { + unsafe { + DwmSetWindowAttribute( + self.hwnd(), + DWMWA_TEXT_COLOR as u32, + &color as *const _ as _, + mem::size_of::() as _, + ); + } + } + + #[inline] + pub fn set_corner_preference(&self, preference: CornerPreference) { + unsafe { + DwmSetWindowAttribute( + self.hwnd(), + DWMWA_WINDOW_CORNER_PREFERENCE as u32, + &(preference as DWM_WINDOW_CORNER_PREFERENCE) as *const _ as _, + mem::size_of::() as _, + ); + } + } +} + +impl Drop for Window { + #[inline] + fn drop(&mut self) { + unsafe { + // The window must be destroyed from the same thread that created it, so we send a + // custom message to be handled by our callback to do the actual work. + PostMessageW(self.hwnd(), DESTROY_MSG_ID.get(), 0, 0); + } + } +} + +pub(super) struct InitData<'a> { + // inputs + pub event_loop: &'a ActiveEventLoop, + pub attributes: WindowAttributes, + pub window_flags: WindowFlags, + // outputs + pub window: Option, +} + +impl<'a> InitData<'a> { + unsafe fn create_window(&self, window: HWND) -> Window { + // Register for touch events if applicable + { + let digitizer = unsafe { GetSystemMetrics(SM_DIGITIZER) as u32 }; + if digitizer & NID_READY != 0 { + unsafe { RegisterTouchWindow(window, TWF_WANTPALM) }; + } + } + + let dpi = unsafe { hwnd_dpi(window) }; + let scale_factor = dpi_to_scale_factor(dpi); + + // If the system theme is dark, we need to set the window theme now + // before we update the window flags (and possibly show the + // window for the first time). + let current_theme = try_theme(window, self.attributes.preferred_theme); + + let window_state = { + let window_state = WindowState::new( + &self.attributes, + scale_factor, + current_theme, + self.attributes.preferred_theme, + ); + let window_state = Arc::new(Mutex::new(window_state)); + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + *f = self.window_flags + }); + window_state + }; + + enable_non_client_dpi_scaling(window); + + unsafe { ImeContext::set_ime_allowed(window, false) }; + + Window { + window, + window_state, + thread_executor: self.event_loop.create_thread_executor(), + } + } + + unsafe fn create_window_data(&self, win: &Window) -> event_loop::WindowData { + let file_drop_handler = if self.attributes.platform_specific.drag_and_drop { + let ole_init_result = unsafe { OleInitialize(ptr::null_mut()) }; + // It is ok if the initialize result is `S_FALSE` because it might happen that + // multiple windows are created on the same thread. + if ole_init_result == OLE_E_WRONGCOMPOBJ { + panic!("OleInitialize failed! Result was: `OLE_E_WRONGCOMPOBJ`"); + } else if ole_init_result == RPC_E_CHANGED_MODE { + panic!( + "OleInitialize failed! Result was: `RPC_E_CHANGED_MODE`. Make sure other \ + crates are not using multithreaded COM library on the same thread or disable \ + drag and drop support." + ); + } + + let file_drop_runner = self.event_loop.runner_shared.clone(); + let file_drop_handler = FileDropHandler::new( + win.window, + Box::new(move |event| { + if let Ok(e) = event.map_nonuser_event() { + file_drop_runner.send_event(e) + } + }), + ); + + let handler_interface_ptr = unsafe { + &mut (*file_drop_handler.data).interface as *mut _ as *mut c_void + }; + + assert_eq!( + unsafe { RegisterDragDrop(win.window, handler_interface_ptr) }, + S_OK + ); + Some(file_drop_handler) + } else { + None + }; + + event_loop::WindowData { + window_state: win.window_state.clone(), + event_loop_runner: self.event_loop.runner_shared.clone(), + key_event_builder: KeyEventBuilder::default(), + _file_drop_handler: file_drop_handler, + userdata_removed: Cell::new(false), + recurse_depth: Cell::new(0), + } + } + + // Returns a pointer to window user data on success. + // The user data will be registered for the window and can be accessed within the window event + // callback. + pub unsafe fn on_nccreate(&mut self, window: HWND) -> Option { + let runner = self.event_loop.runner_shared.clone(); + let result = runner.catch_unwind(|| { + let window = unsafe { self.create_window(window) }; + let window_data = unsafe { self.create_window_data(&window) }; + (window, window_data) + }); + + result.map(|(win, userdata)| { + self.window = Some(win); + let userdata = Box::into_raw(Box::new(userdata)); + userdata as _ + }) + } + + pub unsafe fn on_create(&mut self) { + let win = self.window.as_mut().expect("failed window creation"); + + // making the window transparent + if self.attributes.transparent + && !self.attributes.platform_specific.no_redirection_bitmap + { + // Empty region for the blur effect, so the window is fully transparent + let region = unsafe { CreateRectRgn(0, 0, -1, -1) }; + + let bb = DWM_BLURBEHIND { + dwFlags: DWM_BB_ENABLE | DWM_BB_BLURREGION, + fEnable: true.into(), + hRgnBlur: region, + fTransitionOnMaximized: false.into(), + }; + let hr = unsafe { DwmEnableBlurBehindWindow(win.hwnd(), &bb) }; + if hr < 0 { + warn!( + "Setting transparent window is failed. HRESULT Code: 0x{:X}", + hr + ); + } + unsafe { DeleteObject(region) }; + } + + win.set_skip_taskbar(self.attributes.platform_specific.skip_taskbar); + win.set_window_icon(self.attributes.window_icon.clone()); + win.set_taskbar_icon(self.attributes.platform_specific.taskbar_icon.clone()); + + let attributes = self.attributes.clone(); + + if attributes.content_protected { + win.set_content_protected(true); + } + + win.set_cursor(attributes.cursor); + + // Set visible before setting the size to ensure the + // attribute is correctly applied. + win.set_visible(attributes.visible); + + win.set_enabled_buttons(attributes.enabled_buttons); + + let size = attributes + .inner_size + .unwrap_or_else(|| PhysicalSize::new(800, 600).into()); + let max_size = attributes + .max_inner_size + .unwrap_or_else(|| PhysicalSize::new(f64::MAX, f64::MAX).into()); + let min_size = attributes + .min_inner_size + .unwrap_or_else(|| PhysicalSize::new(0, 0).into()); + let clamped_size = Size::clamp(size, min_size, max_size, win.scale_factor()); + win.request_inner_size(clamped_size); + + // let margins = MARGINS { + // cxLeftWidth: 1, + // cxRightWidth: 1, + // cyTopHeight: 1, + // cyBottomHeight: 1, + // }; + // dbg!(DwmExtendFrameIntoClientArea(win.hwnd(), &margins as *const _)); + + if let Some(position) = attributes.position { + win.set_outer_position(position); + } + + win.set_system_backdrop(self.attributes.platform_specific.backdrop_type); + + if let Some(color) = self.attributes.platform_specific.border_color { + win.set_border_color(color); + } + if let Some(color) = self.attributes.platform_specific.title_background_color { + win.set_title_background_color(color); + } + if let Some(color) = self.attributes.platform_specific.title_text_color { + win.set_title_text_color(color); + } + if let Some(corner) = self.attributes.platform_specific.corner_preference { + win.set_corner_preference(corner); + } + } +} +unsafe fn init( + attributes: WindowAttributes, + event_loop: &ActiveEventLoop, +) -> Result { + let title = util::encode_wide(&attributes.title); + + let class_name = util::encode_wide(&attributes.platform_specific.class_name); + unsafe { register_window_class(&class_name) }; + + let mut window_flags = WindowFlags::empty(); + window_flags.set(WindowFlags::MARKER_DECORATIONS, attributes.decorations); + window_flags.set( + WindowFlags::MARKER_UNDECORATED_SHADOW, + attributes.platform_specific.decoration_shadow, + ); + window_flags.set( + WindowFlags::ALWAYS_ON_TOP, + attributes.window_level == WindowLevel::AlwaysOnTop, + ); + window_flags.set( + WindowFlags::ALWAYS_ON_BOTTOM, + attributes.window_level == WindowLevel::AlwaysOnBottom, + ); + window_flags.set( + WindowFlags::NO_BACK_BUFFER, + attributes.platform_specific.no_redirection_bitmap, + ); + window_flags.set(WindowFlags::MARKER_ACTIVATE, attributes.active); + window_flags.set(WindowFlags::TRANSPARENT, attributes.transparent); + // WindowFlags::VISIBLE and MAXIMIZED are set down below after the window has been configured. + window_flags.set(WindowFlags::RESIZABLE, attributes.resizable); + // Will be changed later using `window.set_enabled_buttons` but we need to set a default here + // so the diffing later can work. + window_flags.set(WindowFlags::CLOSABLE, true); + window_flags.set( + WindowFlags::CLIP_CHILDREN, + attributes.platform_specific.clip_children, + ); + + let mut fallback_parent = || match attributes.platform_specific.owner { + Some(parent) => { + window_flags.set(WindowFlags::POPUP, true); + Some(parent) + } + None => { + window_flags.set(WindowFlags::ON_TASKBAR, true); + None + } + }; + + #[cfg(feature = "rwh_06")] + let parent = match attributes.parent_window.as_ref().map(|handle| handle.0) { + Some(rwh_06::RawWindowHandle::Win32(handle)) => { + window_flags.set(WindowFlags::CHILD, true); + if attributes.platform_specific.menu.is_some() { + warn!("Setting a menu on a child window is unsupported"); + } + Some(handle.hwnd.get() as HWND) + } + Some(raw) => unreachable!("Invalid raw window handle {raw:?} on Windows"), + None => fallback_parent(), + }; + + #[cfg(not(feature = "rwh_06"))] + let parent = fallback_parent(); + + let menu = attributes.platform_specific.menu; + let fullscreen = attributes.fullscreen.clone(); + let maximized = attributes.maximized; + let mut initdata = InitData { + event_loop, + attributes, + window_flags, + window: None, + }; + + let (style, ex_style) = window_flags.to_window_styles(); + let handle = unsafe { + CreateWindowExW( + ex_style, + class_name.as_ptr(), + title.as_ptr(), + style, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + parent.unwrap_or(0), + menu.unwrap_or(0), + util::get_instance_handle(), + &mut initdata as *mut _ as *mut _, + ) + }; + + // If the window creation in `InitData` panicked, then should resume panicking here + if let Err(panic_error) = event_loop.runner_shared.take_panic_error() { + panic::resume_unwind(panic_error) + } + + if handle == 0 { + return Err(os_error!(io::Error::last_os_error())); + } + + // If the handle is non-null, then window creation must have succeeded, which means + // that we *must* have populated the `InitData.window` field. + let win = initdata.window.unwrap(); + + // Need to set FULLSCREEN or MAXIMIZED after CreateWindowEx + // This is because if the size is changed in WM_CREATE, the restored size will be stored in that + // size. + if fullscreen.is_some() { + win.set_fullscreen(fullscreen.map(Into::into)); + unsafe { force_window_active(win.window) }; + } else if maximized { + win.set_maximized(true); + } + + Ok(win) +} + +unsafe fn register_window_class(class_name: &[u16]) { + let class = WNDCLASSEXW { + cbSize: mem::size_of::() as u32, + style: CS_HREDRAW | CS_VREDRAW, + lpfnWndProc: Some(super::event_loop::public_window_callback), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: util::get_instance_handle(), + hIcon: 0, + hCursor: 0, // must be null in order for cursor state to work properly + hbrBackground: 0, + lpszMenuName: ptr::null(), + lpszClassName: class_name.as_ptr(), + hIconSm: 0, + }; + + // We ignore errors because registering the same window class twice would trigger + // an error, and because errors here are detected during CreateWindowEx anyway. + // Also since there is no weird element in the struct, there is no reason for this + // call to fail. + unsafe { RegisterClassExW(&class) }; +} + +struct ComInitialized(#[allow(dead_code)] *mut ()); +impl Drop for ComInitialized { + fn drop(&mut self) { + unsafe { CoUninitialize() }; + } +} + +thread_local! { + static COM_INITIALIZED: ComInitialized = { + unsafe { + CoInitializeEx(ptr::null(), COINIT_APARTMENTTHREADED as u32); + ComInitialized(ptr::null_mut()) + } + }; + + static TASKBAR_LIST: Cell<*mut ITaskbarList> = const { Cell::new(ptr::null_mut()) }; + static TASKBAR_LIST2: Cell<*mut ITaskbarList2> = const { Cell::new(ptr::null_mut()) }; +} + +pub fn com_initialized() { + COM_INITIALIZED.with(|_| {}); +} + +// Reference Implementation: +// https://github.com/chromium/chromium/blob/f18e79d901f56154f80eea1e2218544285e62623/ui/views/win/fullscreen_handler.cc +// +// As per MSDN marking the window as fullscreen should ensure that the +// taskbar is moved to the bottom of the Z-order when the fullscreen window +// is activated. If the window is not fullscreen, the Shell falls back to +// heuristics to determine how the window should be treated, which means +// that it could still consider the window as fullscreen. :( +unsafe fn taskbar_mark_fullscreen(handle: HWND, fullscreen: bool) { + com_initialized(); + + TASKBAR_LIST2.with(|task_bar_list2_ptr| { + let mut task_bar_list2 = task_bar_list2_ptr.get(); + + if task_bar_list2.is_null() { + let hr = unsafe { + CoCreateInstance( + &CLSID_TaskbarList, + ptr::null_mut(), + CLSCTX_ALL, + &IID_ITaskbarList2, + &mut task_bar_list2 as *mut _ as *mut _, + ) + }; + if hr != S_OK { + // In visual studio retrieving the taskbar list fails + return; + } + + let hr_init = unsafe { (*(*task_bar_list2).lpVtbl).parent.HrInit }; + if unsafe { hr_init(task_bar_list2.cast()) } != S_OK { + // In some old windows, the taskbar object could not be created, we just ignore it + return; + } + task_bar_list2_ptr.set(task_bar_list2) + } + + task_bar_list2 = task_bar_list2_ptr.get(); + let mark_fullscreen_window = + unsafe { (*(*task_bar_list2).lpVtbl).MarkFullscreenWindow }; + unsafe { mark_fullscreen_window(task_bar_list2, handle, fullscreen.into()) }; + }) +} + +pub(crate) unsafe fn set_skip_taskbar(hwnd: HWND, skip: bool) { + com_initialized(); + TASKBAR_LIST.with(|task_bar_list_ptr| { + let mut task_bar_list = task_bar_list_ptr.get(); + + if task_bar_list.is_null() { + let hr = unsafe { + CoCreateInstance( + &CLSID_TaskbarList, + ptr::null_mut(), + CLSCTX_ALL, + &IID_ITaskbarList, + &mut task_bar_list as *mut _ as *mut _, + ) + }; + if hr != S_OK { + // In visual studio retrieving the taskbar list fails + return; + } + + let hr_init = unsafe { (*(*task_bar_list).lpVtbl).HrInit }; + if unsafe { hr_init(task_bar_list.cast()) } != S_OK { + // In some old windows, the taskbar object could not be created, we just ignore it + return; + } + task_bar_list_ptr.set(task_bar_list) + } + + task_bar_list = task_bar_list_ptr.get(); + if skip { + let delete_tab = unsafe { (*(*task_bar_list).lpVtbl).DeleteTab }; + unsafe { delete_tab(task_bar_list, hwnd) }; + } else { + let add_tab = unsafe { (*(*task_bar_list).lpVtbl).AddTab }; + unsafe { add_tab(task_bar_list, hwnd) }; + } + }); +} + +unsafe fn force_window_active(handle: HWND) { + // In some situation, calling SetForegroundWindow could not bring up the window, + // This is a little hack which can "steal" the foreground window permission + // We only call this function in the window creation, so it should be fine. + // See : https://stackoverflow.com/questions/10740346/setforegroundwindow-only-working-while-visual-studio-is-open + let alt_sc = unsafe { MapVirtualKeyW(VK_MENU as u32, MAPVK_VK_TO_VSC) }; + + let inputs = [ + INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: VK_LMENU, + wScan: alt_sc as u16, + dwFlags: KEYEVENTF_EXTENDEDKEY, + dwExtraInfo: 0, + time: 0, + }, + }, + }, + INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: VK_LMENU, + wScan: alt_sc as u16, + dwFlags: KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, + dwExtraInfo: 0, + time: 0, + }, + }, + }, + ]; + + // Simulate a key press and release + unsafe { + SendInput( + inputs.len() as u32, + inputs.as_ptr(), + mem::size_of::() as i32, + ) + }; + + unsafe { SetForegroundWindow(handle) }; +} diff --git a/rio-window/src/platform_impl/windows/window_state.rs b/rio-window/src/platform_impl/windows/window_state.rs new file mode 100644 index 00000000..f39b7e72 --- /dev/null +++ b/rio-window/src/platform_impl/windows/window_state.rs @@ -0,0 +1,574 @@ +use crate::dpi::{PhysicalPosition, PhysicalSize, Size}; +use crate::icon::Icon; +use crate::keyboard::ModifiersState; +use crate::platform_impl::platform::{event_loop, util, Fullscreen, SelectedCursor}; +use crate::window::{Theme, WindowAttributes}; +use bitflags::bitflags; +use std::io; +use std::sync::MutexGuard; +use windows_sys::Win32::Foundation::{HWND, RECT}; +use windows_sys::Win32::Graphics::Gdi::InvalidateRgn; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + AdjustWindowRectEx, EnableMenuItem, GetMenu, GetSystemMenu, GetWindowLongW, + SendMessageW, SetWindowLongW, SetWindowPos, ShowWindow, GWL_EXSTYLE, GWL_STYLE, + HWND_BOTTOM, HWND_NOTOPMOST, HWND_TOPMOST, MF_BYCOMMAND, MF_DISABLED, MF_ENABLED, + SC_CLOSE, SWP_ASYNCWINDOWPOS, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, + SWP_NOREPOSITION, SWP_NOSIZE, SWP_NOZORDER, SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, + SW_RESTORE, SW_SHOW, SW_SHOWNOACTIVATE, WINDOWPLACEMENT, WINDOW_EX_STYLE, + WINDOW_STYLE, WS_BORDER, WS_CAPTION, WS_CHILD, WS_CLIPCHILDREN, WS_CLIPSIBLINGS, + WS_EX_ACCEPTFILES, WS_EX_APPWINDOW, WS_EX_LAYERED, WS_EX_NOREDIRECTIONBITMAP, + WS_EX_TOPMOST, WS_EX_TRANSPARENT, WS_EX_WINDOWEDGE, WS_MAXIMIZE, WS_MAXIMIZEBOX, + WS_MINIMIZE, WS_MINIMIZEBOX, WS_OVERLAPPEDWINDOW, WS_POPUP, WS_SIZEBOX, WS_SYSMENU, + WS_VISIBLE, +}; + +/// Contains information about states and the window that the callback is going to use. +pub(crate) struct WindowState { + pub mouse: MouseProperties, + + /// Used by `WM_GETMINMAXINFO`. + pub min_size: Option, + pub max_size: Option, + + pub resize_increments: Option, + + pub window_icon: Option, + pub taskbar_icon: Option, + + pub saved_window: Option, + pub scale_factor: f64, + + pub modifiers_state: ModifiersState, + pub fullscreen: Option, + pub current_theme: Theme, + pub preferred_theme: Option, + + pub window_flags: WindowFlags, + + pub ime_state: ImeState, + pub ime_allowed: bool, + + // Used by WM_NCACTIVATE, WM_SETFOCUS and WM_KILLFOCUS + pub is_active: bool, + pub is_focused: bool, + + // Flag whether redraw was requested. + pub redraw_requested: bool, + + pub dragging: bool, + + pub skip_taskbar: bool, +} + +#[derive(Clone)] +pub struct SavedWindow { + pub placement: WINDOWPLACEMENT, +} + +#[derive(Clone)] +pub struct MouseProperties { + pub(crate) selected_cursor: SelectedCursor, + pub capture_count: u32, + cursor_flags: CursorFlags, + pub last_position: Option>, +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct CursorFlags: u8 { + const GRABBED = 1 << 0; + const HIDDEN = 1 << 1; + const IN_WINDOW = 1 << 2; + } +} +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct WindowFlags: u32 { + const RESIZABLE = 1 << 0; + const MINIMIZABLE = 1 << 1; + const MAXIMIZABLE = 1 << 2; + const CLOSABLE = 1 << 3; + const VISIBLE = 1 << 4; + const ON_TASKBAR = 1 << 5; + const ALWAYS_ON_TOP = 1 << 6; + const ALWAYS_ON_BOTTOM = 1 << 7; + const NO_BACK_BUFFER = 1 << 8; + const TRANSPARENT = 1 << 9; + const CHILD = 1 << 10; + const MAXIMIZED = 1 << 11; + const POPUP = 1 << 12; + + /// Marker flag for fullscreen. Should always match `WindowState::fullscreen`, but is + /// included here to make masking easier. + const MARKER_EXCLUSIVE_FULLSCREEN = 1 << 13; + const MARKER_BORDERLESS_FULLSCREEN = 1 << 14; + + /// The `WM_SIZE` event contains some parameters that can effect the state of `WindowFlags`. + /// In most cases, it's okay to let those parameters change the state. However, when we're + /// running the `WindowFlags::apply_diff` function, we *don't* want those parameters to + /// effect our stored state, because the purpose of `apply_diff` is to update the actual + /// window's state to match our stored state. This controls whether to accept those changes. + const MARKER_RETAIN_STATE_ON_SIZE = 1 << 15; + + const MARKER_IN_SIZE_MOVE = 1 << 16; + + const MINIMIZED = 1 << 17; + + const IGNORE_CURSOR_EVENT = 1 << 18; + + /// Fully decorated window (incl. caption, border and drop shadow). + const MARKER_DECORATIONS = 1 << 19; + /// Drop shadow for undecorated windows. + const MARKER_UNDECORATED_SHADOW = 1 << 20; + + const MARKER_ACTIVATE = 1 << 21; + + const CLIP_CHILDREN = 1 << 22; + + const EXCLUSIVE_FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits(); + } +} + +#[derive(Eq, PartialEq)] +pub enum ImeState { + Disabled, + Enabled, + Preedit, +} + +impl WindowState { + pub(crate) fn new( + attributes: &WindowAttributes, + scale_factor: f64, + current_theme: Theme, + preferred_theme: Option, + ) -> WindowState { + WindowState { + mouse: MouseProperties { + selected_cursor: SelectedCursor::default(), + capture_count: 0, + cursor_flags: CursorFlags::empty(), + last_position: None, + }, + + min_size: attributes.min_inner_size, + max_size: attributes.max_inner_size, + + resize_increments: attributes.resize_increments, + + window_icon: attributes.window_icon.clone(), + taskbar_icon: None, + + saved_window: None, + scale_factor, + + modifiers_state: ModifiersState::default(), + fullscreen: None, + current_theme, + preferred_theme, + window_flags: WindowFlags::empty(), + + ime_state: ImeState::Disabled, + ime_allowed: false, + + is_active: false, + is_focused: false, + redraw_requested: false, + + dragging: false, + + skip_taskbar: false, + } + } + + pub fn window_flags(&self) -> WindowFlags { + self.window_flags + } + + pub fn set_window_flags(mut this: MutexGuard<'_, Self>, window: HWND, f: F) + where + F: FnOnce(&mut WindowFlags), + { + let old_flags = this.window_flags; + f(&mut this.window_flags); + let new_flags = this.window_flags; + + drop(this); + old_flags.apply_diff(window, new_flags); + } + + pub fn set_window_flags_in_place(&mut self, f: F) + where + F: FnOnce(&mut WindowFlags), + { + f(&mut self.window_flags); + } + + pub fn has_active_focus(&self) -> bool { + self.is_active && self.is_focused + } + + // Updates is_active and returns whether active-focus state has changed + pub fn set_active(&mut self, is_active: bool) -> bool { + let old = self.has_active_focus(); + self.is_active = is_active; + old != self.has_active_focus() + } + + // Updates is_focused and returns whether active-focus state has changed + pub fn set_focused(&mut self, is_focused: bool) -> bool { + let old = self.has_active_focus(); + self.is_focused = is_focused; + old != self.has_active_focus() + } +} + +impl MouseProperties { + pub fn cursor_flags(&self) -> CursorFlags { + self.cursor_flags + } + + pub fn set_cursor_flags(&mut self, window: HWND, f: F) -> Result<(), io::Error> + where + F: FnOnce(&mut CursorFlags), + { + let old_flags = self.cursor_flags; + f(&mut self.cursor_flags); + match self.cursor_flags.refresh_os_cursor(window) { + Ok(()) => (), + Err(e) => { + self.cursor_flags = old_flags; + return Err(e); + } + } + + Ok(()) + } +} + +impl WindowFlags { + fn mask(mut self) -> WindowFlags { + if self.contains(WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN) { + self |= WindowFlags::EXCLUSIVE_FULLSCREEN_OR_MASK; + } + self + } + + pub fn to_window_styles(self) -> (WINDOW_STYLE, WINDOW_EX_STYLE) { + // Required styles to properly support common window functionality like aero snap. + let mut style = WS_CAPTION | WS_BORDER | WS_CLIPSIBLINGS | WS_SYSMENU; + let mut style_ex = WS_EX_WINDOWEDGE | WS_EX_ACCEPTFILES; + + if self.contains(WindowFlags::RESIZABLE) { + style |= WS_SIZEBOX; + } + if self.contains(WindowFlags::MAXIMIZABLE) { + style |= WS_MAXIMIZEBOX; + } + if self.contains(WindowFlags::MINIMIZABLE) { + style |= WS_MINIMIZEBOX; + } + if self.contains(WindowFlags::VISIBLE) { + style |= WS_VISIBLE; + } + if self.contains(WindowFlags::ON_TASKBAR) { + style_ex |= WS_EX_APPWINDOW; + } + if self.contains(WindowFlags::ALWAYS_ON_TOP) { + style_ex |= WS_EX_TOPMOST; + } + if self.contains(WindowFlags::NO_BACK_BUFFER) { + style_ex |= WS_EX_NOREDIRECTIONBITMAP; + } + if self.contains(WindowFlags::CHILD) { + style |= WS_CHILD; // This is incompatible with WS_POPUP if that gets added eventually. + + // Remove decorations window styles for child + if !self.contains(WindowFlags::MARKER_DECORATIONS) { + style &= !(WS_CAPTION | WS_BORDER); + style_ex &= !WS_EX_WINDOWEDGE; + } + } + if self.contains(WindowFlags::POPUP) { + style |= WS_POPUP; + } + if self.contains(WindowFlags::MINIMIZED) { + style |= WS_MINIMIZE; + } + if self.contains(WindowFlags::MAXIMIZED) { + style |= WS_MAXIMIZE; + } + if self.contains(WindowFlags::IGNORE_CURSOR_EVENT) { + style_ex |= WS_EX_TRANSPARENT | WS_EX_LAYERED; + } + if self.contains(WindowFlags::CLIP_CHILDREN) { + style |= WS_CLIPCHILDREN; + } + + if self.intersects( + WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN + | WindowFlags::MARKER_BORDERLESS_FULLSCREEN, + ) { + style &= !WS_OVERLAPPEDWINDOW; + } + + (style, style_ex) + } + + /// Adjust the window client rectangle to the return value, if present. + fn apply_diff(mut self, window: HWND, mut new: WindowFlags) { + self = self.mask(); + new = new.mask(); + + let mut diff = self ^ new; + + if diff == WindowFlags::empty() { + return; + } + + if new.contains(WindowFlags::VISIBLE) { + let flag = if !self.contains(WindowFlags::MARKER_ACTIVATE) { + self.set(WindowFlags::MARKER_ACTIVATE, true); + SW_SHOWNOACTIVATE + } else { + SW_SHOW + }; + unsafe { + ShowWindow(window, flag); + } + } + + if diff.intersects(WindowFlags::ALWAYS_ON_TOP | WindowFlags::ALWAYS_ON_BOTTOM) { + unsafe { + SetWindowPos( + window, + match ( + new.contains(WindowFlags::ALWAYS_ON_TOP), + new.contains(WindowFlags::ALWAYS_ON_BOTTOM), + ) { + (true, false) => HWND_TOPMOST, + (false, false) => HWND_NOTOPMOST, + (false, true) => HWND_BOTTOM, + (true, true) => unreachable!(), + }, + 0, + 0, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, + ); + InvalidateRgn(window, 0, false.into()); + } + } + + if diff.contains(WindowFlags::MAXIMIZED) || new.contains(WindowFlags::MAXIMIZED) { + unsafe { + ShowWindow( + window, + match new.contains(WindowFlags::MAXIMIZED) { + true => SW_MAXIMIZE, + false => SW_RESTORE, + }, + ); + } + } + + // Minimize operations should execute after maximize for proper window animations + if diff.contains(WindowFlags::MINIMIZED) { + unsafe { + ShowWindow( + window, + match new.contains(WindowFlags::MINIMIZED) { + true => SW_MINIMIZE, + false => SW_RESTORE, + }, + ); + } + + diff.remove(WindowFlags::MINIMIZED); + } + + if diff.contains(WindowFlags::CLOSABLE) || new.contains(WindowFlags::CLOSABLE) { + let flags = MF_BYCOMMAND + | if new.contains(WindowFlags::CLOSABLE) { + MF_ENABLED + } else { + MF_DISABLED + }; + + unsafe { + EnableMenuItem(GetSystemMenu(window, 0), SC_CLOSE, flags); + } + } + + if !new.contains(WindowFlags::VISIBLE) { + unsafe { + ShowWindow(window, SW_HIDE); + } + } + + if diff != WindowFlags::empty() { + let (style, style_ex) = new.to_window_styles(); + + unsafe { + SendMessageW( + window, + event_loop::SET_RETAIN_STATE_ON_SIZE_MSG_ID.get(), + 1, + 0, + ); + + // This condition is necessary to avoid having an unrestorable window + if !new.contains(WindowFlags::MINIMIZED) { + SetWindowLongW(window, GWL_STYLE, style as i32); + SetWindowLongW(window, GWL_EXSTYLE, style_ex as i32); + } + + let mut flags = SWP_NOZORDER | SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED; + + // We generally don't want style changes here to affect window + // focus, but for fullscreen windows they must be activated + // (i.e. focused) so that they appear on top of the taskbar + if !new.contains(WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN) + && !new.contains(WindowFlags::MARKER_BORDERLESS_FULLSCREEN) + { + flags |= SWP_NOACTIVATE; + } + + // Refresh the window frame + SetWindowPos(window, 0, 0, 0, 0, 0, flags); + SendMessageW( + window, + event_loop::SET_RETAIN_STATE_ON_SIZE_MSG_ID.get(), + 0, + 0, + ); + } + } + } + + pub fn adjust_rect(self, hwnd: HWND, mut rect: RECT) -> Result { + unsafe { + let mut style = GetWindowLongW(hwnd, GWL_STYLE) as u32; + let style_ex = GetWindowLongW(hwnd, GWL_EXSTYLE) as u32; + + // Frameless style implemented by manually overriding the non-client area in + // `WM_NCCALCSIZE`. + if !self.contains(WindowFlags::MARKER_DECORATIONS) { + style &= !(WS_CAPTION | WS_SIZEBOX); + } + + util::win_to_err({ + let b_menu = GetMenu(hwnd) != 0; + if let (Some(get_dpi_for_window), Some(adjust_window_rect_ex_for_dpi)) = ( + *util::GET_DPI_FOR_WINDOW, + *util::ADJUST_WINDOW_RECT_EX_FOR_DPI, + ) { + let dpi = get_dpi_for_window(hwnd); + adjust_window_rect_ex_for_dpi( + &mut rect, + style, + b_menu.into(), + style_ex, + dpi, + ) + } else { + AdjustWindowRectEx(&mut rect, style, b_menu.into(), style_ex) + } + })?; + Ok(rect) + } + } + + pub fn adjust_size(self, hwnd: HWND, size: PhysicalSize) -> PhysicalSize { + let (width, height): (u32, u32) = size.into(); + let rect = RECT { + left: 0, + right: width as i32, + top: 0, + bottom: height as i32, + }; + let rect = self.adjust_rect(hwnd, rect).unwrap_or(rect); + + let outer_x = (rect.right - rect.left).abs(); + let outer_y = (rect.top - rect.bottom).abs(); + + PhysicalSize::new(outer_x as _, outer_y as _) + } + + pub fn set_size(self, hwnd: HWND, size: PhysicalSize) { + unsafe { + let (width, height): (u32, u32) = self.adjust_size(hwnd, size).into(); + SetWindowPos( + hwnd, + 0, + 0, + 0, + width as _, + height as _, + SWP_ASYNCWINDOWPOS + | SWP_NOZORDER + | SWP_NOREPOSITION + | SWP_NOMOVE + | SWP_NOACTIVATE, + ); + InvalidateRgn(hwnd, 0, false.into()); + } + } +} + +impl CursorFlags { + fn refresh_os_cursor(self, window: HWND) -> Result<(), io::Error> { + let client_rect = util::WindowArea::Inner.get_rect(window)?; + + if util::is_focused(window) { + let cursor_clip = match self.contains(CursorFlags::GRABBED) { + true => { + if self.contains(CursorFlags::HIDDEN) { + // Confine the cursor to the center of the window if the cursor is hidden. + // This avoids problems with the cursor activating + // the taskbar if the window borders or overlaps that. + let cx = (client_rect.left + client_rect.right) / 2; + let cy = (client_rect.top + client_rect.bottom) / 2; + Some(RECT { + left: cx, + right: cx + 1, + top: cy, + bottom: cy + 1, + }) + } else { + Some(client_rect) + } + } + false => None, + }; + + let rect_to_tuple = + |rect: RECT| (rect.left, rect.top, rect.right, rect.bottom); + let active_cursor_clip = rect_to_tuple(util::get_cursor_clip()?); + let desktop_rect = rect_to_tuple(util::get_desktop_rect()); + + let active_cursor_clip = match desktop_rect == active_cursor_clip { + true => None, + false => Some(active_cursor_clip), + }; + + // We do this check because calling `set_cursor_clip` incessantly will flood the event + // loop with `WM_MOUSEMOVE` events, and `refresh_os_cursor` is called by + // `set_cursor_flags` which at times gets called once every iteration of the + // eventloop. + if active_cursor_clip != cursor_clip.map(rect_to_tuple) { + util::set_cursor_clip(cursor_clip)?; + } + } + + let cursor_in_client = self.contains(CursorFlags::IN_WINDOW); + if cursor_in_client { + util::set_cursor_hidden(self.contains(CursorFlags::HIDDEN)); + } else { + util::set_cursor_hidden(false); + } + + Ok(()) + } +} diff --git a/rio-window/src/utils.rs b/rio-window/src/utils.rs new file mode 100644 index 00000000..e33f0ae3 --- /dev/null +++ b/rio-window/src/utils.rs @@ -0,0 +1,31 @@ +// A poly-fill for `lazy_cell` +// Replace with std::sync::LazyLock when https://github.com/rust-lang/rust/issues/109736 is stabilized. + +// This isn't used on every platform, which can come up as dead code warnings. +#![allow(dead_code)] + +use std::ops::Deref; +use std::sync::OnceLock; + +pub(crate) struct Lazy { + cell: OnceLock, + init: fn() -> T, +} + +impl Lazy { + pub const fn new(f: fn() -> T) -> Self { + Self { + cell: OnceLock::new(), + init: f, + } + } +} + +impl Deref for Lazy { + type Target = T; + + #[inline] + fn deref(&self) -> &'_ T { + self.cell.get_or_init(self.init) + } +} diff --git a/rio-window/src/window.rs b/rio-window/src/window.rs new file mode 100644 index 00000000..e2c625d8 --- /dev/null +++ b/rio-window/src/window.rs @@ -0,0 +1,1884 @@ +//! The [`Window`] struct and associated types. +use std::fmt; + +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError}; +use crate::monitor::{MonitorHandle, VideoModeHandle}; +use crate::platform_impl::{self, PlatformSpecificWindowAttributes}; + +pub use crate::cursor::{ + BadImage, Cursor, CustomCursor, CustomCursorSource, MAX_CURSOR_SIZE, +}; +pub use crate::icon::{BadIcon, Icon}; + +#[doc(inline)] +pub use cursor_icon::{CursorIcon, ParseError as CursorIconParseError}; + +/// Represents a window. +/// +/// The window is closed when dropped. +/// +/// ## Threading +/// +/// This is `Send + Sync`, meaning that it can be freely used from other +/// threads. +/// +/// However, some platforms (macOS, Web and iOS) only allow user interface +/// interactions on the main thread, so on those platforms, if you use the +/// window from a thread other than the main, the code is scheduled to run on +/// the main thread, and your thread may be blocked until that completes. +/// +/// ## Platform-specific +/// +/// **Web:** The [`Window`], which is represented by a `HTMLElementCanvas`, can +/// not be closed by dropping the [`Window`]. +pub struct Window { + pub(crate) window: platform_impl::Window, +} + +impl fmt::Debug for Window { + fn fmt(&self, fmtr: &mut fmt::Formatter<'_>) -> fmt::Result { + fmtr.pad("Window { .. }") + } +} + +impl Drop for Window { + /// This will close the [`Window`]. + /// + /// See [`Window`] for more details. + fn drop(&mut self) { + self.window.maybe_wait_on_main(|w| { + // If the window is in exclusive fullscreen, we must restore the desktop + // video mode (generally this would be done on application exit, but + // closing the window doesn't necessarily always mean application exit, + // such as when there are multiple windows) + if let Some(Fullscreen::Exclusive(_)) = w.fullscreen().map(|f| f.into()) { + w.set_fullscreen(None); + } + }) + } +} + +/// Identifier of a window. Unique for each window. +/// +/// Can be obtained with [`window.id()`][`Window::id`]. +/// +/// Whenever you receive an event specific to a window, this event contains a `WindowId` which you +/// can then compare to the ids of your windows. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(pub(crate) platform_impl::WindowId); + +impl WindowId { + /// Returns a dummy id, useful for unit testing. + /// + /// # Safety + /// + /// The only guarantee made about the return value of this function is that + /// it will always be equal to itself and to future values returned by this function. + /// No other guarantees are made. This may be equal to a real [`WindowId`]. + /// + /// **Passing this into a winit function will result in undefined behavior.** + pub const unsafe fn dummy() -> Self { + #[allow(unused_unsafe)] + WindowId(unsafe { platform_impl::WindowId::dummy() }) + } +} + +impl fmt::Debug for WindowId { + fn fmt(&self, fmtr: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(fmtr) + } +} + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0.into() + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id.into()) + } +} + +/// Attributes used when creating a window. +#[derive(Debug, Clone)] +pub struct WindowAttributes { + pub inner_size: Option, + pub min_inner_size: Option, + pub max_inner_size: Option, + pub position: Option, + pub resizable: bool, + pub enabled_buttons: WindowButtons, + pub title: String, + pub maximized: bool, + pub visible: bool, + pub transparent: bool, + pub blur: bool, + pub decorations: bool, + pub window_icon: Option, + pub preferred_theme: Option, + pub resize_increments: Option, + pub content_protected: bool, + pub window_level: WindowLevel, + pub active: bool, + pub cursor: Cursor, + #[cfg(feature = "rwh_06")] + pub(crate) parent_window: Option, + pub fullscreen: Option, + // Platform-specific configuration. + #[allow(dead_code)] + pub(crate) platform_specific: PlatformSpecificWindowAttributes, +} + +impl Default for WindowAttributes { + #[inline] + fn default() -> WindowAttributes { + WindowAttributes { + inner_size: None, + min_inner_size: None, + max_inner_size: None, + position: None, + resizable: true, + enabled_buttons: WindowButtons::all(), + title: "winit window".to_owned(), + maximized: false, + fullscreen: None, + visible: true, + transparent: false, + blur: false, + decorations: true, + window_level: Default::default(), + window_icon: None, + preferred_theme: None, + resize_increments: None, + content_protected: false, + cursor: Cursor::default(), + #[cfg(feature = "rwh_06")] + parent_window: None, + active: true, + platform_specific: Default::default(), + } + } +} + +/// Wrapper for [`rwh_06::RawWindowHandle`] for [`WindowAttributes::parent_window`]. +/// +/// # Safety +/// +/// The user has to account for that when using [`WindowAttributes::with_parent_window()`], +/// which is `unsafe`. +#[derive(Debug, Clone)] +#[cfg(feature = "rwh_06")] +pub(crate) struct SendSyncRawWindowHandle(pub(crate) rwh_06::RawWindowHandle); + +#[cfg(feature = "rwh_06")] +unsafe impl Send for SendSyncRawWindowHandle {} +#[cfg(feature = "rwh_06")] +unsafe impl Sync for SendSyncRawWindowHandle {} + +impl WindowAttributes { + /// Initializes new attributes with default values. + #[inline] + #[deprecated = "use `Window::default_attributes` instead"] + pub fn new() -> Self { + Default::default() + } +} + +impl WindowAttributes { + /// Get the parent window stored on the attributes. + #[cfg(feature = "rwh_06")] + pub fn parent_window(&self) -> Option<&rwh_06::RawWindowHandle> { + self.parent_window.as_ref().map(|handle| &handle.0) + } + + /// Requests the window to be of specific dimensions. + /// + /// If this is not set, some platform-specific dimensions will be used. + /// + /// See [`Window::request_inner_size`] for details. + #[inline] + pub fn with_inner_size>(mut self, size: S) -> Self { + self.inner_size = Some(size.into()); + self + } + + /// Sets the minimum dimensions a window can have. + /// + /// If this is not set, the window will have no minimum dimensions (aside + /// from reserved). + /// + /// See [`Window::set_min_inner_size`] for details. + #[inline] + pub fn with_min_inner_size>(mut self, min_size: S) -> Self { + self.min_inner_size = Some(min_size.into()); + self + } + + /// Sets the maximum dimensions a window can have. + /// + /// If this is not set, the window will have no maximum or will be set to + /// the primary monitor's dimensions by the platform. + /// + /// See [`Window::set_max_inner_size`] for details. + #[inline] + pub fn with_max_inner_size>(mut self, max_size: S) -> Self { + self.max_inner_size = Some(max_size.into()); + self + } + + /// Sets a desired initial position for the window. + /// + /// If this is not set, some platform-specific position will be chosen. + /// + /// See [`Window::set_outer_position`] for details. + /// + /// ## Platform-specific + /// + /// - **macOS:** The top left corner position of the window content, the window's "inner" + /// position. The window title bar will be placed above it. The window will be positioned such + /// that it fits on screen, maintaining set `inner_size` if any. If you need to precisely + /// position the top left corner of the whole window you have to use + /// [`Window::set_outer_position`] after creating the window. + /// - **Windows:** The top left corner position of the window title bar, the window's "outer" + /// position. There may be a small gap between this position and the window due to the + /// specifics of the Window Manager. + /// - **X11:** The top left corner of the window, the window's "outer" position. + /// - **Others:** Ignored. + #[inline] + pub fn with_position>(mut self, position: P) -> Self { + self.position = Some(position.into()); + self + } + + /// Sets whether the window is resizable or not. + /// + /// The default is `true`. + /// + /// See [`Window::set_resizable`] for details. + #[inline] + pub fn with_resizable(mut self, resizable: bool) -> Self { + self.resizable = resizable; + self + } + + /// Sets the enabled window buttons. + /// + /// The default is [`WindowButtons::all`] + /// + /// See [`Window::set_enabled_buttons`] for details. + #[inline] + pub fn with_enabled_buttons(mut self, buttons: WindowButtons) -> Self { + self.enabled_buttons = buttons; + self + } + + /// Sets the initial title of the window in the title bar. + /// + /// The default is `"winit window"`. + /// + /// See [`Window::set_title`] for details. + #[inline] + pub fn with_title>(mut self, title: T) -> Self { + self.title = title.into(); + self + } + + /// Sets whether the window should be put into fullscreen upon creation. + /// + /// The default is `None`. + /// + /// See [`Window::set_fullscreen`] for details. + #[inline] + pub fn with_fullscreen(mut self, fullscreen: Option) -> Self { + self.fullscreen = fullscreen; + self + } + + /// Request that the window is maximized upon creation. + /// + /// The default is `false`. + /// + /// See [`Window::set_maximized`] for details. + #[inline] + pub fn with_maximized(mut self, maximized: bool) -> Self { + self.maximized = maximized; + self + } + + /// Sets whether the window will be initially visible or hidden. + /// + /// The default is to show the window. + /// + /// See [`Window::set_visible`] for details. + #[inline] + pub fn with_visible(mut self, visible: bool) -> Self { + self.visible = visible; + self + } + + /// Sets whether the background of the window should be transparent. + /// + /// If this is `true`, writing colors with alpha values different than + /// `1.0` will produce a transparent window. On some platforms this + /// is more of a hint for the system and you'd still have the alpha + /// buffer. To control it see [`Window::set_transparent`]. + /// + /// The default is `false`. + #[inline] + pub fn with_transparent(mut self, transparent: bool) -> Self { + self.transparent = transparent; + self + } + + /// Sets whether the background of the window should be blurred by the system. + /// + /// The default is `false`. + /// + /// See [`Window::set_blur`] for details. + #[inline] + pub fn with_blur(mut self, blur: bool) -> Self { + self.blur = blur; + self + } + + /// Get whether the window will support transparency. + #[inline] + pub fn transparent(&self) -> bool { + self.transparent + } + + /// Sets whether the window should have a border, a title bar, etc. + /// + /// The default is `true`. + /// + /// See [`Window::set_decorations`] for details. + #[inline] + pub fn with_decorations(mut self, decorations: bool) -> Self { + self.decorations = decorations; + self + } + + /// Sets the window level. + /// + /// This is just a hint to the OS, and the system could ignore it. + /// + /// The default is [`WindowLevel::Normal`]. + /// + /// See [`WindowLevel`] for details. + #[inline] + pub fn with_window_level(mut self, level: WindowLevel) -> Self { + self.window_level = level; + self + } + + /// Sets the window icon. + /// + /// The default is `None`. + /// + /// See [`Window::set_window_icon`] for details. + #[inline] + pub fn with_window_icon(mut self, window_icon: Option) -> Self { + self.window_icon = window_icon; + self + } + + /// Sets a specific theme for the window. + /// + /// If `None` is provided, the window will use the system theme. + /// + /// The default is `None`. + /// + /// ## Platform-specific + /// + /// - **macOS:** This is an app-wide setting. + /// - **Wayland:** This controls only CSD. When using `None` it'll try to use dbus to get the + /// system preference. When explicit theme is used, this will avoid dbus all together. + /// - **x11:** Build window with `_GTK_THEME_VARIANT` hint set to `dark` or `light`. + /// - **iOS / Android / Web / x11 / Orbital:** Ignored. + #[inline] + pub fn with_theme(mut self, theme: Option) -> Self { + self.preferred_theme = theme; + self + } + + /// Build window with resize increments hint. + /// + /// The default is `None`. + /// + /// See [`Window::set_resize_increments`] for details. + #[inline] + pub fn with_resize_increments>(mut self, resize_increments: S) -> Self { + self.resize_increments = Some(resize_increments.into()); + self + } + + /// Prevents the window contents from being captured by other apps. + /// + /// The default is `false`. + /// + /// ## Platform-specific + /// + /// - **macOS**: if `false`, [`NSWindowSharingNone`] is used but doesn't completely + /// prevent all apps from reading the window content, for instance, QuickTime. + /// - **iOS / Android / Web / x11 / Orbital:** Ignored. + /// + /// [`NSWindowSharingNone`]: https://developer.apple.com/documentation/appkit/nswindowsharingtype/nswindowsharingnone + #[inline] + pub fn with_content_protected(mut self, protected: bool) -> Self { + self.content_protected = protected; + self + } + + /// Whether the window will be initially focused or not. + /// + /// The window should be assumed as not focused by default + /// following by the [`WindowEvent::Focused`]. + /// + /// ## Platform-specific: + /// + /// **Android / iOS / X11 / Wayland / Orbital:** Unsupported. + /// + /// [`WindowEvent::Focused`]: crate::event::WindowEvent::Focused. + #[inline] + pub fn with_active(mut self, active: bool) -> Self { + self.active = active; + self + } + + /// Modifies the cursor icon of the window. + /// + /// The default is [`CursorIcon::Default`]. + /// + /// See [`Window::set_cursor()`] for more details. + #[inline] + pub fn with_cursor(mut self, cursor: impl Into) -> Self { + self.cursor = cursor.into(); + self + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub unsafe fn with_parent_window( + mut self, + parent_window: Option, + ) -> Self { + self.parent_window = parent_window.map(SendSyncRawWindowHandle); + self + } +} + +/// Base Window functions. +impl Window { + /// Create a new [`WindowAttributes`] which allows modifying the window's attributes before + /// creation. + #[inline] + pub fn default_attributes() -> WindowAttributes { + WindowAttributes::default() + } + + /// Returns an identifier unique to the window. + #[inline] + pub fn id(&self) -> WindowId { + let _span = tracing::debug_span!("rio_window::Window::id",).entered(); + + self.window.maybe_wait_on_main(|w| WindowId(w.id())) + } + + /// Returns the scale factor that can be used to map logical pixels to physical pixels, and + /// vice versa. + /// + /// Note that this value can change depending on user action (for example if the window is + /// moved to another screen); as such, tracking [`WindowEvent::ScaleFactorChanged`] events is + /// the most robust way to track the DPI you need to use to draw. + /// + /// This value may differ from [`MonitorHandle::scale_factor`]. + /// + /// See the [`dpi`] crate for more information. + /// + /// ## Platform-specific + /// + /// The scale factor is calculated differently on different platforms: + /// + /// - **Windows:** On Windows 8 and 10, per-monitor scaling is readily configured by users from + /// the display settings. While users are free to select any option they want, they're only + /// given a selection of "nice" scale factors, i.e. 1.0, 1.25, 1.5... on Windows 7. The scale + /// factor is global and changing it requires logging out. See [this article][windows_1] for + /// technical details. + /// - **macOS:** Recent macOS versions allow the user to change the scaling factor for specific + /// displays. When available, the user may pick a per-monitor scaling factor from a set of + /// pre-defined settings. All "retina displays" have a scaling factor above 1.0 by default, + /// but the specific value varies across devices. + /// - **X11:** Many man-hours have been spent trying to figure out how to handle DPI in X11. + /// Winit currently uses a three-pronged approach: + /// + Use the value in the `WINIT_X11_SCALE_FACTOR` environment variable if present. + /// + If not present, use the value set in `Xft.dpi` in Xresources. + /// + Otherwise, calculate the scale factor based on the millimeter monitor dimensions + /// provided by XRandR. + /// + /// If `WINIT_X11_SCALE_FACTOR` is set to `randr`, it'll ignore the `Xft.dpi` field and use + /// the XRandR scaling method. Generally speaking, you should try to configure the + /// standard system variables to do what you want before resorting to + /// `WINIT_X11_SCALE_FACTOR`. + /// - **Wayland:** The scale factor is suggested by the compositor for each window individually + /// by using the wp-fractional-scale protocol if available. Falls back to integer-scale + /// factors otherwise. + /// + /// The monitor scale factor may differ from the window scale factor. + /// - **iOS:** Scale factors are set by Apple to the value that best suits the device, and range + /// from `1.0` to `3.0`. See [this article][apple_1] and [this article][apple_2] for more + /// information. + /// + /// This uses the underlying `UIView`'s [`contentScaleFactor`]. + /// - **Android:** Scale factors are set by the manufacturer to the value that best suits the + /// device, and range from `1.0` to `4.0`. See [this article][android_1] for more information. + /// + /// This is currently unimplemented, and this function always returns 1.0. + /// - **Web:** The scale factor is the ratio between CSS pixels and the physical device pixels. + /// In other words, it is the value of [`window.devicePixelRatio`][web_1]. It is affected by + /// both the screen scaling and the browser zoom level and can go below `1.0`. + /// - **Orbital:** This is currently unimplemented, and this function always returns 1.0. + /// + /// [`WindowEvent::ScaleFactorChanged`]: crate::event::WindowEvent::ScaleFactorChanged + /// [windows_1]: https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows + /// [apple_1]: https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html + /// [apple_2]: https://developer.apple.com/design/human-interface-guidelines/macos/icons-and-images/image-size-and-resolution/ + /// [android_1]: https://developer.android.com/training/multiscreen/screendensities + /// [web_1]: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio + /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc + #[inline] + pub fn scale_factor(&self) -> f64 { + let _span = tracing::debug_span!("rio_window::Window::scale_factor",).entered(); + + self.window.maybe_wait_on_main(|w| w.scale_factor()) + } + + /// Queues a [`WindowEvent::RedrawRequested`] event to be emitted that aligns with the windowing + /// system drawing loop. + /// + /// This is the **strongly encouraged** method of redrawing windows, as it can integrate with + /// OS-requested redraws (e.g. when a window gets resized). To improve the event delivery + /// consider using [`Window::pre_present_notify`] as described in docs. + /// + /// Applications should always aim to redraw whenever they receive a `RedrawRequested` event. + /// + /// There are no strong guarantees about when exactly a `RedrawRequest` event will be emitted + /// with respect to other events, since the requirements can vary significantly between + /// windowing systems. + /// + /// However as the event aligns with the windowing system drawing loop, it may not arrive in + /// same or even next event loop iteration. + /// + /// ## Platform-specific + /// + /// - **Windows** This API uses `RedrawWindow` to request a `WM_PAINT` message and + /// `RedrawRequested` is emitted in sync with any `WM_PAINT` messages. + /// - **iOS:** Can only be called on the main thread. + /// - **Wayland:** The events are aligned with the frame callbacks when + /// [`Window::pre_present_notify`] is used. + /// - **Web:** [`WindowEvent::RedrawRequested`] will be aligned with the + /// `requestAnimationFrame`. + /// + /// [`WindowEvent::RedrawRequested`]: crate::event::WindowEvent::RedrawRequested + #[inline] + pub fn request_redraw(&self) { + let _span = tracing::debug_span!("rio_window::Window::request_redraw",).entered(); + + self.window.maybe_queue_on_main(|w| w.request_redraw()) + } + + /// Notify the windowing system before presenting to the window. + /// + /// You should call this event after your drawing operations, but before you submit + /// the buffer to the display or commit your drawings. Doing so will help winit to properly + /// schedule and make assumptions about its internal state. For example, it could properly + /// throttle [`WindowEvent::RedrawRequested`]. + /// + /// ## Example + /// + /// This example illustrates how it looks with OpenGL, but it applies to other graphics + /// APIs and software rendering. + /// + /// ```no_run + /// # use rio_window::window::Window; + /// # fn swap_buffers() {} + /// # fn scope(window: &Window) { + /// // Do the actual drawing with OpenGL. + /// + /// // Notify winit that we're about to submit buffer to the windowing system. + /// window.pre_present_notify(); + /// + /// // Submit buffer to the windowing system. + /// swap_buffers(); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// **Wayland:** - schedules a frame callback to throttle [`WindowEvent::RedrawRequested`]. + /// + /// [`WindowEvent::RedrawRequested`]: crate::event::WindowEvent::RedrawRequested + #[inline] + pub fn pre_present_notify(&self) { + let _span = + tracing::debug_span!("rio_window::Window::pre_present_notify",).entered(); + + self.window.maybe_queue_on_main(|w| w.pre_present_notify()); + } + + /// Reset the dead key state of the keyboard. + /// + /// This is useful when a dead key is bound to trigger an action. Then + /// this function can be called to reset the dead key state so that + /// follow-up text input won't be affected by the dead key. + /// + /// ## Platform-specific + /// - **Web, macOS:** Does nothing + // --------------------------- + // Developers' Note: If this cannot be implemented on every desktop platform + // at least, then this function should be provided through a platform specific + // extension trait + pub fn reset_dead_keys(&self) { + let _span = + tracing::debug_span!("rio_window::Window::reset_dead_keys",).entered(); + + self.window.maybe_queue_on_main(|w| w.reset_dead_keys()) + } +} + +/// Position and size functions. +impl Window { + /// Returns the position of the top-left hand corner of the window's client area relative to the + /// top-left hand corner of the desktop. + /// + /// The same conditions that apply to [`Window::outer_position`] apply to this method. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the top left coordinates of the + /// window's [safe area] in the screen space coordinate system. + /// - **Web:** Returns the top-left coordinates relative to the viewport. _Note: this returns + /// the same value as [`Window::outer_position`]._ + /// - **Android / Wayland:** Always returns [`NotSupportedError`]. + /// + /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + let _span = tracing::debug_span!("rio_window::Window::inner_position",).entered(); + + self.window.maybe_wait_on_main(|w| w.inner_position()) + } + + /// Returns the position of the top-left hand corner of the window relative to the + /// top-left hand corner of the desktop. + /// + /// Note that the top-left hand corner of the desktop is not necessarily the same as + /// the screen. If the user uses a desktop with multiple monitors, the top-left hand corner + /// of the desktop is the top-left hand corner of the monitor at the top-left of the desktop. + /// + /// The coordinates can be negative if the top-left hand corner of the window is outside + /// of the visible screen region. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the top left coordinates of the + /// window in the screen space coordinate system. + /// - **Web:** Returns the top-left coordinates relative to the viewport. + /// - **Android / Wayland:** Always returns [`NotSupportedError`]. + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + let _span = tracing::debug_span!("rio_window::Window::outer_position",).entered(); + + self.window.maybe_wait_on_main(|w| w.outer_position()) + } + + /// Modifies the position of the window. + /// + /// See [`Window::outer_position`] for more information about the coordinates. + /// This automatically un-maximizes the window if it's maximized. + /// + /// ```no_run + /// # use rio_window::dpi::{LogicalPosition, PhysicalPosition}; + /// # use rio_window::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the position in logical dimensions like this: + /// window.set_outer_position(LogicalPosition::new(400.0, 200.0)); + /// + /// // Or specify the position in physical dimensions like this: + /// window.set_outer_position(PhysicalPosition::new(400, 200)); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Sets the top left coordinates of the + /// window in the screen space coordinate system. + /// - **Web:** Sets the top-left coordinates relative to the viewport. Doesn't account for CSS + /// [`transform`]. + /// - **Android / Wayland:** Unsupported. + /// + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + #[inline] + pub fn set_outer_position>(&self, position: P) { + let position = position.into(); + let _span = tracing::debug_span!( + "rio_window::Window::set_outer_position", + position = ?position + ) + .entered(); + + self.window + .maybe_queue_on_main(move |w| w.set_outer_position(position)) + } + + /// Returns the physical size of the window's client area. + /// + /// The client area is the content of the window, excluding the title bar and borders. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the `PhysicalSize` of the window's + /// [safe area] in screen space coordinates. + /// - **Web:** Returns the size of the canvas element. Doesn't account for CSS [`transform`]. + /// + /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let _span = tracing::debug_span!("rio_window::Window::inner_size",).entered(); + + self.window.maybe_wait_on_main(|w| w.inner_size()) + } + + /// Request the new size for the window. + /// + /// On platforms where the size is entirely controlled by the user the + /// applied size will be returned immediately, resize event in such case + /// may not be generated. + /// + /// On platforms where resizing is disallowed by the windowing system, the current + /// inner size is returned immediately, and the user one is ignored. + /// + /// When `None` is returned, it means that the request went to the display system, + /// and the actual size will be delivered later with the [`WindowEvent::Resized`]. + /// + /// See [`Window::inner_size`] for more information about the values. + /// + /// The request could automatically un-maximize the window if it's maximized. + /// + /// ```no_run + /// # use rio_window::dpi::{LogicalSize, PhysicalSize}; + /// # use rio_window::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the size in logical dimensions like this: + /// let _ = window.request_inner_size(LogicalSize::new(400.0, 200.0)); + /// + /// // Or specify the size in physical dimensions like this: + /// let _ = window.request_inner_size(PhysicalSize::new(400, 200)); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **Web:** Sets the size of the canvas element. Doesn't account for CSS [`transform`]. + /// + /// [`WindowEvent::Resized`]: crate::event::WindowEvent::Resized + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + #[inline] + #[must_use] + pub fn request_inner_size>( + &self, + size: S, + ) -> Option> { + let size = size.into(); + let _span = tracing::debug_span!( + "rio_window::Window::request_inner_size", + size = ?size + ) + .entered(); + self.window + .maybe_wait_on_main(|w| w.request_inner_size(size)) + } + + /// Returns the physical size of the entire window. + /// + /// These dimensions include the title bar and borders. If you don't want that (and you usually + /// don't), use [`Window::inner_size`] instead. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the [`PhysicalSize`] of the window + /// in screen space coordinates. + /// - **Web:** Returns the size of the canvas element. _Note: this returns the same value as + /// [`Window::inner_size`]._ + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + let _span = tracing::debug_span!("rio_window::Window::outer_size",).entered(); + self.window.maybe_wait_on_main(|w| w.outer_size()) + } + + /// Sets a minimum dimension size for the window. + /// + /// ```no_run + /// # use rio_window::dpi::{LogicalSize, PhysicalSize}; + /// # use rio_window::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the size in logical dimensions like this: + /// window.set_min_inner_size(Some(LogicalSize::new(400.0, 200.0))); + /// + /// // Or specify the size in physical dimensions like this: + /// window.set_min_inner_size(Some(PhysicalSize::new(400, 200))); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Orbital:** Unsupported. + #[inline] + pub fn set_min_inner_size>(&self, min_size: Option) { + let min_size = min_size.map(|s| s.into()); + let _span = tracing::debug_span!( + "rio_window::Window::set_min_inner_size", + min_size = ?min_size + ) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_min_inner_size(min_size)) + } + + /// Sets a maximum dimension size for the window. + /// + /// ```no_run + /// # use rio_window::dpi::{LogicalSize, PhysicalSize}; + /// # use rio_window::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the size in logical dimensions like this: + /// window.set_max_inner_size(Some(LogicalSize::new(400.0, 200.0))); + /// + /// // Or specify the size in physical dimensions like this: + /// window.set_max_inner_size(Some(PhysicalSize::new(400, 200))); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Orbital:** Unsupported. + #[inline] + pub fn set_max_inner_size>(&self, max_size: Option) { + let max_size = max_size.map(|s| s.into()); + let _span = tracing::debug_span!( + "rio_window::Window::max_size", + max_size = ?max_size + ) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_max_inner_size(max_size)) + } + + /// Returns window resize increments if any were set. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Wayland / Orbital:** Always returns [`None`]. + #[inline] + pub fn resize_increments(&self) -> Option> { + let _span = + tracing::debug_span!("rio_window::Window::resize_increments",).entered(); + self.window.maybe_wait_on_main(|w| w.resize_increments()) + } + + /// Sets window resize increments. + /// + /// This is a niche constraint hint usually employed by terminal emulators + /// and other apps that need "blocky" resizes. + /// + /// ## Platform-specific + /// + /// - **macOS:** Increments are converted to logical size and then macOS rounds them to whole + /// numbers. + /// - **Wayland:** Not implemented. + /// - **iOS / Android / Web / Orbital:** Unsupported. + #[inline] + pub fn set_resize_increments>(&self, increments: Option) { + let increments = increments.map(Into::into); + let _span = tracing::debug_span!( + "rio_window::Window::set_resize_increments", + increments = ?increments + ) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_resize_increments(increments)) + } +} + +/// Misc. attribute functions. +impl Window { + /// Modifies the title of the window. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_title(&self, title: &str) { + let _span = + tracing::debug_span!("rio_window::Window::set_title", title).entered(); + self.window.maybe_wait_on_main(|w| w.set_title(title)) + } + + #[inline] + #[cfg(target_os = "macos")] + pub fn set_subtitle(&self, title: &str) { + let _span = + tracing::debug_span!("rio_window::Window::set_title", title).entered(); + self.window.maybe_wait_on_main(|w| w.set_subtitle(title)) + } + + /// Change the window transparency state. + /// + /// This is just a hint that may not change anything about + /// the window transparency, however doing a mismatch between + /// the content of your window and this hint may result in + /// visual artifacts. + /// + /// The default value follows the [`WindowAttributes::with_transparent`]. + /// + /// ## Platform-specific + /// + /// - **macOS:** If you're not drawing to the window yourself, you might have to set the + /// background color of the window to enable transparency. + /// - **Web / iOS / Android:** Unsupported. + /// - **X11:** Can only be set while building the window, with + /// [`WindowAttributes::with_transparent`]. + #[inline] + pub fn set_transparent(&self, transparent: bool) { + let _span = + tracing::debug_span!("rio_window::Window::set_transparent", transparent) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_transparent(transparent)) + } + + /// Change the window blur state. + /// + /// If `true`, this will make the transparent window background blurry. + /// + /// ## Platform-specific + /// + /// - **Android / iOS / X11 / Web / Windows:** Unsupported. + /// - **Wayland:** Only works with org_kde_kwin_blur_manager protocol. + #[inline] + pub fn set_blur(&self, blur: bool) { + let _span = tracing::debug_span!("rio_window::Window::set_blur", blur).entered(); + self.window.maybe_queue_on_main(move |w| w.set_blur(blur)) + } + + /// Modifies the window's visibility. + /// + /// If `false`, this will hide the window. If `true`, this will show the window. + /// + /// ## Platform-specific + /// + /// - **Android / Wayland / Web:** Unsupported. + /// - **iOS:** Can only be called on the main thread. + #[inline] + pub fn set_visible(&self, visible: bool) { + let _span = + tracing::debug_span!("rio_window::Window::set_visible", visible).entered(); + self.window + .maybe_queue_on_main(move |w| w.set_visible(visible)) + } + + /// Gets the window's current visibility state. + /// + /// `None` means it couldn't be determined, so it is not recommended to use this to drive your + /// rendering backend. + /// + /// ## Platform-specific + /// + /// - **X11:** Not implemented. + /// - **Wayland / iOS / Android / Web:** Unsupported. + #[inline] + pub fn is_visible(&self) -> Option { + let _span = tracing::debug_span!("rio_window::Window::is_visible",).entered(); + self.window.maybe_wait_on_main(|w| w.is_visible()) + } + + /// Sets whether the window is resizable or not. + /// + /// Note that making the window unresizable doesn't exempt you from handling + /// [`WindowEvent::Resized`], as that event can still be triggered by DPI scaling, entering + /// fullscreen mode, etc. Also, the window could still be resized by calling + /// [`Window::request_inner_size`]. + /// + /// ## Platform-specific + /// + /// This only has an effect on desktop platforms. + /// + /// - **X11:** Due to a bug in XFCE, this has no effect on Xfwm. + /// - **iOS / Android / Web:** Unsupported. + /// + /// [`WindowEvent::Resized`]: crate::event::WindowEvent::Resized + #[inline] + pub fn set_resizable(&self, resizable: bool) { + let _span = tracing::debug_span!("rio_window::Window::set_resizable", resizable) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_resizable(resizable)) + } + + /// Gets the window's current resizable state. + /// + /// ## Platform-specific + /// + /// - **X11:** Not implemented. + /// - **iOS / Android / Web:** Unsupported. + #[inline] + pub fn is_resizable(&self) -> bool { + let _span = tracing::debug_span!("rio_window::Window::is_resizable",).entered(); + self.window.maybe_wait_on_main(|w| w.is_resizable()) + } + + /// Sets the enabled window buttons. + /// + /// ## Platform-specific + /// + /// - **Wayland / X11 / Orbital:** Not implemented. + /// - **Web / iOS / Android:** Unsupported. + pub fn set_enabled_buttons(&self, buttons: WindowButtons) { + let _span = tracing::debug_span!( + "rio_window::Window::set_enabled_buttons", + buttons = ?buttons + ) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_enabled_buttons(buttons)) + } + + /// Gets the enabled window buttons. + /// + /// ## Platform-specific + /// + /// - **Wayland / X11 / Orbital:** Not implemented. Always returns [`WindowButtons::all`]. + /// - **Web / iOS / Android:** Unsupported. Always returns [`WindowButtons::all`]. + pub fn enabled_buttons(&self) -> WindowButtons { + let _span = + tracing::debug_span!("rio_window::Window::enabled_buttons",).entered(); + self.window.maybe_wait_on_main(|w| w.enabled_buttons()) + } + + /// Sets the window to minimized or back + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Orbital:** Unsupported. + /// - **Wayland:** Un-minimize is unsupported. + #[inline] + pub fn set_minimized(&self, minimized: bool) { + let _span = tracing::debug_span!("rio_window::Window::set_minimized", minimized) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_minimized(minimized)) + } + + /// Gets the window's current minimized state. + /// + /// `None` will be returned, if the minimized state couldn't be determined. + /// + /// ## Note + /// + /// - You shouldn't stop rendering for minimized windows, however you could lower the fps. + /// + /// ## Platform-specific + /// + /// - **Wayland**: always `None`. + /// - **iOS / Android / Web / Orbital:** Unsupported. + #[inline] + pub fn is_minimized(&self) -> Option { + let _span = tracing::debug_span!("rio_window::Window::is_minimized",).entered(); + self.window.maybe_wait_on_main(|w| w.is_minimized()) + } + + /// Sets the window to maximized or back. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** Unsupported. + #[inline] + pub fn set_maximized(&self, maximized: bool) { + let _span = tracing::debug_span!("rio_window::Window::set_maximized", maximized) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_maximized(maximized)) + } + + /// Gets the window's current maximized state. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** Unsupported. + #[inline] + pub fn is_maximized(&self) -> bool { + let _span = tracing::debug_span!("rio_window::Window::is_maximized",).entered(); + self.window.maybe_wait_on_main(|w| w.is_maximized()) + } + + /// Sets the window to fullscreen or back. + /// + /// ## Platform-specific + /// + /// - **macOS:** [`Fullscreen::Exclusive`] provides true exclusive mode with a video mode + /// change. *Caveat!* macOS doesn't provide task switching (or spaces!) while in exclusive + /// fullscreen mode. This mode should be used when a video mode change is desired, but for a + /// better user experience, borderless fullscreen might be preferred. + /// + /// [`Fullscreen::Borderless`] provides a borderless fullscreen window on a + /// separate space. This is the idiomatic way for fullscreen games to work + /// on macOS. See `WindowExtMacOs::set_simple_fullscreen` if + /// separate spaces are not preferred. + /// + /// The dock and the menu bar are disabled in exclusive fullscreen mode. + /// - **iOS:** Can only be called on the main thread. + /// - **Wayland:** Does not support exclusive fullscreen mode and will no-op a request. + /// - **Windows:** Screen saver is disabled in fullscreen mode. + /// - **Android / Orbital:** Unsupported. + /// - **Web:** Does nothing without a [transient activation]. + /// + /// [transient activation]: https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation + #[inline] + pub fn set_fullscreen(&self, fullscreen: Option) { + let _span = tracing::debug_span!( + "rio_window::Window::set_fullscreen", + fullscreen = ?fullscreen + ) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_fullscreen(fullscreen.map(|f| f.into()))) + } + + /// Gets the window's current fullscreen state. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. + /// - **Android / Orbital:** Will always return `None`. + /// - **Wayland:** Can return `Borderless(None)` when there are no monitors. + /// - **Web:** Can only return `None` or `Borderless(None)`. + #[inline] + pub fn fullscreen(&self) -> Option { + let _span = tracing::debug_span!("rio_window::Window::fullscreen",).entered(); + self.window + .maybe_wait_on_main(|w| w.fullscreen().map(|f| f.into())) + } + + /// Turn window decorations on or off. + /// + /// Enable/disable window decorations provided by the server or Winit. + /// By default this is enabled. Note that fullscreen windows and windows on + /// mobile and web platforms naturally do not have decorations. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** No effect. + #[inline] + pub fn set_decorations(&self, decorations: bool) { + let _span = + tracing::debug_span!("rio_window::Window::set_decorations", decorations) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_decorations(decorations)) + } + + /// Gets the window's current decorations state. + /// + /// Returns `true` when windows are decorated (server-side or by Winit). + /// Also returns `true` when no decorations are required (mobile, web). + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** Always returns `true`. + #[inline] + pub fn is_decorated(&self) -> bool { + let _span = tracing::debug_span!("rio_window::Window::is_decorated",).entered(); + self.window.maybe_wait_on_main(|w| w.is_decorated()) + } + + /// Change the window level. + /// + /// This is just a hint to the OS, and the system could ignore it. + /// + /// See [`WindowLevel`] for details. + pub fn set_window_level(&self, level: WindowLevel) { + let _span = tracing::debug_span!( + "rio_window::Window::set_window_level", + level = ?level + ) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_window_level(level)) + } + + /// Sets the window icon. + /// + /// On Windows and X11, this is typically the small icon in the top-left + /// corner of the titlebar. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Wayland / macOS / Orbital:** Unsupported. + /// + /// - **Windows:** Sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's + /// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. + /// + /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. + /// That said, it's usually in the same ballpark as on Windows. + #[inline] + pub fn set_window_icon(&self, window_icon: Option) { + let _span = + tracing::debug_span!("rio_window::Window::set_window_icon",).entered(); + self.window + .maybe_queue_on_main(move |w| w.set_window_icon(window_icon)) + } + + /// Set the IME cursor editing area, where the `position` is the top left corner of that area + /// and `size` is the size of this area starting from the position. An example of such area + /// could be a input field in the UI or line in the editor. + /// + /// The windowing system could place a candidate box close to that area, but try to not obscure + /// the specified area, so the user input to it stays visible. + /// + /// The candidate box is the window / popup / overlay that allows you to select the desired + /// characters. The look of this box may differ between input devices, even on the same + /// platform. + /// + /// (Apple's official term is "candidate window", see their [chinese] and [japanese] guides). + /// + /// ## Example + /// + /// ```no_run + /// # use rio_window::dpi::{LogicalPosition, PhysicalPosition, LogicalSize, PhysicalSize}; + /// # use rio_window::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the position in logical dimensions like this: + /// window.set_ime_cursor_area(LogicalPosition::new(400.0, 200.0), LogicalSize::new(100, 100)); + /// + /// // Or specify the position in physical dimensions like this: + /// window.set_ime_cursor_area(PhysicalPosition::new(400, 200), PhysicalSize::new(100, 100)); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **X11:** - area is not supported, only position. + /// - **iOS / Android / Web / Orbital:** Unsupported. + /// + /// [chinese]: https://support.apple.com/guide/chinese-input-method/use-the-candidate-window-cim12992/104/mac/12.0 + /// [japanese]: https://support.apple.com/guide/japanese-input-method/use-the-candidate-window-jpim10262/6.3/mac/12.0 + #[inline] + pub fn set_ime_cursor_area, S: Into>( + &self, + position: P, + size: S, + ) { + let position = position.into(); + let size = size.into(); + let _span = tracing::debug_span!( + "rio_window::Window::set_ime_cursor_area", + position = ?position, + size = ?size, + ) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_ime_cursor_area(position, size)) + } + + /// Sets whether the window should get IME events + /// + /// When IME is allowed, the window will receive [`Ime`] events, and during the + /// preedit phase the window will NOT get [`KeyboardInput`] events. The window + /// should allow IME while it is expecting text input. + /// + /// When IME is not allowed, the window won't receive [`Ime`] events, and will + /// receive [`KeyboardInput`] events for every keypress instead. Not allowing + /// IME is useful for games for example. + /// + /// IME is **not** allowed by default. + /// + /// ## Platform-specific + /// + /// - **macOS:** IME must be enabled to receive text-input where dead-key sequences are + /// combined. + /// - **iOS / Android / Web / Orbital:** Unsupported. + /// - **X11**: Enabling IME will disable dead keys reporting during compose. + /// + /// [`Ime`]: crate::event::WindowEvent::Ime + /// [`KeyboardInput`]: crate::event::WindowEvent::KeyboardInput + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + let _span = tracing::debug_span!("rio_window::Window::set_ime_allowed", allowed) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_ime_allowed(allowed)) + } + + /// Sets the IME purpose for the window using [`ImePurpose`]. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported. + #[inline] + pub fn set_ime_purpose(&self, purpose: ImePurpose) { + let _span = tracing::debug_span!( + "rio_window::Window::set_ime_purpose", + purpose = ?purpose + ) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_ime_purpose(purpose)) + } + + /// Brings the window to the front and sets input focus. Has no effect if the window is + /// already in focus, minimized, or not visible. + /// + /// This method steals input focus from other applications. Do not use this method unless + /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive + /// user experience. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Wayland / Orbital:** Unsupported. + #[inline] + pub fn focus_window(&self) { + let _span = tracing::debug_span!("rio_window::Window::focus_window",).entered(); + self.window.maybe_queue_on_main(|w| w.focus_window()) + } + + /// Gets whether the window has keyboard focus. + /// + /// This queries the same state information as [`WindowEvent::Focused`]. + /// + /// [`WindowEvent::Focused`]: crate::event::WindowEvent::Focused + #[inline] + pub fn has_focus(&self) -> bool { + let _span = tracing::debug_span!("rio_window::Window::has_focus",).entered(); + self.window.maybe_wait_on_main(|w| w.has_focus()) + } + + /// Requests user attention to the window, this has no effect if the application + /// is already focused. How requesting for user attention manifests is platform dependent, + /// see [`UserAttentionType`] for details. + /// + /// Providing `None` will unset the request for user attention. Unsetting the request for + /// user attention might not be done automatically by the WM when the window receives input. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Orbital:** Unsupported. + /// - **macOS:** `None` has no effect. + /// - **X11:** Requests for user attention must be manually cleared. + /// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect. + #[inline] + pub fn request_user_attention(&self, request_type: Option) { + let _span = tracing::debug_span!( + "rio_window::Window::request_user_attention", + request_type = ?request_type + ) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.request_user_attention(request_type)) + } + + /// Sets the current window theme. Use `None` to fallback to system default. + /// + /// ## Platform-specific + /// + /// - **macOS:** This is an app-wide setting. + /// - **Wayland:** Sets the theme for the client side decorations. Using `None` will use dbus to + /// get the system preference. + /// - **X11:** Sets `_GTK_THEME_VARIANT` hint to `dark` or `light` and if `None` is used, it + /// will default to [`Theme::Dark`]. + /// - **iOS / Android / Web / Orbital:** Unsupported. + #[inline] + pub fn set_theme(&self, theme: Option) { + let _span = tracing::debug_span!( + "rio_window::Window::set_theme", + theme = ?theme + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_theme(theme)) + } + + /// Returns the current window theme. + /// + /// ## Platform-specific + /// + /// - **macOS:** This is an app-wide setting. + /// - **iOS / Android / Wayland / x11 / Orbital:** Unsupported. + #[inline] + pub fn theme(&self) -> Option { + let _span = tracing::debug_span!("rio_window::Window::theme",).entered(); + self.window.maybe_wait_on_main(|w| w.theme()) + } + + /// Prevents the window contents from being captured by other apps. + /// + /// ## Platform-specific + /// + /// - **macOS**: if `false`, [`NSWindowSharingNone`] is used but doesn't completely + /// prevent all apps from reading the window content, for instance, QuickTime. + /// - **iOS / Android / x11 / Wayland / Web / Orbital:** Unsupported. + /// + /// [`NSWindowSharingNone`]: https://developer.apple.com/documentation/appkit/nswindowsharingtype/nswindowsharingnone + pub fn set_content_protected(&self, protected: bool) { + let _span = + tracing::debug_span!("rio_window::Window::set_content_protected", protected) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_content_protected(protected)) + } + + /// Gets the current title of the window. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / x11 / Wayland / Web:** Unsupported. Always returns an empty string. + #[inline] + pub fn title(&self) -> String { + let _span = tracing::debug_span!("rio_window::Window::title",).entered(); + self.window.maybe_wait_on_main(|w| w.title()) + } +} + +/// Cursor functions. +impl Window { + /// Modifies the cursor icon of the window. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Orbital:** Unsupported. + /// - **Web:** Custom cursors have to be loaded and decoded first, until then the previous + /// cursor is shown. + #[inline] + pub fn set_cursor(&self, cursor: impl Into) { + let cursor = cursor.into(); + let _span = tracing::debug_span!("rio_window::Window::set_cursor",).entered(); + self.window + .maybe_queue_on_main(move |w| w.set_cursor(cursor)) + } + + /// Deprecated! Use [`Window::set_cursor()`] instead. + #[deprecated = "Renamed to `set_cursor`"] + #[inline] + pub fn set_cursor_icon(&self, icon: CursorIcon) { + self.set_cursor(icon) + } + + /// Changes the position of the cursor in window coordinates. + /// + /// ```no_run + /// # use rio_window::dpi::{LogicalPosition, PhysicalPosition}; + /// # use rio_window::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the position in logical dimensions like this: + /// window.set_cursor_position(LogicalPosition::new(400.0, 200.0)); + /// + /// // Or specify the position in physical dimensions like this: + /// window.set_cursor_position(PhysicalPosition::new(400, 200)); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **Wayland**: Cursor must be in [`CursorGrabMode::Locked`]. + /// - **iOS / Android / Web / Orbital:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn set_cursor_position>( + &self, + position: P, + ) -> Result<(), ExternalError> { + let position = position.into(); + let _span = tracing::debug_span!( + "rio_window::Window::set_cursor_position", + position = ?position + ) + .entered(); + self.window + .maybe_wait_on_main(|w| w.set_cursor_position(position)) + } + + /// Set grabbing [mode][CursorGrabMode] on the cursor preventing it from leaving the window. + /// + /// # Example + /// + /// First try confining the cursor, and if that fails, try locking it instead. + /// + /// ```no_run + /// # use rio_window::window::{CursorGrabMode, Window}; + /// # fn scope(window: &Window) { + /// window + /// .set_cursor_grab(CursorGrabMode::Confined) + /// .or_else(|_e| window.set_cursor_grab(CursorGrabMode::Locked)) + /// .unwrap(); + /// # } + /// ``` + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let _span = tracing::debug_span!( + "rio_window::Window::set_cursor_grab", + mode = ?mode + ) + .entered(); + self.window.maybe_wait_on_main(|w| w.set_cursor_grab(mode)) + } + + /// Modifies the cursor's visibility. + /// + /// If `false`, this will hide the cursor. If `true`, this will show the cursor. + /// + /// ## Platform-specific + /// + /// - **Windows:** The cursor is only hidden within the confines of the window. + /// - **X11:** The cursor is only hidden within the confines of the window. + /// - **Wayland:** The cursor is only hidden within the confines of the window. + /// - **macOS:** The cursor is hidden as long as the window has input focus, even if the cursor + /// is outside of the window. + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + let _span = + tracing::debug_span!("rio_window::Window::set_cursor_visible", visible) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.set_cursor_visible(visible)) + } + + /// Moves the window with the left mouse button until the button is released. + /// + /// There's no guarantee that this will work unless the left mouse button was pressed + /// immediately before this function is called. + /// + /// ## Platform-specific + /// + /// - **X11:** Un-grabs the cursor. + /// - **Wayland:** Requires the cursor to be inside the window to be dragged. + /// - **macOS:** May prevent the button release event to be triggered. + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + let _span = tracing::debug_span!("rio_window::Window::drag_window",).entered(); + self.window.maybe_wait_on_main(|w| w.drag_window()) + } + + /// Resizes the window with the left mouse button until the button is released. + /// + /// There's no guarantee that this will work unless the left mouse button was pressed + /// immediately before this function is called. + /// + /// ## Platform-specific + /// + /// - **macOS:** Always returns an [`ExternalError::NotSupported`] + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn drag_resize_window( + &self, + direction: ResizeDirection, + ) -> Result<(), ExternalError> { + let _span = tracing::debug_span!( + "rio_window::Window::drag_resize_window", + direction = ?direction + ) + .entered(); + self.window + .maybe_wait_on_main(|w| w.drag_resize_window(direction)) + } + + /// Show [window menu] at a specified position . + /// + /// This is the context menu that is normally shown when interacting with + /// the title bar. This is useful when implementing custom decorations. + /// + /// ## Platform-specific + /// **Android / iOS / macOS / Orbital / Wayland / Web / X11:** Unsupported. + /// + /// [window menu]: https://en.wikipedia.org/wiki/Common_menus_in_Microsoft_Windows#System_menu + pub fn show_window_menu(&self, position: impl Into) { + let position = position.into(); + let _span = tracing::debug_span!( + "rio_window::Window::show_window_menu", + position = ?position + ) + .entered(); + self.window + .maybe_queue_on_main(move |w| w.show_window_menu(position)) + } + + /// Modifies whether the window catches cursor events. + /// + /// If `true`, the window will catch the cursor events. If `false`, events are passed through + /// the window such that any other window behind it receives them. By default hittest is + /// enabled. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Orbital:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + let _span = + tracing::debug_span!("rio_window::Window::set_cursor_hittest", hittest) + .entered(); + self.window + .maybe_wait_on_main(|w| w.set_cursor_hittest(hittest)) + } +} + +/// Monitor info functions. +impl Window { + /// Returns the monitor on which the window currently resides. + /// + /// Returns `None` if current monitor can't be detected. + #[inline] + pub fn current_monitor(&self) -> Option { + let _span = + tracing::debug_span!("rio_window::Window::current_monitor",).entered(); + self.window.maybe_wait_on_main(|w| { + w.current_monitor().map(|inner| MonitorHandle { inner }) + }) + } + + /// Returns the list of all the monitors available on the system. + /// + /// This is the same as [`ActiveEventLoop::available_monitors`], and is provided for + /// convenience. + /// + /// [`ActiveEventLoop::available_monitors`]: crate::event_loop::ActiveEventLoop::available_monitors + #[inline] + pub fn available_monitors(&self) -> impl Iterator { + let _span = + tracing::debug_span!("rio_window::Window::available_monitors",).entered(); + self.window.maybe_wait_on_main(|w| { + w.available_monitors() + .into_iter() + .map(|inner| MonitorHandle { inner }) + }) + } + + /// Returns the primary monitor of the system. + /// + /// Returns `None` if it can't identify any monitor as a primary one. + /// + /// This is the same as [`ActiveEventLoop::primary_monitor`], and is provided for convenience. + /// + /// ## Platform-specific + /// + /// **Wayland / Web:** Always returns `None`. + /// + /// [`ActiveEventLoop::primary_monitor`]: crate::event_loop::ActiveEventLoop::primary_monitor + #[inline] + pub fn primary_monitor(&self) -> Option { + let _span = + tracing::debug_span!("rio_window::Window::primary_monitor",).entered(); + self.window.maybe_wait_on_main(|w| { + w.primary_monitor().map(|inner| MonitorHandle { inner }) + }) + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasWindowHandle for Window { + fn window_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self.window.raw_window_handle_rwh_06()?; + + // SAFETY: The window handle will never be deallocated while the window is alive, + // and the main thread safety requirements are upheld internally by each platform. + Ok(unsafe { rwh_06::WindowHandle::borrow_raw(raw) }) + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for Window { + fn display_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self.window.raw_display_handle_rwh_06()?; + + // SAFETY: The window handle will never be deallocated while the window is alive, + // and the main thread safety requirements are upheld internally by each platform. + Ok(unsafe { rwh_06::DisplayHandle::borrow_raw(raw) }) + } +} + +/// The behavior of cursor grabbing. +/// +/// Use this enum with [`Window::set_cursor_grab`] to grab the cursor. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum CursorGrabMode { + /// No grabbing of the cursor is performed. + None, + + /// The cursor is confined to the window area. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **macOS:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. + Confined, + + /// The cursor is locked inside the window area to the certain position. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **X11 / Windows:** Not implemented. Always returns [`ExternalError::NotSupported`] for + /// now. + /// - **iOS / Android:** Always returns an [`ExternalError::NotSupported`]. + Locked, +} + +/// Defines the orientation that a window resize will be performed. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum ResizeDirection { + East, + North, + NorthEast, + NorthWest, + South, + SouthEast, + SouthWest, + West, +} + +impl From for CursorIcon { + fn from(direction: ResizeDirection) -> Self { + use ResizeDirection::*; + match direction { + East => CursorIcon::EResize, + North => CursorIcon::NResize, + NorthEast => CursorIcon::NeResize, + NorthWest => CursorIcon::NwResize, + South => CursorIcon::SResize, + SouthEast => CursorIcon::SeResize, + SouthWest => CursorIcon::SwResize, + West => CursorIcon::WResize, + } + } +} + +/// Fullscreen modes. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Fullscreen { + Exclusive(VideoModeHandle), + + /// Providing `None` to `Borderless` will fullscreen on the current monitor. + Borderless(Option), +} + +/// The theme variant to use. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Theme { + /// Use the light variant. + Light, + + /// Use the dark variant. + Dark, +} + +/// ## Platform-specific +/// +/// - **X11:** Sets the WM's `XUrgencyHint`. No distinction between [`Critical`] and +/// [`Informational`]. +/// +/// [`Critical`]: Self::Critical +/// [`Informational`]: Self::Informational +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum UserAttentionType { + /// ## Platform-specific + /// + /// - **macOS:** Bounces the dock icon until the application is in focus. + /// - **Windows:** Flashes both the window and the taskbar button until the application is in + /// focus. + Critical, + + /// ## Platform-specific + /// + /// - **macOS:** Bounces the dock icon once. + /// - **Windows:** Flashes the taskbar button until the application is in focus. + #[default] + Informational, +} + +bitflags::bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct WindowButtons: u32 { + const CLOSE = 1 << 0; + const MINIMIZE = 1 << 1; + const MAXIMIZE = 1 << 2; + } +} + +/// A window level groups windows with respect to their z-position. +/// +/// The relative ordering between windows in different window levels is fixed. +/// The z-order of a window within the same window level may change dynamically on user interaction. +/// +/// ## Platform-specific +/// +/// - **iOS / Android / Web / Wayland:** Unsupported. +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +pub enum WindowLevel { + /// The window will always be below normal windows. + /// + /// This is useful for a widget-based app. + AlwaysOnBottom, + + /// The default. + #[default] + Normal, + + /// The window will always be on top of normal windows. + AlwaysOnTop, +} + +/// Generic IME purposes for use in [`Window::set_ime_purpose`]. +/// +/// The purpose may improve UX by optimizing the IME for the specific use case, +/// if winit can express the purpose to the platform and the platform reacts accordingly. +/// +/// ## Platform-specific +/// +/// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[non_exhaustive] +pub enum ImePurpose { + /// No special hints for the IME (default). + Normal, + /// The IME is used for password input. + Password, + /// The IME is used to input into a terminal. + /// + /// For example, that could alter OSK on Wayland to show extra buttons. + Terminal, +} + +impl Default for ImePurpose { + fn default() -> Self { + Self::Normal + } +} + +/// An opaque token used to activate the [`Window`]. +/// +/// [`Window`]: crate::window::Window +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ActivationToken { + pub(crate) _token: String, +} + +impl ActivationToken { + pub(crate) fn _new(_token: String) -> Self { + Self { _token } + } +} diff --git a/rio-window/tests/send_objects.rs b/rio-window/tests/send_objects.rs new file mode 100644 index 00000000..1ed5411e --- /dev/null +++ b/rio-window/tests/send_objects.rs @@ -0,0 +1,36 @@ +#[allow(dead_code)] +fn needs_send() {} + +#[test] +fn event_loop_proxy_send() { + #[allow(dead_code)] + fn is_send() { + // ensures that `rio_window::EventLoopProxy` implements `Send` + needs_send::>(); + } +} + +#[test] +fn window_send() { + // ensures that `rio_window::Window` implements `Send` + needs_send::(); +} + +#[test] +fn window_builder_send() { + needs_send::(); +} + +#[test] +fn ids_send() { + // ensures that the various `..Id` types implement `Send` + needs_send::(); + needs_send::(); + needs_send::(); +} + +#[test] +fn custom_cursor_send() { + needs_send::(); + needs_send::(); +} diff --git a/rio-window/tests/sync_object.rs b/rio-window/tests/sync_object.rs new file mode 100644 index 00000000..55291736 --- /dev/null +++ b/rio-window/tests/sync_object.rs @@ -0,0 +1,28 @@ +#[allow(dead_code)] +fn needs_sync() {} + +#[test] +fn event_loop_proxy_send() { + #[allow(dead_code)] + fn is_send() { + // ensures that `rio_window::EventLoopProxy` implements `Sync` + needs_sync::>(); + } +} + +#[test] +fn window_sync() { + // ensures that `rio_window::Window` implements `Sync` + needs_sync::(); +} + +#[test] +fn window_builder_sync() { + needs_sync::(); +} + +#[test] +fn custom_cursor_sync() { + needs_sync::(); + needs_sync::(); +}