From 85b940e427f931930fb1deb20929ac225971a2f5 Mon Sep 17 00:00:00 2001 From: Shawn Wallace Date: Mon, 29 Apr 2024 00:27:22 -0400 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.lock | 747 ++++++++++++++++++++++++++ Cargo.toml | 10 + LICENSE | 373 +++++++++++++ README.md | 13 + satellite/Cargo.toml | 31 ++ satellite/src/clientside.rs | 200 +++++++ satellite/src/lib.rs | 197 +++++++ satellite/src/main.rs | 7 + satellite/src/server/dispatch.rs | 867 +++++++++++++++++++++++++++++++ satellite/src/server/event.rs | 605 +++++++++++++++++++++ satellite/src/server/mod.rs | 768 +++++++++++++++++++++++++++ satellite/src/server/tests.rs | 866 ++++++++++++++++++++++++++++++ satellite/src/xstate.rs | 535 +++++++++++++++++++ satellite/tests/integration.rs | 269 ++++++++++ testwl/Cargo.lock | 273 ++++++++++ testwl/Cargo.toml | 9 + testwl/src/lib.rs | 839 ++++++++++++++++++++++++++++++ wl_drm/Cargo.toml | 11 + wl_drm/src/drm.xml | 185 +++++++ wl_drm/src/lib.rs | 19 + 21 files changed, 6825 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 satellite/Cargo.toml create mode 100644 satellite/src/clientside.rs create mode 100644 satellite/src/lib.rs create mode 100644 satellite/src/main.rs create mode 100644 satellite/src/server/dispatch.rs create mode 100644 satellite/src/server/event.rs create mode 100644 satellite/src/server/mod.rs create mode 100644 satellite/src/server/tests.rs create mode 100644 satellite/src/xstate.rs create mode 100644 satellite/tests/integration.rs create mode 100644 testwl/Cargo.lock create mode 100644 testwl/Cargo.toml create mode 100644 testwl/src/lib.rs create mode 100644 wl_drm/Cargo.toml create mode 100644 wl_drm/src/drm.xml create mode 100644 wl_drm/src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ae8625b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,747 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "bindgen" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "cc" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[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 = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "io-lifetimes" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" + +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger 0.10.2", + "log", +] + +[[package]] +name = "proc-macro2" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "testwl" +version = "0.1.0" +dependencies = [ + "wayland-protocols", + "wayland-server", + "wl_drm", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wayland-backend" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" +dependencies = [ + "bitflags 2.5.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", + "wayland-server", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" +dependencies = [ + "proc-macro2", + "quick-xml 0.31.0", + "quote", +] + +[[package]] +name = "wayland-server" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e6e4d5c285bc24ba4ed2d5a4bd4febd5fd904451f465973225c8e99772fdb7" +dependencies = [ + "bitflags 2.5.0", + "downcast-rs", + "io-lifetimes", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-sys" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "wl_drm" +version = "0.1.0" +dependencies = [ + "wayland-client", + "wayland-scanner", + "wayland-server", +] + +[[package]] +name = "xcb" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e75181b5a62b6eeaa72f303d3cef7dbb841e22885bf6d3e66fe23e88c55dc6" +dependencies = [ + "bitflags 1.3.2", + "libc", + "quick-xml 0.30.0", +] + +[[package]] +name = "xcb-util-cursor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cca04fb82324e278cdcc920289ab454635d17f2d3036e2aec1c320b435b036" +dependencies = [ + "xcb", + "xcb-util-cursor-sys", +] + +[[package]] +name = "xcb-util-cursor-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13724e3af85816d1bbd617cc899b9ff7a55ca53413c4cc5c269e8c62bcc1702" +dependencies = [ + "bindgen", +] + +[[package]] +name = "xwayland-satellite" +version = "0.1.0" +dependencies = [ + "bitflags 2.5.0", + "env_logger 0.11.3", + "libc", + "log", + "paste", + "pretty_env_logger", + "rustix", + "signal-hook", + "slotmap", + "testwl", + "wayland-client", + "wayland-protocols", + "wayland-scanner", + "wayland-server", + "wl_drm", + "xcb", + "xcb-util-cursor", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..40a6ee2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +members = [ + "satellite", + "testwl" , + "wl_drm" +] + +resolver = "2" + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md new file mode 100644 index 0000000..08e8727 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# xwayland-satellite +xwayland-satellite grants rootless Xwayland integration to any Wayland compositor implementing xdg_wm_base. +This is particularly useful for compositors that (understandably) do not want to go through implementing support for rootless Xwayland themselves. + +## Usage +Run `xwayland-satellite`. You can specify an X display to use (i.e. `:12`). Be sure to set the same `DISPLAY` environment variable for any X11 clients. +Because xwayland-satellite is a Wayland client (in addition to being a Wayland compositor), it will need to launch after your compositor launches, but obviously before any X11 applications. + +## Building +``` +cargo build +cargo run +``` diff --git a/satellite/Cargo.toml b/satellite/Cargo.toml new file mode 100644 index 0000000..e677b2b --- /dev/null +++ b/satellite/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "xwayland-satellite" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["lib"] + +[dependencies] +bitflags = "2.5.0" +paste = "1.0.14" +rustix = { version = "0.38.31", features = ["event"] } +wayland-client = "0.31.2" +wayland-protocols = { version = "0.31.2", features = ["client", "server", "staging"] } +wayland-scanner = "0.31.1" +wayland-server = "0.31.1" +xcb = { version = "1.3.0", features = ["composite"] } +wl_drm = { path = "../wl_drm" } +signal-hook = "0.3.17" +libc = "0.2.153" +log = "0.4.21" +env_logger = "0.11.3" +pretty_env_logger = "0.5.0" +slotmap = "1.0.7" +xcb-util-cursor = "0.3.2" + +[dev-dependencies] +rustix = { version = "0.38.31", features = ["fs"] } +testwl = { path = "../testwl" } diff --git a/satellite/src/clientside.rs b/satellite/src/clientside.rs new file mode 100644 index 0000000..23d6d9c --- /dev/null +++ b/satellite/src/clientside.rs @@ -0,0 +1,200 @@ +use crate::server::{ObjectEvent, ObjectKey}; +use std::os::fd::OwnedFd; +use std::os::unix::net::UnixStream; +use std::sync::mpsc; +use wayland_client::protocol::{ + wl_buffer::WlBuffer, wl_callback::WlCallback, wl_compositor::WlCompositor, + wl_keyboard::WlKeyboard, wl_output::WlOutput, wl_pointer::WlPointer, wl_registry::WlRegistry, + wl_seat::WlSeat, wl_shm::WlShm, wl_shm_pool::WlShmPool, wl_surface::WlSurface, +}; +use wayland_client::{delegate_noop, Connection, Dispatch, EventQueue, Proxy, QueueHandle}; +use wayland_protocols::wp::relative_pointer::zv1::client::{ + zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, + zwp_relative_pointer_v1::ZwpRelativePointerV1, +}; +use wayland_protocols::{ + wp::{ + linux_dmabuf::zv1::client::{ + self as dmabuf, + zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1 as DmabufFeedback, + zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, + }, + viewporter::client::{wp_viewport::WpViewport, wp_viewporter::WpViewporter}, + }, + xdg::{ + shell::client::{ + xdg_popup::XdgPopup, xdg_positioner::XdgPositioner, xdg_surface::XdgSurface, + xdg_toplevel::XdgToplevel, xdg_wm_base::XdgWmBase, + }, + xdg_output::zv1::client::{ + zxdg_output_manager_v1::ZxdgOutputManagerV1, zxdg_output_v1::ZxdgOutputV1 as XdgOutput, + }, + }, +}; +use wayland_server::protocol as server; +use wl_drm::client::wl_drm::WlDrm; + +#[derive(Debug)] +pub struct GlobalData { + pub name: u32, + pub interface: String, + pub version: u32, +} + +#[derive(Default)] +pub struct Globals { + pub(crate) events: Vec<(ObjectKey, ObjectEvent)>, + pub new_globals: Vec, +} + +pub type ClientQueueHandle = QueueHandle; + +pub struct ClientShmPool { + pub pool: WlShmPool, + pub fd: OwnedFd, +} + +pub struct ClientState { + pub connection: Connection, + pub queue: EventQueue, + pub qh: ClientQueueHandle, + pub globals: Globals, + pub registry: WlRegistry, +} + +impl ClientState { + pub fn new(server_connection: Option) -> Self { + let connection = if let Some(stream) = server_connection { + Connection::from_socket(stream) + } else { + Connection::connect_to_env() + } + .unwrap(); + let mut queue = connection.new_event_queue::(); + let qh = queue.handle(); + let mut globals = Globals::default(); + + let registry = connection.display().get_registry(&qh, ()); + // Get initial globals + queue.roundtrip(&mut globals).unwrap(); + + Self { + connection, + queue, + qh, + globals, + registry, + } + } +} + +pub type Event = ::Event; + +delegate_noop!(Globals: WlCompositor); +delegate_noop!(Globals: ignore WlShm); +delegate_noop!(Globals: ignore ZwpLinuxDmabufV1); +delegate_noop!(Globals: ZwpRelativePointerManagerV1); +delegate_noop!(Globals: ignore dmabuf::zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1); +delegate_noop!(Globals: XdgPositioner); +delegate_noop!(Globals: WlShmPool); +delegate_noop!(Globals: WpViewporter); +delegate_noop!(Globals: WpViewport); +delegate_noop!(Globals: ZxdgOutputManagerV1); + +impl Dispatch for Globals { + fn event( + state: &mut Self, + _: &WlRegistry, + event: ::Event, + _: &(), + _: &wayland_client::Connection, + _: &wayland_client::QueueHandle, + ) { + if let Event::::Global { + name, + interface, + version, + } = event + { + state.new_globals.push(GlobalData { + name, + interface, + version, + }); + }; + } +} + +impl Dispatch for Globals { + fn event( + _: &mut Self, + base: &XdgWmBase, + event: ::Event, + _: &(), + _: &wayland_client::Connection, + _: &wayland_client::QueueHandle, + ) { + if let Event::::Ping { serial } = event { + base.pong(serial); + } + } +} + +impl Dispatch for Globals { + fn event( + _: &mut Self, + _: &WlCallback, + event: ::Event, + s_callback: &server::wl_callback::WlCallback, + _: &Connection, + _: &QueueHandle, + ) { + if let Event::::Done { callback_data } = event { + s_callback.done(callback_data); + } + } +} + +impl Dispatch>> for Globals { + fn event( + _: &mut Self, + _: &WlOutput, + event: ::Event, + data: &mpsc::Sender>, + _: &Connection, + _: &QueueHandle, + ) { + let _ = data.send(event); + } +} + +macro_rules! push_events { + ($type:ident) => { + impl Dispatch<$type, ObjectKey> for Globals { + fn event( + state: &mut Self, + _: &$type, + event: <$type as Proxy>::Event, + key: &ObjectKey, + _: &Connection, + _: &QueueHandle, + ) { + state.events.push((*key, event.into())); + } + } + }; +} + +push_events!(WlSurface); +push_events!(WlBuffer); +push_events!(XdgSurface); +push_events!(XdgToplevel); +push_events!(XdgPopup); +push_events!(WlSeat); +push_events!(WlPointer); +push_events!(WlOutput); +push_events!(WlKeyboard); +push_events!(ZwpRelativePointerV1); +push_events!(WlDrm); +push_events!(DmabufFeedback); +push_events!(XdgOutput); diff --git a/satellite/src/lib.rs b/satellite/src/lib.rs new file mode 100644 index 0000000..13a8b93 --- /dev/null +++ b/satellite/src/lib.rs @@ -0,0 +1,197 @@ +mod clientside; +mod server; +mod xstate; + +use crate::server::{PendingSurfaceState, ServerState}; +use crate::xstate::XState; +use log::{error, info}; +use rustix::event::{poll, PollFd, PollFlags}; +use signal_hook::consts::*; +use std::io::{BufRead, BufReader, Read, Write}; +use std::mem::MaybeUninit; +use std::os::fd::{AsFd, AsRawFd, BorrowedFd}; +use std::os::unix::{net::UnixStream, process::CommandExt}; +use std::process::{Command, Stdio}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::Sender, + Arc, +}; +use wayland_server::{Display, ListeningSocket}; +use xcb::x; + +pub trait XConnection: Sized + 'static { + type ExtraData: FromServerState; + + fn root_window(&self) -> x::Window; + fn set_window_dims(&mut self, window: x::Window, dims: PendingSurfaceState); + fn set_fullscreen(&mut self, window: x::Window, fullscreen: bool, data: Self::ExtraData); + fn focus_window(&mut self, window: x::Window, data: Self::ExtraData); + fn close_window(&mut self, window: x::Window, data: Self::ExtraData); +} + +pub trait FromServerState { + fn create(state: &ServerState) -> Self; +} + +type RealServerState = ServerState>; + +#[derive(Debug, PartialEq, Eq)] +pub enum StateEvent { + CreatedServer, + ConnectedServer, + XwaylandReady, +} + +pub fn main(comp: Option, state_updater: Option>) -> Option<()> { + let display_arg = get_display()?; + + let socket = ListeningSocket::bind_auto("wayland", 1..=128).unwrap(); + let mut display = Display::::new().unwrap(); + let dh = display.handle(); + + let mut server_state = RealServerState::new(dh, comp); + if let Some(ref s) = state_updater { + s.send(StateEvent::CreatedServer).unwrap(); + } + + let (xsock_wl, xsock_xwl) = UnixStream::pair().unwrap(); + // Prevent creation of new Xwayland command from closing fd + rustix::io::fcntl_setfd(&xsock_xwl, rustix::io::FdFlags::empty()).unwrap(); + + // Flag when Xwayland is ready to accept our connection + let ready = Arc::new(AtomicBool::new(false)); + signal_hook::flag::register(SIGUSR1, ready.clone()).unwrap(); + + let mut xwayland = Command::new("Xwayland"); + let mut xwayland = unsafe { + xwayland.pre_exec(|| { + // Set SIGUSR1 to SIG_IGN for Xwayland to get SIGUSR1 to our main process, + // which signifies that the X server is ready to accept connections + let mut sa_mask = MaybeUninit::uninit(); + libc::sigemptyset(sa_mask.as_mut_ptr()); + let sa_mask = sa_mask.assume_init(); + let act = libc::sigaction { + sa_sigaction: libc::SIG_IGN, + sa_mask, + sa_flags: 0, + sa_restorer: None, + }; + libc::sigaction(SIGUSR1, &act, std::ptr::null_mut()); + Ok(()) + }) + } + .env("WAYLAND_DISPLAY", socket.socket_name().unwrap()) + //.env("WAYLAND_DEBUG", "1") + .args([ + &display_arg, + "-rootless", + "-wm", + &format!("{}", &xsock_xwl.as_raw_fd()), + ]) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + + let (mut finish_tx, mut finish_rx) = UnixStream::pair().unwrap(); + let stderr = xwayland.stderr.take().unwrap(); + std::thread::spawn(move || { + let reader = BufReader::new(stderr); + for line in reader.lines() { + let line = line.unwrap(); + info!(target: "xwayland_process", "{line}"); + } + let status = Box::new(xwayland.wait().unwrap()); + let status = Box::into_raw(status) as usize; + finish_tx.write_all(&status.to_ne_bytes()).unwrap(); + }); + + let mut ready_fds = [ + PollFd::new(&socket, PollFlags::IN), + PollFd::new(&finish_rx, PollFlags::IN), + ]; + + let connection = match poll(&mut ready_fds, -1) { + Ok(_) => { + if !ready_fds[1].revents().is_empty() { + let mut data = [0; (usize::BITS / 8) as usize]; + finish_rx.read_exact(&mut data).unwrap(); + let data = usize::from_ne_bytes(data); + let status: Box = + unsafe { Box::from_raw(data as *mut _) }; + + error!("Xwayland exited early with {status}"); + return None; + } + + if let Some(ref s) = state_updater { + s.send(StateEvent::ConnectedServer).unwrap(); + } + socket.accept().unwrap().unwrap() + } + Err(e) => { + panic!("first poll failed: {e:?}") + } + }; + drop(finish_rx); + + server_state.connect(connection); + server_state.run(); + + let mut xstate: Option = None; + + // Remove the lifetimes on our fds to avoid borrowing issues, since we know they will exist for + // the rest of our program anyway + let server_fd = unsafe { BorrowedFd::borrow_raw(server_state.clientside_fd().as_raw_fd()) }; + let display_fd = unsafe { BorrowedFd::borrow_raw(display.backend().poll_fd().as_raw_fd()) }; + + let mut fds = [ + PollFd::from_borrowed_fd(server_fd, PollFlags::IN), + PollFd::new(&xsock_wl, PollFlags::IN), + PollFd::from_borrowed_fd(display_fd, PollFlags::IN), + ]; + + loop { + match poll(&mut fds, -1) { + Ok(_) => {} + Err(rustix::io::Errno::INTR) => { + // Typically caused by SIGUSR1 + if !ready.load(Ordering::Relaxed) { + continue; + } + } + Err(other) => panic!("Poll failed: {other:?}"), + } + + if xstate.is_none() && ready.load(Ordering::Relaxed) { + let xstate = xstate.insert(XState::new(xsock_wl.as_fd())); + info!("Connected to Xwayland on {display_arg}"); + if let Some(ref s) = state_updater { + s.send(StateEvent::XwaylandReady).unwrap(); + } + server_state.set_x_connection(xstate.connection.clone()); + server_state.atoms = Some(xstate.atoms.clone()); + } + + if let Some(state) = &mut xstate { + state.handle_events(&mut server_state); + } + + display.dispatch_clients(&mut server_state).unwrap(); + server_state.run(); + display.flush_clients().unwrap(); + } +} + +fn get_display() -> Option { + let mut args: Vec<_> = std::env::args().collect(); + if args.len() > 2 { + error!("Unexpected arguments: {:?}", &args[2..]); + return None; + } + if args.len() == 1 { + Some(":0".into()) + } else { + Some(args.swap_remove(1)) + } +} diff --git a/satellite/src/main.rs b/satellite/src/main.rs new file mode 100644 index 0000000..eefcd52 --- /dev/null +++ b/satellite/src/main.rs @@ -0,0 +1,7 @@ +fn main() { + pretty_env_logger::formatted_timed_builder() + .filter_level(log::LevelFilter::Info) + .parse_default_env() + .init(); + xwayland_satellite::main(None, None); +} diff --git a/satellite/src/server/dispatch.rs b/satellite/src/server/dispatch.rs new file mode 100644 index 0000000..326197e --- /dev/null +++ b/satellite/src/server/dispatch.rs @@ -0,0 +1,867 @@ +use super::*; +use log::{debug, trace, warn}; +use std::sync::{Arc, OnceLock}; +use wayland_protocols::{ + wp::{ + linux_dmabuf::zv1::{client as c_dmabuf, server as s_dmabuf}, + relative_pointer::zv1::{ + client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1 as RelativePointerManClient, + server::{ + zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1 as RelativePointerManServer, + zwp_relative_pointer_v1::ZwpRelativePointerV1 as RelativePointerServer, + }, + }, + viewporter::{client as c_vp, server as s_vp}, + }, + xdg::xdg_output::zv1::{ + client::zxdg_output_manager_v1::ZxdgOutputManagerV1 as OutputManClient, + server::{ + zxdg_output_manager_v1::{ + self as s_output_man, ZxdgOutputManagerV1 as OutputManServer, + }, + zxdg_output_v1::{self as s_xdgo, ZxdgOutputV1 as XdgOutputServer}, + }, + }, +}; +use wayland_server::{ + protocol::{ + wl_buffer::WlBuffer, wl_callback::WlCallback, wl_compositor::WlCompositor, + wl_keyboard::WlKeyboard, wl_output::WlOutput, wl_pointer::WlPointer, wl_seat::WlSeat, + wl_shm::WlShm, wl_shm_pool::WlShmPool, wl_surface::WlSurface, + }, + Dispatch, DisplayHandle, GlobalDispatch, Resource, +}; + +// noop +impl Dispatch for ServerState { + fn request( + _: &mut Self, + _: &wayland_server::Client, + _: &WlCallback, + _: ::Request, + _: &(), + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + unreachable!(); + } +} + +impl Dispatch for ServerState { + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &WlSurface, + request: ::Request, + key: &ObjectKey, + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + let surface: &SurfaceData = state.objects[*key].as_ref(); + let configured = + surface.role.is_none() || surface.xdg().is_none() || surface.xdg().unwrap().configured; + + match request { + Request::::Attach { buffer, x, y } => { + if buffer.is_none() { + trace!("xwayland attached null buffer to {:?}", surface.client); + } + let buffer = buffer.as_ref().map(|b| { + let key: &ObjectKey = b.data().unwrap(); + let data: &Buffer = state.objects[*key].as_ref(); + &data.client + }); + + if configured { + surface.client.attach(buffer, x, y); + } else { + let buffer = buffer.cloned(); + let surface: &mut SurfaceData = state.objects[*key].as_mut(); + surface.attach = Some(SurfaceAttach { buffer, x, y }); + } + } + Request::::DamageBuffer { + x, + y, + width, + height, + } => { + if configured { + surface.client.damage_buffer(x, y, width, height); + } + } + Request::::Frame { callback } => { + let cb = data_init.init(callback, ()); + if configured { + surface.client.frame(&state.qh, cb); + } else { + let surface: &mut SurfaceData = state.objects[*key].as_mut(); + surface.frame_callback = Some(cb); + } + } + Request::::Commit => { + if configured { + surface.client.commit(); + } + } + Request::::Destroy => { + let mut object = state.objects.remove(*key).unwrap(); + let surface: &mut SurfaceData = object.as_mut(); + surface.destroy_role(); + surface.client.destroy(); + debug!("deleting key: {:?}", key); + } + Request::::SetBufferScale { scale } => { + surface.client.set_buffer_scale(scale); + } + other => warn!("unhandled surface request: {other:?}"), + } + } +} + +impl + Dispatch> + for ServerState +{ + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &WlCompositor, + request: ::Request, + client: &ClientGlobalWrapper, + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + Request::::CreateSurface { id } => { + let mut surface_id = None; + + let key = state.objects.insert_with_key(|key| { + debug!("new surface with key {key:?}"); + let client = client.create_surface(&state.qh, key); + let server = data_init.init(id, key); + surface_id = Some(server.id().protocol_id()); + + SurfaceData { + client, + server, + key, + attach: None, + frame_callback: None, + role: None, + } + .into() + }); + + let surface_id = surface_id.unwrap(); + + if let Some((win, window_data)) = + state.windows.iter_mut().find_map(|(win, data)| { + Some(*win).zip((data.surface_id == surface_id).then_some(data)) + }) + { + window_data.surface_key = Some(key); + state.associated_windows.insert(key, win); + debug!("associate surface {surface_id} with window {win:?}"); + if window_data.mapped { + state.create_role_window(win, key); + } + } + } + other => { + warn!("unhandled wlcompositor request: {other:?}"); + } + } + } +} + +impl Dispatch for ServerState { + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &WlBuffer, + request: ::Request, + key: &ObjectKey, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + assert!(matches!(request, Request::::Destroy)); + + let buf: &Buffer = state.objects[*key].as_ref(); + buf.client.destroy(); + state.objects.remove(*key); + } +} + +impl Dispatch for ServerState { + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &WlShmPool, + request: ::Request, + c_pool: &ClientShmPool, + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + Request::::CreateBuffer { + id, + offset, + width, + height, + stride, + format, + } => { + state.objects.insert_with_key(|key| { + let client = c_pool.pool.create_buffer( + offset, + width, + height, + stride, + convert_wenum(format), + &state.qh, + key, + ); + let server = data_init.init(id, key); + Buffer { server, client }.into() + }); + } + Request::::Destroy => { + c_pool.pool.destroy(); + } + other => warn!("unhandled shmpool request: {other:?}"), + } + } +} + +impl Dispatch> + for ServerState +{ + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &WlShm, + request: ::Request, + client: &ClientGlobalWrapper, + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + Request::::CreatePool { id, fd, size } => { + let c_pool = client.create_pool(fd.as_fd(), size, &state.qh, ()); + let c_pool = ClientShmPool { pool: c_pool, fd }; + data_init.init(id, c_pool); + } + other => { + warn!("unhandled shm pool request: {other:?}"); + } + } + } +} + +impl Dispatch for ServerState { + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &WlPointer, + request: ::Request, + key: &ObjectKey, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + let Pointer { + client: c_pointer, .. + }: &Pointer = state.objects[*key].as_ref(); + + match request { + Request::::SetCursor { + serial, + hotspot_x, + hotspot_y, + surface, + } => { + let c_surface = surface.map(|s| state.get_client_surface_from_server(s)); + c_pointer.set_cursor(serial, c_surface, hotspot_x, hotspot_y); + } + Request::::Release => { + c_pointer.release(); + state.objects.remove(*key); + } + _ => warn!("unhandled cursor request: {request:?}"), + } + } +} + +impl Dispatch for ServerState { + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &WlKeyboard, + request: ::Request, + key: &ObjectKey, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + Request::::Release => { + let Keyboard { client, .. }: &_ = state.objects[*key].as_ref(); + client.release(); + state.objects.remove(*key); + } + _ => unreachable!(), + } + } +} + +impl Dispatch for ServerState { + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &WlSeat, + request: ::Request, + key: &ObjectKey, + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + Request::::GetPointer { id } => { + state + .objects + .insert_from_other_objects([*key], |[seat_obj], key| { + let Seat { client, .. }: &Seat = seat_obj.try_into().unwrap(); + let client = client.get_pointer(&state.qh, key); + let server = data_init.init(id, key); + trace!("new pointer: {server:?}"); + Pointer::new(server, client).into() + }); + } + Request::::GetKeyboard { id } => { + state + .objects + .insert_from_other_objects([*key], |[seat_obj], key| { + let Seat { client, .. }: &Seat = seat_obj.try_into().unwrap(); + let client = client.get_keyboard(&state.qh, key); + let server = data_init.init(id, key); + Keyboard { client, server }.into() + }); + } + other => warn!("unhandled seat request: {other:?}"), + } + } +} + +impl Dispatch for ServerState { + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &RelativePointerServer, + request: ::Request, + key: &ObjectKey, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + if let Request::::Destroy = request { + let obj: &RelativePointer = state.objects[*key].as_ref(); + obj.client.destroy(); + state.objects.remove(*key); + } + } +} + +impl + Dispatch> + for ServerState +{ + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &RelativePointerManServer, + request: ::Request, + client: &ClientGlobalWrapper, + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + Request::::GetRelativePointer { id, pointer } => { + let p_key: ObjectKey = pointer.data().copied().unwrap(); + state + .objects + .insert_from_other_objects([p_key], |[pointer_obj], key| { + let pointer: &Pointer = pointer_obj.try_into().unwrap(); + let client = client.get_relative_pointer(&pointer.client, &state.qh, key); + let server = data_init.init(id, key); + RelativePointer { client, server }.into() + }); + } + _ => warn!("unhandled relative pointer request: {request:?}"), + } + } +} + +impl Dispatch for ServerState { + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &WlOutput, + request: ::Request, + key: &ObjectKey, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + wayland_server::protocol::wl_output::Request::Release => { + let Output { client, .. }: &_ = state.objects[*key].as_ref(); + client.release(); + todo!("handle wloutput destruction"); + } + _ => warn!("unhandled output request {request:?}"), + } + } +} + +impl + Dispatch + for ServerState +{ + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &s_dmabuf::zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1, + request: ::Request, + key: &ObjectKey, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + use s_dmabuf::zwp_linux_dmabuf_feedback_v1::Request::*; + match request { + Destroy => { + let dmabuf: &DmabufFeedback = state.objects[*key].as_ref(); + dmabuf.client.destroy(); + state.objects.remove(*key); + } + _ => unreachable!(), + } + } +} + +impl + Dispatch< + s_dmabuf::zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1, + c_dmabuf::zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1, + > for ServerState +{ + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &s_dmabuf::zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1, + request: ::Request, + c_params: &c_dmabuf::zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1, + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + use s_dmabuf::zwp_linux_buffer_params_v1::Request::*; + match request { + // TODO: Xwayland doesn't actually seem to use the Create request, and I don't feel like implementing it... + Create { .. } => todo!(), + CreateImmed { + buffer_id, + width, + height, + format, + flags, + } => { + state.objects.insert_with_key(|key| { + let client = c_params.create_immed( + width, + height, + format, + convert_wenum(flags), + &state.qh, + key, + ); + let server = data_init.init(buffer_id, key); + Buffer { server, client }.into() + }); + } + Add { + fd, + plane_idx, + offset, + stride, + modifier_hi, + modifier_lo, + } => { + c_params.add( + fd.as_fd(), + plane_idx, + offset, + stride, + modifier_hi, + modifier_lo, + ); + } + Destroy => { + c_params.destroy(); + } + _ => warn!("unhandled params request: {request:?}"), + } + } +} + +impl + Dispatch< + s_dmabuf::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, + ClientGlobalWrapper, + > for ServerState +{ + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &s_dmabuf::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, + request: ::Request, + client: &ClientGlobalWrapper, + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + use s_dmabuf::zwp_linux_dmabuf_v1::Request::*; + match request { + Destroy => { + client.destroy(); + } + CreateParams { params_id } => { + let c_params = client.create_params(&state.qh, ()); + data_init.init(params_id, c_params); + } + GetDefaultFeedback { id } => { + state.objects.insert_with_key(|key| { + let client = client.get_default_feedback(&state.qh, key); + let server = data_init.init(id, key); + DmabufFeedback { client, server }.into() + }); + } + GetSurfaceFeedback { id, surface } => { + let surf_key: ObjectKey = surface.data().copied().unwrap(); + state + .objects + .insert_from_other_objects([surf_key], |[surface_obj], key| { + let SurfaceData { + client: c_surface, .. + }: &SurfaceData = surface_obj.try_into().unwrap(); + let client = client.get_surface_feedback(c_surface, &state.qh, key); + let server = data_init.init(id, key); + DmabufFeedback { client, server }.into() + }); + } + _ => warn!("unhandled dmabuf request: {request:?}"), + } + } +} + +impl Dispatch for ServerState { + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &WlDrmServer, + request: ::Request, + key: &ObjectKey, + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + use wl_drm::server::wl_drm::Request::*; + + type DrmFn = dyn FnOnce( + &wl_drm::client::wl_drm::WlDrm, + ObjectKey, + &ClientQueueHandle, + ) -> client::wl_buffer::WlBuffer; + + let mut bufs: Option<(Box, wayland_server::New)> = None; + match request { + CreateBuffer { + id, + name, + width, + height, + stride, + format, + } => { + bufs = Some(( + Box::new(move |drm, key, qh| { + drm.create_buffer(name, width, height, stride, format, qh, key) + }), + id, + )); + } + CreatePlanarBuffer { + id, + name, + width, + height, + format, + offset0, + stride0, + offset1, + stride1, + offset2, + stride2, + } => { + bufs = Some(( + Box::new(move |drm, key, qh| { + drm.create_planar_buffer( + name, width, height, format, offset0, stride0, offset1, stride1, + offset2, stride2, qh, key, + ) + }), + id, + )); + } + CreatePrimeBuffer { + id, + name, + width, + height, + format, + offset0, + stride0, + offset1, + stride1, + offset2, + stride2, + } => { + bufs = Some(( + Box::new(move |drm, key, qh| { + drm.create_prime_buffer( + name.as_fd(), + width, + height, + format, + offset0, + stride0, + offset1, + stride1, + offset2, + stride2, + qh, + key, + ) + }), + id, + )); + } + Authenticate { id } => { + let drm: &Drm = state.objects[*key].as_ref(); + drm.client.authenticate(id); + } + _ => unreachable!(), + } + + if let Some((buf_create, id)) = bufs { + state + .objects + .insert_from_other_objects([*key], |[drm_obj], key| { + let drm: &Drm = drm_obj.try_into().unwrap(); + let client = buf_create(&drm.client, key, &state.qh); + let server = data_init.init(id, key); + Buffer { client, server }.into() + }); + } + } +} + +impl Dispatch + for ServerState +{ + fn request( + _: &mut Self, + _: &wayland_server::Client, + _: &s_vp::wp_viewport::WpViewport, + request: ::Request, + c_viewport: &c_vp::wp_viewport::WpViewport, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + simple_event_shunt! { + c_viewport, request: s_vp::wp_viewport::Request => [ + SetSource { x, y, width, height }, + SetDestination { width, height }, + Destroy + ] + } + } +} + +impl + Dispatch< + s_vp::wp_viewporter::WpViewporter, + ClientGlobalWrapper, + > for ServerState +{ + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &s_vp::wp_viewporter::WpViewporter, + request: ::Request, + client: &ClientGlobalWrapper, + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + use s_vp::wp_viewporter; + match request { + wp_viewporter::Request::GetViewport { id, surface } => { + let c_surface = state.get_client_surface_from_server(surface); + let c_viewport = client.get_viewport(c_surface, &state.qh, ()); + data_init.init(id, c_viewport); + } + wp_viewporter::Request::Destroy => { + client.destroy(); + } + _ => unreachable!(), + } + } +} + +impl Dispatch for ServerState { + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &XdgOutputServer, + request: ::Request, + key: &ObjectKey, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + let s_xdgo::Request::Destroy = request else { + unreachable!(); + }; + + let output: &XdgOutput = state.objects[*key].as_ref(); + output.client.destroy(); + state.objects.remove(*key); + } +} + +impl Dispatch> + for ServerState +{ + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &OutputManServer, + request: ::Request, + client: &ClientGlobalWrapper, + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + s_output_man::Request::GetXdgOutput { id, output } => { + let output_key: ObjectKey = output.data().copied().unwrap(); + state + .objects + .insert_from_other_objects([output_key], |[output_obj], key| { + let output: &Output = output_obj.try_into().unwrap(); + let client = client.get_xdg_output(&output.client, &state.qh, key); + let server = data_init.init(id, key); + XdgOutput { server, client }.into() + }); + } + s_output_man::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +#[derive(Clone)] +pub(crate) struct ClientGlobalWrapper(Arc>); +impl std::ops::Deref for ClientGlobalWrapper { + type Target = T; + fn deref(&self) -> &Self::Target { + self.0.get().unwrap() + } +} + +impl Default for ClientGlobalWrapper { + fn default() -> Self { + Self(Arc::default()) + } +} + +macro_rules! global_dispatch_no_events { + ($server:ty, $client:ty) => { + impl GlobalDispatch<$server, GlobalData> for ServerState + where + ServerState: Dispatch<$server, ClientGlobalWrapper<$client>>, + Globals: wayland_client::Dispatch<$client, ()>, + { + fn bind( + state: &mut Self, + _: &DisplayHandle, + _: &wayland_server::Client, + resource: wayland_server::New<$server>, + data: &GlobalData, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + let client = ClientGlobalWrapper::<$client>::default(); + let server = data_init.init(resource, client.clone()); + client + .0 + .set( + state + .clientside + .registry + .bind(data.name, server.version(), &state.qh, ()), + ) + .unwrap(); + } + } + }; +} + +macro_rules! global_dispatch_with_events { + ($server:ty, $client:ty) => { + impl GlobalDispatch<$server, GlobalData> for ServerState + where + $server: Resource, + $client: Proxy, + ServerState: Dispatch<$server, ObjectKey>, + Globals: wayland_client::Dispatch<$client, ObjectKey>, + GenericObject<$server, $client>: Into, + { + fn bind( + state: &mut Self, + _: &DisplayHandle, + _: &wayland_server::Client, + resource: wayland_server::New<$server>, + data: &GlobalData, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + state.objects.insert_with_key(|key| { + let server = data_init.init(resource, key); + let client = state.clientside.registry.bind::<$client, _, _>( + data.name, + server.version(), + &state.qh, + key, + ); + GenericObject { server, client }.into() + }); + } + } + }; +} + +global_dispatch_no_events!(WlShm, client::wl_shm::WlShm); +global_dispatch_no_events!(WlCompositor, client::wl_compositor::WlCompositor); +global_dispatch_no_events!(RelativePointerManServer, RelativePointerManClient); +global_dispatch_no_events!( + s_dmabuf::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, + c_dmabuf::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1 +); +global_dispatch_no_events!(OutputManServer, OutputManClient); +global_dispatch_no_events!( + s_vp::wp_viewporter::WpViewporter, + c_vp::wp_viewporter::WpViewporter +); + +global_dispatch_with_events!(WlSeat, client::wl_seat::WlSeat); +global_dispatch_with_events!(WlOutput, client::wl_output::WlOutput); +global_dispatch_with_events!(WlDrmServer, WlDrmClient); diff --git a/satellite/src/server/event.rs b/satellite/src/server/event.rs new file mode 100644 index 0000000..96ef300 --- /dev/null +++ b/satellite/src/server/event.rs @@ -0,0 +1,605 @@ +use super::*; +use log::{debug, trace, warn}; +use std::os::fd::AsFd; +use wayland_client::{protocol as client, Proxy}; +use wayland_protocols::{ + wp::relative_pointer::zv1::{ + client::zwp_relative_pointer_v1::{self, ZwpRelativePointerV1 as RelativePointerClient}, + server::zwp_relative_pointer_v1::ZwpRelativePointerV1 as RelativePointerServer, + }, + xdg::{ + shell::client::{xdg_popup, xdg_surface, xdg_toplevel}, + xdg_output::zv1::{ + client::zxdg_output_v1::{self, ZxdgOutputV1 as ClientXdgOutput}, + server::zxdg_output_v1::ZxdgOutputV1 as ServerXdgOutput, + }, + }, +}; +use wayland_server::protocol::{ + wl_buffer::WlBuffer, wl_keyboard::WlKeyboard, wl_output::WlOutput, wl_pointer::WlPointer, + wl_seat::WlSeat, +}; + +/// Lord forgive me, I am a sinner, who's probably gonna sin again +/// This macro takes an enum variant name and a list of the field names of the enum +/// and/or closures that take an argument that must be named the same as the field name, +/// and converts that into a destructured enum +/// shunt_helper_enum!(Foo [a, |b| b.do_thing(), c]) -> Foo {a, b, c} +macro_rules! shunt_helper_enum { + // No fields + ($variant:ident) => { $variant }; + // Starting state: variant destructure + ($variant:ident $([$($body:tt)+])?) => { + shunt_helper_enum!($variant [$($($body)+)?] -> []) + }; + // Add field to list + ($variant:ident [$field:ident $(, $($rest:tt)+)?] -> [$($body:tt)*]) => { + shunt_helper_enum!($variant [$($($rest)+)?] -> [$($body)*, $field]) + }; + // Add closure field to list + ($variant:ident [|$field:ident| $conv:expr $(, $($rest:tt)+)?] -> [$($body:tt)*]) => { + shunt_helper_enum!($variant [$($($rest)+)?] -> [$($body)*, $field]) + }; + // Finalize into enum variant + ($variant:ident [] -> [,$($body:tt)+]) => { $variant { $($body)+ } }; +} + +/// This does the same thing as shunt_helper_enum, except it transforms the fields into the given +/// function/method call. +/// shunt_helper_fn!({obj.foo} [a, |b| b.do_thing(), c]) -> obj.foo(a, b.do_thing(), c) +macro_rules! shunt_helper_fn { + // No fields + ({$($fn:tt)+}) => { $($fn)+() }; + // Starting state + ($fn:tt [$($body:tt)+]) => { + shunt_helper_fn!($fn [$($body)+] -> []) + }; + // Add field to list + ($fn:tt [$field:ident $(, $($rest:tt)+)?] -> [$($body:tt)*]) => { + shunt_helper_fn!($fn [$($($rest)+)?] -> [$($body)*, $field]) + }; + // Add closure expression to list + ($fn:tt [|$field:ident| $conv:expr $(, $($rest:tt)+)?] -> [$($body:tt)*]) => { + shunt_helper_fn!($fn [$($($rest)+)?] -> [$($body)*, $conv]) + }; + // Finalize into function call + ({$($fn:tt)+} [] -> [,$($body:tt)+]) => { $($fn)+($($body)+) }; +} + +/// Takes an object, the name of a variable holding an event, the event type, and a list of the +/// variants with their fields, and converts them into function calls on their arguments +/// Event { field1, field2 } => obj.event(field1, field2) +macro_rules! simple_event_shunt { + ($obj:expr, $event:ident: $event_type:path => [ + $( $variant:ident $({ $($fields:tt)* })? ),+ + ]) => { + { + use $event_type::*; + match $event { + $( + shunt_helper_enum!( $variant $( [ $($fields)* ] )? ) => { + paste::paste! { + shunt_helper_fn!( { $obj.[<$variant:snake>] } $( [ $($fields)* ] )? ) + } + } + )+ + _ => log::warn!(concat!("unhandled", stringify!($event_type), ": {:?}"), $event) + } + } + } +} + +pub(crate) use shunt_helper_enum; +pub(crate) use shunt_helper_fn; +pub(crate) use simple_event_shunt; + +#[derive(Debug)] +pub(crate) enum SurfaceEvents { + WlSurface(client::wl_surface::Event), + XdgSurface(xdg_surface::Event), + Toplevel(xdg_toplevel::Event), + Popup(xdg_popup::Event), +} +macro_rules! impl_from { + ($type:ty, $variant:ident) => { + impl From<$type> for ObjectEvent { + fn from(value: $type) -> Self { + Self::Surface(SurfaceEvents::$variant(value)) + } + } + }; +} +impl_from!(client::wl_surface::Event, WlSurface); +impl_from!(xdg_surface::Event, XdgSurface); +impl_from!(xdg_toplevel::Event, Toplevel); +impl_from!(xdg_popup::Event, Popup); + +impl HandleEvent for SurfaceData { + type Event = SurfaceEvents; + fn handle_event(&mut self, event: Self::Event, state: &mut ServerState) { + match event { + SurfaceEvents::WlSurface(event) => self.surface_event(event, state), + SurfaceEvents::XdgSurface(event) => self.xdg_event(event, state), + SurfaceEvents::Toplevel(event) => self.toplevel_event(event, state), + SurfaceEvents::Popup(event) => self.popup_event(event, state), + } + } +} + +impl SurfaceData { + fn surface_event( + &self, + event: client::wl_surface::Event, + state: &mut ServerState, + ) { + let surface = &self.server; + simple_event_shunt! { + surface, event: client::wl_surface::Event => [ + Enter { |output| &state.get_object_from_client_object::(&output).server }, + Leave { |output| &state.get_object_from_client_object::(&output).server }, + PreferredBufferScale { factor } + ] + } + } + + fn xdg_event(&mut self, event: xdg_surface::Event, state: &mut ServerState) { + let connection = state.connection.as_mut().unwrap(); + let xdg_surface::Event::Configure { serial } = event else { + unreachable!(); + }; + + let xdg = self.xdg_mut().unwrap(); + xdg.surface.ack_configure(serial); + xdg.configured = true; + + if let Some(pending) = xdg.pending.take() { + let window = state.associated_windows[self.key]; + let window = state.windows.get_mut(&window).unwrap(); + let width = if pending.width > 0 { + pending.width as _ + } else { + window.dims.width + }; + let height = if pending.height > 0 { + pending.height as _ + } else { + window.dims.height + }; + debug!( + "configuring {:?}: {}x{}, {width}x{height}", + window.window, pending.x, pending.y + ); + connection.set_window_dims( + window.window, + PendingSurfaceState { + x: pending.x, + y: pending.y, + width: width as _, + height: height as _, + }, + ); + window.dims = WindowDims { + x: pending.x as _, + y: pending.y as _, + width, + height, + }; + } + + if let Some(SurfaceAttach { buffer, x, y }) = self.attach.take() { + self.client.attach(buffer.as_ref(), x, y); + } + if let Some(cb) = self.frame_callback.take() { + self.client.frame(&state.qh, cb); + } + self.client.commit(); + } + + fn toplevel_event( + &mut self, + event: xdg_toplevel::Event, + state: &mut ServerState, + ) { + match event { + xdg_toplevel::Event::Configure { + width, + height, + states, + } => { + debug!("configuring toplevel {width}x{height}, {states:?}"); + let activated = states.contains(&(u32::from(xdg_toplevel::State::Activated) as u8)); + + if activated { + let window = state.associated_windows[self.key]; + state.to_focus = Some(window); + } + + if let Some(SurfaceRole::Toplevel(Some(toplevel))) = &mut self.role { + let prev_fs = toplevel.fullscreen; + toplevel.fullscreen = + states.contains(&(u32::from(xdg_toplevel::State::Fullscreen) as u8)); + if toplevel.fullscreen != prev_fs { + let window = state.associated_windows[self.key]; + let data = C::ExtraData::create(state); + state.connection.as_mut().unwrap().set_fullscreen( + window, + toplevel.fullscreen, + data, + ); + } + }; + + self.xdg_mut().unwrap().pending = Some(PendingSurfaceState { + width, + height, + ..Default::default() + }); + } + xdg_toplevel::Event::Close => { + let window = state.associated_windows[self.key]; + state.close_x_window(window); + } + ref other => warn!("unhandled xdgtoplevel event: {other:?}"), + } + } + + fn popup_event(&mut self, event: xdg_popup::Event, _: &mut ServerState) { + match event { + xdg_popup::Event::Configure { + x, + y, + width, + height, + } => { + trace!("popup configure: {x}x{y}, {width}x{height}"); + self.xdg_mut().unwrap().pending = Some(PendingSurfaceState { + x, + y, + width, + height, + }); + } + xdg_popup::Event::Repositioned { .. } => {} + other => todo!("{other:?}"), + } + } +} + +pub struct GenericObject { + pub server: Server, + pub client: Client, +} + +pub type Buffer = GenericObject; +impl HandleEvent for Buffer { + type Event = client::wl_buffer::Event; + fn handle_event(&mut self, _: Self::Event, _: &mut ServerState) { + // The only event from a buffer would be the release. + self.server.release(); + } +} + +pub type XdgOutput = GenericObject; +impl HandleEvent for XdgOutput { + type Event = zxdg_output_v1::Event; + fn handle_event(&mut self, event: Self::Event, _: &mut ServerState) { + simple_event_shunt! { + self.server, event: zxdg_output_v1::Event => [ + LogicalPosition { x, y }, + LogicalSize { width, height }, + Done, + Name { name }, + Description { description } + ] + } + } +} + +pub type Seat = GenericObject; +impl HandleEvent for Seat { + type Event = client::wl_seat::Event; + + fn handle_event(&mut self, event: Self::Event, _: &mut ServerState) { + simple_event_shunt! { + self.server, event: client::wl_seat::Event => [ + Capabilities { |capabilities| convert_wenum(capabilities) }, + Name { name } + ] + } + } +} + +pub struct Pointer { + server: WlPointer, + pub client: client::wl_pointer::WlPointer, + pending_enter: PendingEnter, +} + +impl Pointer { + pub fn new(server: WlPointer, client: client::wl_pointer::WlPointer) -> Self { + Self { + server, + client, + pending_enter: PendingEnter(None), + } + } +} + +struct PendingEnter(Option); + +impl HandleEvent for Pointer { + type Event = client::wl_pointer::Event; + + fn handle_event(&mut self, event: Self::Event, state: &mut ServerState) { + // Workaround GTK (stupidly) autoclosing popups if it receives an wl_pointer.enter + // event shortly after creation. + // When Niri creates a popup, it immediately sends wl_pointer.enter on the new surface, + // generating an EnterNotify event, and Xwayland will send a release button event. + // In its menu implementation, GTK treats EnterNotify "this menu is now active" and will + // destroy the menu if this occurs within a 500 ms interval (which it always does with + // Niri). Other compositors do not run into this problem because they appear to not send + // wl_pointer.enter until the user actually moves the mouse in the popup. + let mut process_event = Vec::new(); + match event { + client::wl_pointer::Event::Enter { + serial, + ref surface, + surface_x, + surface_y, + } => { + let do_enter = || { + debug!("entering surface ({serial})"); + let surface = state.get_server_surface_from_client(surface.clone()); + self.server.enter(serial, surface, surface_x, surface_y); + }; + let surface_key: ObjectKey = surface.data().copied().unwrap(); + let surface_data: &SurfaceData = state.objects[surface_key].as_ref(); + + if matches!(surface_data.role, Some(SurfaceRole::Popup(_))) { + match self.pending_enter.0.take() { + Some(e) => { + let client::wl_pointer::Event::Enter { + serial: pending_serial, + .. + } = e + else { + unreachable!(); + }; + if serial == pending_serial { + do_enter(); + } else { + self.pending_enter.0 = Some(event); + } + } + None => { + self.pending_enter.0 = Some(event); + } + } + } else { + self.pending_enter.0.take(); + do_enter(); + } + } + client::wl_pointer::Event::Leave { serial, surface } => { + debug!("leaving surface ({serial})"); + self.pending_enter.0.take(); + self.server + .leave(serial, state.get_server_surface_from_client(surface)); + } + client::wl_pointer::Event::Motion { + time, + surface_x, + surface_y, + } => { + if let Some(p) = &self.pending_enter.0 { + let client::wl_pointer::Event::Enter { + serial, + surface, + surface_x, + surface_y, + } = p + else { + unreachable!(); + }; + process_event.push(client::wl_pointer::Event::Enter { + serial: *serial, + surface: surface.clone(), + surface_x: *surface_x, + surface_y: *surface_y, + }); + process_event.push(event); + trace!("resending enter ({serial}) before motion"); + } else { + self.server.motion(time, surface_x, surface_y); + } + } + _ => simple_event_shunt! { + self.server, event: client::wl_pointer::Event => [ + Enter { + serial, + |surface| state.get_server_surface_from_client(surface), + surface_x, + surface_y + }, + Leave { + serial, + |surface| state.get_server_surface_from_client(surface) + }, + Motion { + time, + surface_x, + surface_y + }, + Frame, + Button { + serial, + time, + button, + |state| convert_wenum(state) + }, + Axis { + time, + |axis| convert_wenum(axis), + value + }, + AxisSource { + |axis_source| convert_wenum(axis_source) + }, + AxisStop { + time, + |axis| convert_wenum(axis) + }, + AxisDiscrete { + |axis| convert_wenum(axis), + discrete + }, + AxisValue120 { + |axis| convert_wenum(axis), + value120 + }, + AxisRelativeDirection { + |axis| convert_wenum(axis), + |direction| convert_wenum(direction) + } + ] + }, + } + + for event in process_event { + self.handle_event(event, state); + } + } +} + +pub type Keyboard = GenericObject; +impl HandleEvent for Keyboard { + type Event = client::wl_keyboard::Event; + + fn handle_event(&mut self, event: Self::Event, state: &mut ServerState) { + simple_event_shunt! { + self.server, event: client::wl_keyboard::Event => [ + Keymap { + |format| convert_wenum(format), + |fd| fd.as_fd(), + size + }, + Enter { + serial, + |surface| state.get_server_surface_from_client(surface), + keys + }, + Leave { + serial, + |surface| state.get_server_surface_from_client(surface) + }, + Key { + serial, + time, + key, + |state| convert_wenum(state) + }, + Modifiers { + serial, + mods_depressed, + mods_latched, + mods_locked, + group + }, + RepeatInfo { + rate, + delay + } + ] + } + } +} + +pub type Output = GenericObject; +impl HandleEvent for Output { + type Event = client::wl_output::Event; + + fn handle_event(&mut self, event: Self::Event, _: &mut ServerState) { + simple_event_shunt! { + self.server, event: client::wl_output::Event => [ + Name { name }, + Description { description }, + Mode { + |flags| convert_wenum(flags), + width, + height, + refresh + }, + Scale { factor }, + Geometry { + x, + y, + physical_width, + physical_height, + |subpixel| convert_wenum(subpixel), + make, + model, + |transform| convert_wenum(transform) + }, + Done + ] + } + } +} + +pub type Drm = GenericObject; +impl HandleEvent for Drm { + type Event = wl_drm::client::wl_drm::Event; + + fn handle_event(&mut self, event: Self::Event, _: &mut ServerState) { + simple_event_shunt! { + self.server, event: wl_drm::client::wl_drm::Event => [ + Device { name }, + Format { format }, + Authenticated, + Capabilities { value } + ] + } + } +} + +pub type DmabufFeedback = GenericObject< + s_dmabuf::zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1, + c_dmabuf::zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1, +>; +impl HandleEvent for DmabufFeedback { + type Event = c_dmabuf::zwp_linux_dmabuf_feedback_v1::Event; + + fn handle_event(&mut self, event: Self::Event, _: &mut ServerState) { + simple_event_shunt! { + self.server, event: c_dmabuf::zwp_linux_dmabuf_feedback_v1::Event => [ + Done, + FormatTable { |fd| fd.as_fd(), size }, + MainDevice { device }, + TrancheDone, + TrancheTargetDevice { device }, + TrancheFormats { indices }, + TrancheFlags { |flags| convert_wenum(flags) } + ] + } + } +} + +pub type RelativePointer = GenericObject; +impl HandleEvent for RelativePointer { + type Event = zwp_relative_pointer_v1::Event; + + fn handle_event(&mut self, event: Self::Event, _: &mut ServerState) { + simple_event_shunt! { + self.server, event: rp::client::zwp_relative_pointer_v1::Event => [ + RelativeMotion { + utime_hi, + utime_lo, + dx, + dy, + dx_unaccel, + dy_unaccel + } + ] + } + } +} diff --git a/satellite/src/server/mod.rs b/satellite/src/server/mod.rs new file mode 100644 index 0000000..63dc9fa --- /dev/null +++ b/satellite/src/server/mod.rs @@ -0,0 +1,768 @@ +mod dispatch; +mod event; + +#[cfg(test)] +mod tests; + +use self::event::*; +use super::FromServerState; +use crate::clientside::*; +use crate::xstate::{Atoms, WindowDims, WmNormalHints}; +use crate::XConnection; +use log::{debug, warn}; +use rustix::event::{poll, PollFd, PollFlags}; +use slotmap::{new_key_type, HopSlotMap, SparseSecondaryMap}; +use std::collections::HashMap; +use std::os::fd::{AsFd, BorrowedFd}; +use std::os::unix::net::UnixStream; +use wayland_client::{protocol as client, Proxy}; +use wayland_protocols::{ + wp::{ + linux_dmabuf::zv1::{client as c_dmabuf, server as s_dmabuf}, + relative_pointer::zv1::{ + self as rp, server::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, + }, + viewporter::server as s_vp, + }, + xdg::{ + shell::client::{ + xdg_popup::XdgPopup, + xdg_positioner::{Anchor, Gravity, XdgPositioner}, + xdg_surface::XdgSurface, + xdg_toplevel::XdgToplevel, + xdg_wm_base::XdgWmBase, + }, + xdg_output::zv1::server::zxdg_output_manager_v1::ZxdgOutputManagerV1, + }, +}; +use wayland_server::{ + protocol::{ + wl_callback::WlCallback, wl_compositor::WlCompositor, wl_output::WlOutput, wl_seat::WlSeat, + wl_shm::WlShm, wl_surface::WlSurface, + }, + DisplayHandle, Resource, WEnum, +}; +use wl_drm::{client::wl_drm::WlDrm as WlDrmClient, server::wl_drm::WlDrm as WlDrmServer}; +use xcb::x; + +impl From<&x::CreateNotifyEvent> for WindowDims { + fn from(value: &x::CreateNotifyEvent) -> Self { + Self { + x: value.x(), + y: value.y(), + width: value.width(), + height: value.height(), + } + } +} + +type Request = ::Request; + +/// Converts a WEnum from its client side version to its server side version +fn convert_wenum(wenum: WEnum) -> Server +where + u32: From>, + Server: TryFrom, + >::Error: std::fmt::Debug, +{ + u32::from(wenum).try_into().unwrap() +} + +#[derive(Debug)] +struct WindowData { + window: x::Window, + surface_key: Option, + mapped: bool, + surface_id: u32, + popup_for: Option, + dims: WindowDims, + hints: Option, + override_redirect: bool, +} + +impl WindowData { + fn new( + window: x::Window, + override_redirect: bool, + dims: WindowDims, + parent: Option, + ) -> Self { + Self { + window, + surface_key: None, + mapped: false, + popup_for: parent, + surface_id: 0, + dims, + hints: None, + override_redirect, + } + } +} + +struct SurfaceAttach { + buffer: Option, + x: i32, + y: i32, +} + +pub struct SurfaceData { + client: client::wl_surface::WlSurface, + server: WlSurface, + key: ObjectKey, + frame_callback: Option, + attach: Option, + role: Option, +} + +impl SurfaceData { + fn xdg(&self) -> Option<&XdgSurfaceData> { + match self + .role + .as_ref() + .expect("Tried to get XdgSurface for surface without role") + { + SurfaceRole::Toplevel(ref t) => t.as_ref().map(|t| &t.xdg), + SurfaceRole::Popup(ref p) => p.as_ref().map(|p| &p.xdg), + } + } + + fn xdg_mut(&mut self) -> Option<&mut XdgSurfaceData> { + match self + .role + .as_mut() + .expect("Tried to get XdgSurface for surface without role") + { + SurfaceRole::Toplevel(ref mut t) => t.as_mut().map(|t| &mut t.xdg), + SurfaceRole::Popup(ref mut p) => p.as_mut().map(|p| &mut p.xdg), + } + } + + fn destroy_role(&mut self) { + if let Some(role) = self.role.take() { + match role { + SurfaceRole::Toplevel(Some(t)) => { + t.toplevel.destroy(); + t.xdg.surface.destroy(); + } + SurfaceRole::Popup(Some(p)) => { + p.positioner.destroy(); + p.popup.destroy(); + p.xdg.surface.destroy(); + } + _ => {} + } + } + } +} + +#[derive(Debug)] +enum SurfaceRole { + Toplevel(Option), + Popup(Option), +} + +#[derive(Debug)] +struct XdgSurfaceData { + surface: XdgSurface, + configured: bool, + pending: Option, +} + +#[derive(Debug)] +struct ToplevelData { + toplevel: XdgToplevel, + xdg: XdgSurfaceData, + fullscreen: bool, +} + +#[derive(Debug)] +struct PopupData { + popup: XdgPopup, + positioner: XdgPositioner, + xdg: XdgSurfaceData, +} + +pub(crate) trait HandleEvent { + type Event; + fn handle_event(&mut self, event: Self::Event, state: &mut ServerState); +} + +macro_rules! enum_try_from { + ( + $(#[$meta:meta])* + $pub:vis enum $enum:ident { + $( $variant:ident($ty:ty) ),+ + } + ) => { + $(#[$meta])* + $pub enum $enum { + $( $variant($ty) ),+ + } + + $( + impl TryFrom<$enum> for $ty { + type Error = String; + fn try_from(value: $enum) -> Result { + enum_try_from!(@variant_match value $enum $variant) + } + } + + impl<'a> TryFrom<&'a $enum> for &'a $ty { + type Error = String; + fn try_from(value: &'a $enum) -> Result { + enum_try_from!(@variant_match value $enum $variant) + } + } + + impl<'a> TryFrom<&'a mut $enum> for &'a mut $ty { + type Error = String; + fn try_from(value: &'a mut $enum) -> Result { + enum_try_from!(@variant_match value $enum $variant) + } + } + + impl From<$ty> for $enum { + fn from(value: $ty) -> Self { + $enum::$variant(value) + } + } + )+ + }; + (@variant_match $value:ident $enum:ident $variant:ident) => { + match $value { + $enum::$variant(obj) => Ok(obj), + other => Err(format!("wrong variant type: {}", std::any::type_name_of_val(&other))) + } + } +} + +/// Implement HandleEvent for our enum +macro_rules! handle_event_enum { + ( + $(#[$meta:meta])* + $pub:vis enum $name:ident { + $( $variant:ident($ty:ty) ),+ + } + ) => { + enum_try_from! { + $(#[$meta])* + $pub enum $name { + $( $variant($ty) ),+ + } + } + + paste::paste! { + enum_try_from! { + #[derive(Debug)] + $pub enum [<$name Event>] { + $( $variant(<$ty as HandleEvent>::Event) ),+ + } + } + } + + impl HandleEvent for $name { + paste::paste! { + type Event = [<$name Event>]; + } + + fn handle_event(&mut self, event: Self::Event, state: &mut ServerState) { + match self { + $( + Self::$variant(v) => { + let Self::Event::$variant(event) = event else { + unreachable!(); + }; + v.handle_event(event, state) + } + ),+ + } + } + } + } +} + +handle_event_enum! { + +/// Objects that generate client side events that we will have to process. +pub(crate) enum Object { + Surface(SurfaceData), + Buffer(Buffer), + Seat(Seat), + Pointer(Pointer), + Keyboard(Keyboard), + Output(Output), + RelativePointer(RelativePointer), + DmabufFeedback(DmabufFeedback), + Drm(Drm), + XdgOutput(XdgOutput) +} + +} + +struct WrappedObject(Option); + +impl From for WrappedObject +where + T: Into, +{ + fn from(value: T) -> Self { + Self(Some(value.into())) + } +} + +impl AsRef for WrappedObject +where + for<'a> &'a T: TryFrom<&'a Object, Error = String>, +{ + fn as_ref(&self) -> &T { + <&T>::try_from(self.0.as_ref().unwrap()).unwrap() + } +} + +impl AsMut for WrappedObject +where + for<'a> &'a mut T: TryFrom<&'a mut Object, Error = String>, +{ + fn as_mut(&mut self) -> &mut T { + <&mut T>::try_from(self.0.as_mut().unwrap()).unwrap() + } +} + +type ObjectMap = HopSlotMap; +trait ObjectMapExt { + fn insert_from_other_objects(&mut self, keys: [ObjectKey; N], insert_fn: F) + where + F: FnOnce([&Object; N], ObjectKey) -> Object; +} + +impl ObjectMapExt for ObjectMap { + /// Insert an object into our map that needs some other values from our map as well + fn insert_from_other_objects(&mut self, keys: [ObjectKey; N], insert_fn: F) + where + F: FnOnce([&Object; N], ObjectKey) -> Object, + { + let objects = keys.each_ref().map(|key| self[*key].0.take().unwrap()); + let key = self.insert(WrappedObject(None)); + let obj = insert_fn(objects.each_ref(), key); + debug_assert!(self[key].0.replace(obj).is_none()); + for (object, key) in objects.into_iter().zip(keys.into_iter()) { + debug_assert!(self[key].0.replace(object).is_none()); + } + } +} + +new_key_type! { + pub struct ObjectKey; +} +pub struct ServerState { + pub atoms: Option, + dh: DisplayHandle, + clientside: ClientState, + objects: ObjectMap, + associated_windows: SparseSecondaryMap, + windows: HashMap, + + xdg_wm_base: XdgWmBase, + qh: ClientQueueHandle, + to_focus: Option, + last_focused_toplevel: Option, + connection: Option, +} + +const XDG_WM_BASE_VERSION: u32 = 2; + +impl ServerState { + pub fn new(dh: DisplayHandle, server_connection: Option) -> Self { + let mut clientside = ClientState::new(server_connection); + let qh = clientside.qh.clone(); + + let xdg_pos = clientside + .globals + .new_globals + .iter() + .position(|g| g.interface == XdgWmBase::interface().name) + .expect("Did not get an xdg_wm_base global"); + + let data = clientside.globals.new_globals.swap_remove(xdg_pos); + + assert!( + data.version >= XDG_WM_BASE_VERSION, + "xdg_wm_base older than version {XDG_WM_BASE_VERSION}" + ); + + let xdg_wm_base = + clientside + .registry + .bind::(data.name, XDG_WM_BASE_VERSION, &qh, ()); + + let mut ret = Self { + windows: HashMap::new(), + clientside, + atoms: None, + qh, + dh, + to_focus: None, + last_focused_toplevel: None, + connection: None, + objects: Default::default(), + associated_windows: Default::default(), + xdg_wm_base, + }; + ret.handle_new_globals(); + ret + } + + pub fn clientside_fd(&self) -> BorrowedFd<'_> { + self.clientside.queue.as_fd() + } + + pub fn connect(&mut self, connection: UnixStream) { + self.dh + .insert_client(connection, std::sync::Arc::new(())) + .unwrap(); + } + + pub fn set_x_connection(&mut self, connection: C) { + self.connection = Some(connection); + } + + fn handle_new_globals(&mut self) { + let globals = std::mem::take(&mut self.clientside.globals.new_globals); + for data in globals { + macro_rules! server_global { + ($($global:ty),+) => { + match data.interface { + $( + ref x if x == <$global>::interface().name => { + self.dh.create_global::(data.version, data); + } + )+ + _ => {} + } + } + } + + server_global![ + WlCompositor, + WlShm, + WlSeat, + WlOutput, + ZwpRelativePointerManagerV1, + WlDrmServer, + s_dmabuf::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, + ZxdgOutputManagerV1, + s_vp::wp_viewporter::WpViewporter + ]; + } + } + + fn get_object_from_client_object(&self, proxy: &P) -> &T + where + for<'a> &'a T: TryFrom<&'a Object, Error = String>, + Globals: wayland_client::Dispatch, + { + let key: ObjectKey = proxy.data().copied().unwrap(); + self.objects[key].as_ref() + } + + pub fn new_window( + &mut self, + window: x::Window, + override_redirect: bool, + dims: WindowDims, + parent: Option, + ) { + self.windows.insert( + window, + WindowData::new(window, override_redirect, dims, parent), + ); + } + + pub fn set_win_hints(&mut self, window: x::Window, hints: WmNormalHints) { + let win = self.windows.get_mut(&window).unwrap(); + + if win.hints.is_none() || *win.hints.as_ref().unwrap() != hints { + debug!("setting {window:?} hints {hints:?}"); + if let Some(surface) = win.surface_key { + let surface: &SurfaceData = self.objects[surface].as_ref(); + if let Some(SurfaceRole::Toplevel(Some(data))) = &surface.role { + if let Some(min_size) = &hints.min_size { + data.toplevel.set_min_size(min_size.width, min_size.height); + } + if let Some(max_size) = &hints.max_size { + data.toplevel.set_max_size(max_size.width, max_size.height); + } + } + } + win.hints = Some(hints); + } + } + + pub fn associate_window(&mut self, window: x::Window, surface_id: u32) { + let win = self.windows.get_mut(&window).unwrap(); + win.surface_id = surface_id; + + if let Some(key) = self + .objects + .iter_mut() + .filter_map(|(key, obj)| { + Some(key).zip(<&mut SurfaceData>::try_from(obj.0.as_mut().unwrap()).ok()) + }) + .find_map(|(key, surface)| { + (surface_id == surface.server.id().protocol_id()).then_some(key) + }) + { + win.surface_key = Some(key); + self.associated_windows.insert(key, window); + debug!("associate {:?} with surface {surface_id}", window); + if win.mapped { + self.create_role_window(window, key); + } + } + } + + pub fn reconfigure_window(&mut self, event: x::ConfigureNotifyEvent) { + let win = self.windows.get_mut(&event.window()).unwrap(); + win.dims = WindowDims { + x: event.x(), + y: event.y(), + width: event.width(), + height: event.height(), + }; + } + + pub fn map_window(&mut self, window: x::Window) { + debug!("mapping {window:?}"); + + let window = self.windows.get_mut(&window).unwrap(); + window.mapped = true; + } + + pub fn unmap_window(&mut self, window: x::Window) { + let Some(win) = self.windows.get_mut(&window) else { + return; + }; + if !win.mapped { + return; + } + debug!("unmapping {window:?}"); + + if matches!(self.last_focused_toplevel, Some(x) if x == window) { + self.last_focused_toplevel.take(); + } + win.mapped = false; + + if let Some(key) = win.surface_key.take() { + let surface: &mut SurfaceData = self.objects[key].as_mut(); + surface.destroy_role(); + } + } + + pub fn set_fullscreen(&mut self, window: x::Window, state: super::xstate::SetState) { + let win = self.windows.get(&window).unwrap(); + let Some(key) = win.surface_key else { + warn!("Tried to set window without surface fullscreen: {window:?}"); + return; + }; + let surface: &mut SurfaceData = self.objects[key].as_mut(); + let Some(SurfaceRole::Toplevel(Some(ref toplevel))) = surface.role else { + warn!("Tried to set an unmapped toplevel or non toplevel fullscreen: {window:?}"); + return; + }; + + match state { + crate::xstate::SetState::Add => toplevel.toplevel.set_fullscreen(None), + crate::xstate::SetState::Remove => toplevel.toplevel.unset_fullscreen(), + crate::xstate::SetState::Toggle => { + if toplevel.fullscreen { + toplevel.toplevel.unset_fullscreen() + } else { + toplevel.toplevel.set_fullscreen(None) + } + } + } + } + + pub fn destroy_window(&mut self, window: x::Window) { + let _ = self.windows.remove(&window); + } + + pub fn run(&mut self) { + if let Some(r) = self.clientside.queue.prepare_read() { + let fd = r.connection_fd(); + let pollfd = PollFd::new(&fd, PollFlags::IN); + if poll(&mut [pollfd], 0).unwrap() > 0 { + let _ = r.read(); + } + } + self.clientside + .queue + .dispatch_pending(&mut self.clientside.globals) + .unwrap(); + self.handle_clientside_events(); + self.clientside.queue.flush().unwrap(); + } + + pub fn handle_clientside_events(&mut self) { + self.handle_new_globals(); + + let client_events = std::mem::take(&mut self.clientside.globals.events); + for (key, event) in client_events { + let object = &mut self.objects[key]; + let mut object = object.0.take().unwrap(); + object.handle_event(event, self); + debug_assert!(self.objects[key].0.replace(object).is_none()); + } + + { + if let Some(win) = self.to_focus.take() { + let data = C::ExtraData::create(self); + let conn = self.connection.as_mut().unwrap(); + debug!("focusing window {win:?}"); + conn.focus_window(win, data); + self.last_focused_toplevel = Some(win); + } + } + + self.clientside.queue.flush().unwrap(); + } + + fn create_role_window(&mut self, window: x::Window, surface_key: ObjectKey) { + let surface: &SurfaceData = self.objects[surface_key].as_ref(); + let client = &surface.client; + client.attach(None, 0, 0); + client.commit(); + + let xdg_surface = self + .xdg_wm_base + .get_xdg_surface(client, &self.qh, surface_key); + + let window_data = self.windows.get_mut(&window).unwrap(); + if window_data.override_redirect { + // Override redirect is hard to convert to Wayland! + // We will just make them be popups for the last focused toplevel. + if let Some(win) = self.last_focused_toplevel { + window_data.popup_for = Some(win) + } + } + let window = self.windows.get(&window).unwrap(); + + let role = if let Some(parent) = window.popup_for { + debug!( + "creating popup ({:?}) {:?} {:?} {:?} {surface_key:?}", + window.window, + parent, + window.dims, + surface.client.id() + ); + + let parent_window = self.windows.get(&parent).unwrap(); + let parent_surface: &SurfaceData = + self.objects[parent_window.surface_key.unwrap()].as_ref(); + + let positioner = self.xdg_wm_base.create_positioner(&self.qh, ()); + positioner.set_size(window.dims.width as _, window.dims.height as _); + positioner.set_offset(window.dims.x as i32, window.dims.y as i32); + positioner.set_anchor(Anchor::TopLeft); + positioner.set_gravity(Gravity::BottomRight); + positioner.set_anchor_rect( + 0, + 0, + parent_window.dims.width as _, + parent_window.dims.height as _, + ); + let popup = xdg_surface.get_popup( + Some(&parent_surface.xdg().unwrap().surface), + &positioner, + &self.qh, + surface_key, + ); + let popup = PopupData { + popup, + positioner, + xdg: XdgSurfaceData { + surface: xdg_surface, + configured: false, + pending: None, + }, + }; + SurfaceRole::Popup(Some(popup)) + } else { + let data = self.create_toplevel(window, surface_key, xdg_surface); + SurfaceRole::Toplevel(Some(data)) + }; + + let surface: &mut SurfaceData = self.objects[surface_key].as_mut(); + + let new_role_type = std::mem::discriminant(&role); + let prev = surface.role.replace(role); + if let Some(role) = prev { + let old_role_type = std::mem::discriminant(&role); + assert_eq!( + new_role_type, old_role_type, + "Surface for {:?} already had a role: {:?}", + window.window, role + ); + } + + surface.client.commit(); + } + + fn create_toplevel( + &self, + window: &WindowData, + surface_key: ObjectKey, + xdg: XdgSurface, + ) -> ToplevelData { + debug!("creating toplevel for {:?}", window.window); + let toplevel = xdg.get_toplevel(&self.qh, surface_key); + if let Some(hints) = &window.hints { + if let Some(min) = &hints.min_size { + toplevel.set_min_size(min.width, min.height); + } + if let Some(max) = &hints.max_size { + toplevel.set_max_size(max.width, max.height); + } + } + + ToplevelData { + xdg: XdgSurfaceData { + surface: xdg, + configured: false, + pending: None, + }, + toplevel, + fullscreen: false, + } + } + + fn get_server_surface_from_client(&self, surface: client::wl_surface::WlSurface) -> &WlSurface { + let key: &ObjectKey = surface.data().unwrap(); + let surface: &SurfaceData = self.objects[*key].as_ref(); + &surface.server + } + + fn get_client_surface_from_server(&self, surface: WlSurface) -> &client::wl_surface::WlSurface { + let key: &ObjectKey = surface.data().unwrap(); + let surface: &SurfaceData = self.objects[*key].as_ref(); + &surface.client + } + + fn close_x_window(&mut self, window: x::Window) { + debug!("sending close request to {window:?}"); + let data = C::ExtraData::create(self); + self.connection.as_mut().unwrap().close_window(window, data); + if self.last_focused_toplevel == Some(window) { + self.last_focused_toplevel.take(); + } + } +} + +#[derive(Default, Debug)] +pub struct PendingSurfaceState { + pub x: i32, + pub y: i32, + pub width: i32, + pub height: i32, +} diff --git a/satellite/src/server/tests.rs b/satellite/src/server/tests.rs new file mode 100644 index 0000000..7970777 --- /dev/null +++ b/satellite/src/server/tests.rs @@ -0,0 +1,866 @@ +use super::{ServerState, WindowDims}; +use crate::xstate::SetState; +use paste::paste; +use rustix::event::{poll, PollFd, PollFlags}; +use std::collections::HashMap; +use std::os::fd::BorrowedFd; +use std::os::unix::net::UnixStream; +use std::sync::{Arc, Mutex}; +use wayland_client::{ + backend::{protocol::Message, Backend, ObjectData, ObjectId, WaylandError}, + protocol::{ + wl_buffer::WlBuffer, + wl_compositor::WlCompositor, + wl_display::WlDisplay, + wl_registry::WlRegistry, + wl_seat::WlSeat, + wl_shm::{Format, WlShm}, + wl_shm_pool::WlShmPool, + wl_surface::WlSurface, + }, + Connection, Proxy, WEnum, +}; +use wayland_protocols::{ + wp::{ + linux_dmabuf::zv1::client::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, + relative_pointer::zv1::client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, + viewporter::client::wp_viewporter::WpViewporter, + }, + xdg::{ + shell::server::{xdg_positioner, xdg_toplevel}, + xdg_output::zv1::client::zxdg_output_manager_v1::ZxdgOutputManagerV1, + }, +}; +use wayland_server::{protocol as s_proto, Display, Resource}; +use wl_drm::client::wl_drm::WlDrm; +use xcb::x::Window; + +use xcb::XidNew; + +macro_rules! with_optional { + ( + $( #[$attr:meta] )? + struct $name:ident$(<$($lifetimes:lifetime),+>)? { + $( + $field:ident: $type:ty + ),+$(,)? + } + ) => { + $( #[$attr] )? + struct $name$(<$($lifetimes),+>)? { + $( + $field: $type + ),+ + } + + paste! { + #[derive(Default)] + struct [< $name Optional >] { + $( + $field: Option<$type> + ),+ + } + } + + paste! { + impl From<[<$name Optional>]> for $name { + fn from(opt: [<$name Optional>]) -> Self { + Self { + $( + $field: opt.$field.expect(concat!("uninitialized field ", stringify!($field))) + ),+ + } + } + } + } + } +} + +with_optional! { + +struct Compositor { + compositor: TestObject, + shm: TestObject, +} + +} + +impl Compositor { + fn create_surface(&self) -> (TestObject, TestObject) { + let fd = unsafe { BorrowedFd::borrow_raw(0) }; + let pool = TestObject::::from_request( + &self.shm.obj, + Req::::CreatePool { fd, size: 1024 }, + ); + let buffer = TestObject::::from_request( + &pool.obj, + Req::::CreateBuffer { + offset: 0, + width: 10, + height: 10, + stride: 1, + format: WEnum::Value(Format::Xrgb8888A8), + }, + ); + let surface = TestObject::::from_request( + &self.compositor.obj, + Req::::CreateSurface {}, + ); + surface + .send_request(Req::::Attach { + buffer: Some(buffer.obj.clone()), + x: 0, + y: 0, + }) + .unwrap(); + + (buffer, surface) + } +} + +#[derive(Debug)] +struct WindowData { + mapped: bool, + fullscreen: bool, + dims: WindowDims, +} +struct FakeXConnection { + root: Window, + focused_window: Option, + windows: HashMap, +} + +impl FakeXConnection { + #[track_caller] + fn window(&mut self, window: Window) -> &mut WindowData { + self.windows + .get_mut(&window) + .expect("Unknown window: {window:?}") + } +} + +impl Default for FakeXConnection { + fn default() -> Self { + Self { + root: unsafe { Window::new(9001) }, + focused_window: None, + windows: HashMap::new(), + } + } +} + +impl super::FromServerState for () { + fn create(_: &FakeServerState) -> Self { + () + } +} + +impl super::XConnection for FakeXConnection { + type ExtraData = (); + fn root_window(&self) -> Window { + self.root + } + + #[track_caller] + fn close_window(&mut self, window: Window, _: ()) { + log::debug!("closing window {window:?}"); + self.window(window).mapped = false; + } + + #[track_caller] + fn set_fullscreen(&mut self, window: xcb::x::Window, fullscreen: bool, _: ()) { + self.window(window).fullscreen = fullscreen; + } + + #[track_caller] + fn set_window_dims(&mut self, window: Window, state: super::PendingSurfaceState) { + self.window(window).dims = WindowDims { + x: state.x as _, + y: state.y as _, + width: state.width as _, + height: state.height as _, + }; + } + + #[track_caller] + fn focus_window(&mut self, window: Window, _: ()) { + assert!( + self.windows.contains_key(&window), + "Unknown window: {window:?}" + ); + self.focused_window = window.into(); + } +} + +type FakeServerState = ServerState; + +struct TestFixture { + testwl: testwl::Server, + exwayland: FakeServerState, + /// Our connection to exwayland - i.e., where Xwayland sends requests to + exwl_connection: Arc, + /// Exwayland's display - must dispatch this for our server state to advance + exwl_display: Display, +} + +static INIT: std::sync::Once = std::sync::Once::new(); + +impl TestFixture { + fn new() -> Self { + INIT.call_once(|| { + env_logger::builder() + .is_test(true) + .filter_level(log::LevelFilter::Trace) + .init() + }); + + let (client_s, server_s) = UnixStream::pair().unwrap(); + let mut testwl = testwl::Server::new(true); + let display = Display::::new().unwrap(); + testwl.connect(server_s); + // Handle initial globals roundtrip setup requirement + let thread = std::thread::spawn(move || { + let mut pollfd = [PollFd::from_borrowed_fd(testwl.poll_fd(), PollFlags::IN)]; + if poll(&mut pollfd, 1000).unwrap() == 0 { + panic!("Did not get events for testwl!"); + } + testwl.dispatch(); + testwl + }); + let mut exwayland = FakeServerState::new(display.handle(), Some(client_s)); + let testwl = thread.join().unwrap(); + + let (fake_client, ex_server) = UnixStream::pair().unwrap(); + exwayland.connect(ex_server); + + exwayland.set_x_connection(FakeXConnection::default()); + let mut f = TestFixture { + testwl, + exwayland, + exwl_connection: Connection::from_socket(fake_client).unwrap().into(), + exwl_display: display, + }; + f.run(); + f + } + + fn new_with_compositor() -> (Self, Compositor) { + let mut f = Self::new(); + let compositor = f.compositor(); + (f, compositor) + } + + fn connection(&self) -> &FakeXConnection { + self.exwayland.connection.as_ref().unwrap() + } + + fn compositor(&mut self) -> Compositor { + let mut ret = CompositorOptional::default(); + let wl_display = self.exwl_connection.display(); + + let registry = + TestObject::::from_request(&wl_display, Req::::GetRegistry {}); + self.run(); + + let events = std::mem::take(&mut *registry.data.events.lock().unwrap()); + assert!(events.len() > 0); + + let bind_req = |name, interface, version| Req::::Bind { + name, + id: (interface, version), + }; + + for event in events { + if let Ev::::Global { + name, + interface, + version, + } = event + { + match interface { + x if x == WlCompositor::interface().name => { + ret.compositor = Some(TestObject::from_request( + ®istry.obj, + bind_req(name, WlCompositor::interface(), version), + )); + } + x if x == WlShm::interface().name => { + ret.shm = Some(TestObject::from_request( + ®istry.obj, + bind_req(name, WlShm::interface(), version), + )); + } + _ => {} + } + } + } + + ret.into() + } + + /// Cascade our requests/events through exwayland and testwl + fn run(&mut self) { + // Flush our requests to exwayland + self.exwl_connection.flush().unwrap(); + + // Have exwayland dispatch our requests + self.exwl_display + .dispatch_clients(&mut self.exwayland) + .unwrap(); + self.exwl_display.flush_clients().unwrap(); + + // Dispatch any clientside requests + self.exwayland.run(); + + // Have testwl dispatch the clientside requests + self.testwl.dispatch(); + + // Handle clientside events + self.exwayland.handle_clientside_events(); + + self.testwl.dispatch(); + + // Get our events + let res = self.exwl_connection.prepare_read().unwrap().read(); + if res.is_err() + && !matches!(res, Err(WaylandError::Io(ref e)) if e.kind() == std::io::ErrorKind::WouldBlock) + { + panic!("Read failed: {res:?}") + } + } + + fn register_window(&mut self, window: Window, data: WindowData) { + self.exwayland + .connection + .as_mut() + .unwrap() + .windows + .insert(window, data); + } + + fn create_and_map_window( + &mut self, + comp: &Compositor, + window: Window, + override_redirect: bool, + ) -> (TestObject, testwl::SurfaceId) { + let (_buffer, surface) = comp.create_surface(); + + let data = WindowData { + mapped: true, + dims: WindowDims { + x: 0, + y: 0, + width: 50, + height: 50, + }, + fullscreen: false + }; + + let dims = data.dims; + + self.register_window(window, data); + self.exwayland + .new_window(window, override_redirect, dims, None); + self.exwayland.map_window(window); + self.exwayland + .associate_window(window, surface.id().protocol_id()); + + self.run(); + + let testwl_id = self + .testwl + .last_created_surface_id() + .expect("Surface not created"); + + assert!(self.testwl.get_surface_data(testwl_id).is_some()); + + (surface, testwl_id) + } + + fn create_toplevel( + &mut self, + comp: &Compositor, + window: Window, + ) -> (TestObject, testwl::SurfaceId) { + let (_buffer, surface) = comp.create_surface(); + + let data = WindowData { + mapped: true, + dims: WindowDims { + x: 0, + y: 0, + width: 50, + height: 50, + }, + fullscreen: false + }; + + let dims = data.dims; + + self.register_window(window, data); + self.exwayland.new_window(window, false, dims, None); + self.exwayland.map_window(window); + self.exwayland + .associate_window(window, surface.id().protocol_id()); + + self.run(); + + let testwl_id = self + .testwl + .last_created_surface_id() + .expect("Toplevel surface not created"); + { + let surface_data = self.testwl.get_surface_data(testwl_id).unwrap(); + assert!( + surface_data.surface + == self + .testwl + .get_object::(testwl_id) + .unwrap() + ); + assert!(surface_data.buffer.is_none()); + assert!( + matches!(surface_data.role, Some(testwl::SurfaceRole::Toplevel(_))), + "surface role: {:?}", + surface_data.role + ); + } + + self.testwl + .configure_toplevel(testwl_id, 100, 100, vec![xdg_toplevel::State::Activated]); + self.run(); + + { + let surface_data = self.testwl.get_surface_data(testwl_id).unwrap(); + assert!(surface_data.buffer.is_some()); + } + + let win_data = self.connection().windows.get(&window).map(|d| &d.dims); + assert!( + matches!( + win_data, + Some(&super::WindowDims { + x: 0, + y: 0, + width: 100, + height: 100 + }) + ), + "Incorrect window geometry: {win_data:?}" + ); + + (surface, testwl_id) + } + + fn create_popup( + &mut self, + comp: &Compositor, + window: Window, + parent_id: testwl::SurfaceId, + ) -> (TestObject, testwl::SurfaceId) { + let (_, popup_surface) = comp.create_surface(); + let data = WindowData { + mapped: true, + dims: WindowDims { + x: 10, + y: 10, + width: 50, + height: 50, + }, + fullscreen: false + }; + let dims = data.dims; + self.register_window(window, data); + self.exwayland.new_window(window, true, dims, None); + self.exwayland.map_window(window); + self.exwayland + .associate_window(window, popup_surface.id().protocol_id()); + self.run(); + let popup_id = self.testwl.last_created_surface_id().unwrap(); + assert_ne!(popup_id, parent_id); + + { + let surface_data = self.testwl.get_surface_data(popup_id).unwrap(); + assert!( + surface_data.surface + == self + .testwl + .get_object::(popup_id) + .unwrap() + ); + assert!(surface_data.buffer.is_none()); + assert!( + matches!(surface_data.role, Some(testwl::SurfaceRole::Popup(_))), + "surface was not a popup (role: {:?})", + surface_data.role + ); + + let toplevel_xdg = &self + .testwl + .get_surface_data(parent_id) + .unwrap() + .xdg() + .surface; + assert_eq!(&surface_data.popup().parent, toplevel_xdg); + + let pos = &surface_data.popup().positioner_state; + assert_eq!(pos.size.as_ref().unwrap(), &testwl::Vec2 { x: 50, y: 50 }); + assert_eq!( + pos.anchor_rect.as_ref().unwrap(), + &testwl::Rect { + size: testwl::Vec2 { x: 100, y: 100 }, + offset: testwl::Vec2::default() + } + ); + assert_eq!( + pos.offset, + testwl::Vec2 { + x: dims.x as _, + y: dims.y as _ + } + ); + assert_eq!(pos.anchor, xdg_positioner::Anchor::TopLeft); + assert_eq!(pos.gravity, xdg_positioner::Gravity::BottomRight); + } + + self.testwl.configure_popup(popup_id); + self.run(); + + { + let surface_data = self.testwl.get_surface_data(popup_id).unwrap(); + assert!(surface_data.buffer.is_some()); + } + + (popup_surface, popup_id) + } +} + +struct TestObjectData { + events: Mutex>, + _phantom: std::marker::PhantomData, +} + +impl Default for TestObjectData { + fn default() -> Self { + Self { + events: Default::default(), + _phantom: Default::default(), + } + } +} + +impl ObjectData for TestObjectData +where + T::Event: Send + Sync + std::fmt::Debug, +{ + fn event( + self: Arc, + backend: &Backend, + msg: Message, + ) -> Option> { + let connection = Connection::from_backend(backend.clone()); + let event = T::parse_event(&connection, msg).unwrap().1; + self.events.lock().unwrap().push(event); + None + } + + fn destroyed(&self, _: ObjectId) {} +} + +struct TestObject { + obj: T, + data: Arc>, +} + +impl std::ops::Deref for TestObject { + type Target = T; + fn deref(&self) -> &T { + &self.obj + } +} + +impl TestObject +where + T::Event: Sync + Send + std::fmt::Debug, +{ + fn from_request(object: &P, request: P::Request<'_>) -> Self { + let data = Arc::>::default(); + let obj: T = P::send_constructor(object, request, data.clone()).unwrap(); + Self { obj, data } + } +} + +type Req<'a, T> = ::Request<'a>; +type Ev = ::Event; + +// TODO: tests to add +// - destroy window before surface +// - destroy surface before window +// - destroy popup and reassociate with new surface +// - reconfigure window (popup) before mapping +// - associate window after surface is already created + +// Matches Xwayland flow. +#[test] +fn toplevel_flow() { + let (mut f, compositor) = TestFixture::new_with_compositor(); + + let window = unsafe { Window::new(1) }; + let (surface, testwl_id) = f.create_toplevel(&compositor, window); + { + let surface_data = f.testwl.get_surface_data(testwl_id).unwrap(); + assert!(surface_data.buffer.is_some()); + } + + f.testwl.close_toplevel(testwl_id); + f.run(); + + assert!(!f.exwayland.connection.as_ref().unwrap().windows[&window].mapped); + + assert!( + f.testwl.get_surface_data(testwl_id).is_some(), + "Surface should still exist for closed toplevel" + ); + assert!(surface.obj.is_alive()); + + // For some reason, we can get two UnmapNotify events + // https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.4 + f.exwayland.unmap_window(window); + f.exwayland.unmap_window(window); + f.exwayland.destroy_window(window); + surface.obj.destroy(); + f.run(); + + assert!(f.testwl.get_surface_data(testwl_id).is_none()); +} + +#[test] +fn popup_flow_simple() { + let (mut f, compositor) = TestFixture::new_with_compositor(); + + let win_toplevel = unsafe { Window::new(1) }; + let (_, toplevel_id) = f.create_toplevel(&compositor, win_toplevel); + + let win_popup = unsafe { Window::new(2) }; + let (popup_surface, popup_id) = f.create_popup(&compositor, win_popup, toplevel_id); + + f.exwayland.unmap_window(win_popup); + f.exwayland.destroy_window(win_popup); + popup_surface.obj.destroy(); + f.run(); + + assert!(f.testwl.get_surface_data(popup_id).is_none()); +} + +#[test] +fn pass_through_globals() { + use wayland_client::protocol::wl_output::WlOutput; + + let mut f = TestFixture::new(); + + const fn check() {} + + macro_rules! globals_struct { + ($($field:ident),+) => { + $( check::<$field>(); )+ + #[derive(Default)] + #[allow(non_snake_case)] + struct SupportedGlobals { + $( $field: bool ),+ + } + + impl SupportedGlobals { + fn check_globals(&self) { + $( assert!(self.$field, "Missing global {}", stringify!($field)); )+ + } + + fn global_found(&mut self, interface: String) { + match interface { + $( + x if x == $field::interface().name => { + self.$field = true; + } + )+ + _ => panic!("Found an unhandled global: {interface}") + } + } + } + } + } + + // New globals need to be added here and in testwl. + globals_struct! { + WlCompositor, + WlShm, + WlOutput, + WlSeat, + ZwpLinuxDmabufV1, + ZwpRelativePointerManagerV1, + ZxdgOutputManagerV1, + WpViewporter, + WlDrm + } + + let mut globals = SupportedGlobals::default(); + let display = f.exwl_connection.display(); + let registry = + TestObject::::from_request(&display, Req::::GetRegistry {}); + f.run(); + let events = std::mem::take(&mut *registry.data.events.lock().unwrap()); + assert!(events.len() > 0); + for event in events { + let Ev::::Global { interface, .. } = event else { + unreachable!(); + }; + + globals.global_found(interface); + } + + globals.check_globals(); +} + +#[test] +fn last_activated_toplevel_is_focused() { + let (mut f, comp) = TestFixture::new_with_compositor(); + let win1 = unsafe { Window::new(1) }; + + let (_surface1, id1) = f.create_toplevel(&comp, win1); + assert_eq!( + f.connection().focused_window, + Some(win1), + "new toplevel's window is not focused" + ); + + let win2 = unsafe { Window::new(2) }; + let _data2 = f.create_toplevel(&comp, win2); + assert_eq!( + f.connection().focused_window, + Some(win2), + "toplevel focus did not switch" + ); + + f.testwl.configure_toplevel(id1, 100, 100, vec![]); + f.run(); + assert_eq!( + f.connection().focused_window, + Some(win2), + "toplevel focus did not stay the same" + ); +} + +#[test] +fn popup_window_changes_surface() { + let (mut f, comp) = TestFixture::new_with_compositor(); + let t_win = unsafe { Window::new(1) }; + let (_, toplevel_id) = f.create_toplevel(&comp, t_win); + + let win = unsafe { Window::new(2) }; + let (surface, old_id) = f.create_popup(&comp, win, toplevel_id); + + f.exwayland.unmap_window(win); + surface.obj.destroy(); + f.run(); + + assert!(f.testwl.get_surface_data(old_id).is_none()); + + let (_, surface) = comp.create_surface(); + f.run(); + let id = f + .testwl + .last_created_surface_id() + .expect("No surface created"); + + assert_ne!(old_id, id); + assert!(f.testwl.get_surface_data(id).is_some()); + + f.exwayland.map_window(win); + f.exwayland + .associate_window(win, surface.id().protocol_id()); + f.run(); + + let data = f.testwl.get_surface_data(id).unwrap(); + assert!(data.popup().popup.is_alive()); + + f.testwl.configure_popup(id); + f.run(); + + let data = f.testwl.get_surface_data(id).unwrap(); + assert!(data.popup().popup.is_alive()); +} + +#[test] +fn override_redirect_window_after_toplevel_close() { + let (mut f, comp) = TestFixture::new_with_compositor(); + let win1 = unsafe { Window::new(1) }; + let (obj, first) = f.create_toplevel(&comp, win1); + f.testwl.close_toplevel(first); + f.run(); + + f.exwayland.unmap_window(win1); + f.exwayland.destroy_window(win1); + obj.obj.destroy(); + f.run(); + + assert!(f.testwl.get_surface_data(first).is_none()); + + let win2 = unsafe { Window::new(2) }; + let (_, second) = f.create_and_map_window(&comp, win2, true); + let data = f.testwl.get_surface_data(second).unwrap(); + assert!( + matches!(data.role, Some(testwl::SurfaceRole::Toplevel(_))), + "wrong role: {:?}", + data.role + ) +} + +#[test] +fn fullscreen() { + let (mut f, comp) = TestFixture::new_with_compositor(); + let win = unsafe { Window::new(1) }; + let (_, id) = f.create_toplevel(&comp, win); + + f.exwayland.set_fullscreen(win, SetState::Add); + f.run(); + f.run(); + + let data = f.testwl.get_surface_data(id).unwrap(); + assert!(data + .toplevel() + .states + .contains(&xdg_toplevel::State::Fullscreen)); + + f.exwayland.set_fullscreen(win, SetState::Remove); + f.run(); + f.run(); + + let data = f.testwl.get_surface_data(id).unwrap(); + assert!(!data + .toplevel() + .states + .contains(&xdg_toplevel::State::Fullscreen)); + + f.exwayland.set_fullscreen(win, SetState::Toggle); + f.run(); + f.run(); + + let data = f.testwl.get_surface_data(id).unwrap(); + assert!(data + .toplevel() + .states + .contains(&xdg_toplevel::State::Fullscreen)); + + f.exwayland.set_fullscreen(win, SetState::Toggle); + f.run(); + f.run(); + + let data = f.testwl.get_surface_data(id).unwrap(); + assert!(!data + .toplevel() + .states + .contains(&xdg_toplevel::State::Fullscreen)); +} + +/// See Pointer::handle_event for an explanation. +#[test] +fn popup_pointer_motion_workaround() {} diff --git a/satellite/src/xstate.rs b/satellite/src/xstate.rs new file mode 100644 index 0000000..86003e6 --- /dev/null +++ b/satellite/src/xstate.rs @@ -0,0 +1,535 @@ +use bitflags::bitflags; +use log::{debug, trace, warn}; +use std::os::fd::{AsRawFd, BorrowedFd}; +use std::sync::Arc; +use xcb::{x, Xid, XidNew}; +use xcb_util_cursor::{Cursor, CursorContext}; + +pub struct XState { + pub connection: Arc, + root: x::Window, + pub atoms: Atoms, + window_types: WindowTypes, +} + +impl XState { + pub fn new(fd: BorrowedFd) -> Self { + let connection = Arc::new(xcb::Connection::connect_to_fd(fd.as_raw_fd(), None).unwrap()); + let setup = connection.get_setup(); + let screen = setup.roots().next().unwrap(); + let root = screen.root(); + + connection + .send_and_check_request(&x::ChangeWindowAttributes { + window: root, + value_list: &[x::Cw::EventMask( + x::EventMask::SUBSTRUCTURE_REDIRECT // To have Xwayland send us WL_SURFACE_ID + | x::EventMask::SUBSTRUCTURE_NOTIFY // To get notified whenever new windows are created + | x::EventMask::RESIZE_REDIRECT, + )], + }) + .unwrap(); + + let atoms = Atoms::intern_all(&connection).unwrap(); + trace!("atoms: {atoms:#?}"); + let window_types = WindowTypes::new(&connection); + + // This makes Xwayland spit out damage tracking + connection + .send_and_check_request(&xcb::composite::RedirectSubwindows { + window: screen.root(), + update: xcb::composite::Redirect::Manual, + }) + .unwrap(); + + // Setup default cursor theme + let ctx = CursorContext::new(&connection, screen).unwrap(); + let left_ptr = ctx.load_cursor(Cursor::LeftPtr); + connection + .send_and_check_request(&x::ChangeWindowAttributes { + window: root, + value_list: &[x::Cw::Cursor(left_ptr)], + }) + .unwrap(); + + let mut r = Self { + connection, + root, + atoms, + window_types, + }; + r.create_ewmh_window(); + r + } + + fn set_root_property(&self, property: x::Atom, r#type: x::Atom, data: &[P]) { + self.connection + .send_and_check_request(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: self.root, + property, + r#type, + data, + }) + .unwrap(); + } + + fn create_ewmh_window(&mut self) { + let window = self.connection.generate_id(); + self.connection + .send_and_check_request(&x::CreateWindow { + depth: 0, + wid: window, + parent: self.root, + x: 0, + y: 0, + width: 1, + height: 1, + border_width: 0, + class: x::WindowClass::InputOnly, + visual: x::COPY_FROM_PARENT, + value_list: &[], + }) + .unwrap(); + + self.set_root_property(self.atoms.wm_check, x::ATOM_WINDOW, &[window]); + self.set_root_property(self.atoms.active_win, x::ATOM_WINDOW, &[x::Window::none()]); + self.set_root_property( + self.atoms.supported, + x::ATOM_ATOM, + &[self.atoms.active_win, self.atoms.client_list], + ); + + self.connection + .send_and_check_request(&x::ChangeProperty { + mode: x::PropMode::Replace, + window, + property: self.atoms.wm_check, + r#type: x::ATOM_WINDOW, + data: &[window], + }) + .unwrap(); + + self.connection + .send_and_check_request(&x::ChangeProperty { + mode: x::PropMode::Replace, + window, + property: self.atoms.wm_name, + r#type: x::ATOM_STRING, + data: b"exwayland wm", + }) + .unwrap(); + } + + pub fn handle_events(&mut self, server_state: &mut super::RealServerState) { + while let Some(event) = self.connection.poll_for_event().unwrap() { + trace!("x11 event: {event:?}"); + match event { + xcb::Event::X(x::Event::CreateNotify(e)) => { + debug!("new window: {:?}", e); + match self + .connection + .send_and_check_request(&x::ChangeWindowAttributes { + window: e.window(), + value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)], + }) { + // This can sometimes fail if the window was created and then immediately + // destroyed. + Ok(()) | Err(xcb::ProtocolError::X(x::Error::Window(_), _)) => {} + Err(other) => { + panic!("error subscribing to property change on new window: {other:?}") + } + } + + let parent = e.parent(); + let parent = if parent.is_none() || parent == self.root { + None + } else { + Some(parent) + }; + server_state.new_window(e.window(), e.override_redirect(), (&e).into(), parent); + } + xcb::Event::X(x::Event::MapRequest(e)) => { + debug!("requested to map {:?}", e.window()); + self.connection + .send_and_check_request(&x::MapWindow { window: e.window() }) + .unwrap(); + } + xcb::Event::X(x::Event::MapNotify(e)) => { + server_state.map_window(e.window()); + } + xcb::Event::X(x::Event::ConfigureNotify(e)) => { + server_state.reconfigure_window(e); + } + xcb::Event::X(x::Event::UnmapNotify(e)) => { + trace!("unmap event: {:?}", e.event()); + server_state.unmap_window(e.window()); + } + xcb::Event::X(x::Event::DestroyNotify(e)) => { + debug!("destroying window {:?}", e.window()); + server_state.destroy_window(e.window()); + } + xcb::Event::X(x::Event::PropertyNotify(e)) => { + self.handle_property_change(e, server_state); + } + xcb::Event::X(x::Event::ConfigureRequest(e)) => { + debug!("{:?} request: {:?}", e.window(), e.value_mask()); + let mut list = Vec::new(); + let mask = e.value_mask(); + + if mask.contains(x::ConfigWindowMask::X) { + list.push(x::ConfigWindow::X(e.x().into())); + } + if mask.contains(x::ConfigWindowMask::Y) { + list.push(x::ConfigWindow::Y(e.y().into())); + } + if mask.contains(x::ConfigWindowMask::WIDTH) { + list.push(x::ConfigWindow::Width(e.width().into())); + } + if mask.contains(x::ConfigWindowMask::HEIGHT) { + list.push(x::ConfigWindow::Height(e.height().into())); + } + + self.connection + .send_and_check_request(&x::ConfigureWindow { + window: e.window(), + value_list: &list, + }) + .unwrap(); + } + xcb::Event::X(x::Event::ClientMessage(e)) => match e.r#type() { + x if x == self.atoms.wl_surface_id => { + let x::ClientMessageData::Data32(data) = e.data() else { + unreachable!(); + }; + let id: u32 = (data[0] as u64 | ((data[1] as u64) << 32)) as u32; + server_state.associate_window(e.window(), id); + } + x if x == self.atoms.net_wm_state => { + let x::ClientMessageData::Data32(data) = e.data() else { + unreachable!(); + }; + let Ok(action) = SetState::try_from(data[0]) else { + warn!("unknown action for _NET_WM_STATE: {}", data[0]); + continue; + }; + let prop1 = unsafe { x::Atom::new(data[1]) }; + let prop2 = unsafe { x::Atom::new(data[2]) }; + + trace!("_NET_WM_STATE ({action:?}) props: {prop1:?} {prop2:?}"); + + for prop in [prop1, prop2] { + match prop { + x if x == self.atoms.wm_fullscreen => { + server_state.set_fullscreen(e.window(), action); + } + _ => {} + } + } + } + t => warn!("unrecognized message: {t:?}"), + }, + xcb::Event::X(x::Event::MappingNotify(_)) => {} + other => { + warn!("unhandled event: {other:?}"); + } + } + + server_state.run(); + } + } + + fn handle_property_change( + &self, + event: x::PropertyNotifyEvent, + server_state: &mut super::RealServerState, + ) { + let get_prop = |r#type, long_length| { + self.connection + .wait_for_reply(self.connection.send_request(&x::GetProperty { + window: event.window(), + property: event.atom(), + r#type, + long_offset: 0, + long_length, + delete: false, + })) + }; + if event.state() != x::Property::NewValue { + return; + } + + match event.atom() { + x if x == self.atoms.wm_window_type => { + let Ok(prop) = get_prop(x::ATOM_ATOM, 8) else { + return; + }; + let types: &[x::Atom] = prop.value(); + let win_type = types.iter().find_map(|a| self.window_types.get_type(*a)); + debug!( + "set {:?} type to {} ({})", + event.window(), + win_type.unwrap_or("[Unknown/Unrecognized]".to_string()), + types.len() + ); + } + x if x == x::ATOM_WM_NORMAL_HINTS => { + let Ok(prop) = get_prop(x::ATOM_WM_SIZE_HINTS, 9) else { + return; + }; + let data: &[u32] = prop.value(); + let hints = WmNormalHints::from(data); + server_state.set_win_hints(event.window(), hints); + } + _ => { + let prop = self + .connection + .wait_for_reply( + self.connection + .send_request(&x::GetAtomName { atom: event.atom() }), + ) + .unwrap(); + + debug!( + "changed property {:?} for {:?}", + prop.name(), + event.window() + ); + } + } + } +} + +xcb::atoms_struct! { + #[derive(Clone, Debug)] + pub struct Atoms { + pub wl_surface_id => b"WL_SURFACE_ID" only_if_exists = false, + pub wm_protocols => b"WM_PROTOCOLS" only_if_exists = false, + pub wm_delete_window => b"WM_DELETE_WINDOW" only_if_exists = false, + pub wm_transient_for => b"WM_TRANSIENT_FOR" only_if_exists = false, + pub wm_hints => b"WM_HINTS" only_if_exists = false, + pub wm_check => b"_NET_SUPPORTING_WM_CHECK" only_if_exists = false, + pub wm_name => b"_NET_WM_NAME" only_if_exists = false, + pub wm_window_type => b"_NET_WM_WINDOW_TYPE" only_if_exists = false, + pub wm_pid => b"_NET_WM_PID" only_if_exists = false, + pub net_wm_state => b"_NET_WM_STATE" only_if_exists = false, + pub wm_fullscreen => b"_NET_WM_STATE_FULLSCREEN" only_if_exists = false, + pub active_win => b"_NET_ACTIVE_WINDOW" only_if_exists = false, + pub client_list => b"_NET_CLIENT_LIST" only_if_exists = false, + pub supported => b"_NET_SUPPORTED" only_if_exists = false, + } +} + +xcb::atoms_struct! { + pub struct WindowTypes { + pub normal => b"_NET_WM_WINDOW_TYPE_NORMAL" only_if_exists = false, + pub dialog => b"_NET_WM_WINDOW_TYPE_DIALOG" only_if_exists = false, + pub splash => b"_NET_WM_WINDOW_TYPE_SPLASH" only_if_exists = false, + pub menu => b"_NET_WM_WINDOW_TYPE_MENU" only_if_exists = false, + pub utility => b"_NET_WM_WINDOW_TYPE_UTILITY" only_if_exists = false, + } +} + +impl WindowTypes { + pub fn new(connection: &xcb::Connection) -> Self { + let r = Self::intern_all(connection).unwrap(); + assert_ne!(r.normal, x::ATOM_NONE); + assert_ne!(r.dialog, x::ATOM_NONE); + assert_ne!(r.utility, x::ATOM_NONE); + r + } + pub fn get_type(&self, atom: x::Atom) -> Option { + match atom { + x if x == self.normal => Some("Normal".to_string()), + x if x == self.dialog => Some("Dialog".to_string()), + x if x == self.utility => Some("Utility".to_string()), + x if x == self.menu => Some("Menu".to_string()), + _ => None, + } + } +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct WindowDims { + pub x: i16, + pub y: i16, + pub width: u16, + pub height: u16, +} + +bitflags! { + /// From ICCCM spec. + /// https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.2.3 + pub struct WmSizeHintsFlags: u32 { + const UserPosition = 1; + const UserSize = 2; + const ProgramPosition = 4; + const ProgramSize = 8; + const ProgramMinSize = 16; + const ProgramMaxSize = 32; + const ProgramResizeIncrement = 64; + const ProgramAspect = 128; + const ProgramBaseSize = 256; + const ProgramWinGravity = 512; + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct WinSize { + pub width: i32, + pub height: i32, +} + +#[derive(Default, Debug, PartialEq, Eq)] +pub struct WmNormalHints { + pub min_size: Option, + pub max_size: Option, +} + +#[derive(Debug, Clone, Copy)] +pub enum SetState { + Remove, + Add, + Toggle, +} + +impl TryFrom for SetState { + type Error = (); + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Self::Remove), + 1 => Ok(Self::Add), + 2 => Ok(Self::Toggle), + _ => Err(()), + } + } +} + +impl From<&[u32]> for WmNormalHints { + fn from(value: &[u32]) -> Self { + let mut ret = Self::default(); + let flags = WmSizeHintsFlags::from_bits(value[0]).unwrap(); + + if flags.contains(WmSizeHintsFlags::ProgramMinSize) { + ret.min_size = Some(WinSize { + width: value[5] as _, + height: value[6] as _, + }); + } + + if flags.contains(WmSizeHintsFlags::ProgramMaxSize) { + ret.max_size = Some(WinSize { + width: value[7] as _, + height: value[8] as _, + }); + } + + ret + } +} + +impl super::XConnection for Arc { + type ExtraData = Atoms; + + fn root_window(&self) -> x::Window { + self.get_setup().roots().next().unwrap().root() + } + + fn set_window_dims(&mut self, window: x::Window, dims: crate::server::PendingSurfaceState) { + trace!("reconfiguring window {window:?}"); + self.send_and_check_request(&x::ConfigureWindow { + window, + value_list: &[ + x::ConfigWindow::X(dims.x), + x::ConfigWindow::Y(dims.y), + x::ConfigWindow::Width(dims.width as _), + x::ConfigWindow::Height(dims.height as _), + ], + }) + .unwrap(); + } + + fn set_fullscreen(&mut self, window: x::Window, fullscreen: bool, atoms: Self::ExtraData) { + let data = if fullscreen { + std::slice::from_ref(&atoms.wm_fullscreen) + } else { + &[] + }; + self.send_and_check_request(&x::ChangeProperty:: { + mode: x::PropMode::Replace, + window, + property: atoms.net_wm_state, + r#type: x::ATOM_ATOM, + data, + }) + .unwrap(); + } + + fn focus_window(&mut self, window: x::Window, atoms: Self::ExtraData) { + let prop = self + .wait_for_reply(self.send_request(&x::GetProperty { + delete: false, + window, + property: atoms.wm_hints, + r#type: atoms.wm_hints, + long_offset: 0, + long_length: 8, + })) + .unwrap(); + + let fields: &[u32] = prop.value(); + let mut input = false; + if !fields.is_empty() { + let flags = fields[0]; + if (flags & 0x1) > 0 { + input = fields[1] > 0; + } + } + + if input { + // might fail if window is not visible but who cares + let _ = self.send_and_check_request(&x::SetInputFocus { + focus: window, + revert_to: x::InputFocus::None, + time: x::CURRENT_TIME, + }); + } + + self.send_and_check_request(&x::ConfigureWindow { + window, + value_list: &[x::ConfigWindow::StackMode(x::StackMode::Above)], + }) + .unwrap(); + self.send_and_check_request(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: self.root_window(), + property: atoms.active_win, + r#type: x::ATOM_WINDOW, + data: &[window], + }) + .unwrap(); + } + + fn close_window(&mut self, window: x::Window, atoms: Self::ExtraData) { + let data = [atoms.wm_delete_window.resource_id(), 0, 0, 0, 0]; + let event = &x::ClientMessageEvent::new( + window, + atoms.wm_protocols, + x::ClientMessageData::Data32(data), + ); + + self.send_and_check_request(&x::SendEvent { + destination: x::SendEventDest::Window(window), + propagate: false, + event_mask: x::EventMask::empty(), + event, + }) + .unwrap(); + } +} + +impl super::FromServerState> for Atoms { + fn create(state: &super::RealServerState) -> Self { + state.atoms.as_ref().unwrap().clone() + } +} diff --git a/satellite/tests/integration.rs b/satellite/tests/integration.rs new file mode 100644 index 0000000..aa30407 --- /dev/null +++ b/satellite/tests/integration.rs @@ -0,0 +1,269 @@ +use rustix::event::{poll, PollFd, PollFlags}; +use std::mem::ManuallyDrop; +use std::os::fd::{AsRawFd, BorrowedFd}; +use std::os::unix::net::UnixStream; +use std::sync::mpsc; +use std::sync::Once; +use std::thread::JoinHandle; +use std::time::Duration; +use wayland_protocols::xdg::shell::server::xdg_toplevel; +use wayland_server::Resource; +use xcb::{x, Xid}; +use xwayland_satellite as xwls; + +struct Fixture { + testwl: testwl::Server, + thread: ManuallyDrop>>, + pollfd: PollFd<'static>, +} + +impl Drop for Fixture { + fn drop(&mut self) { + let thread = unsafe { ManuallyDrop::take(&mut self.thread) }; + if thread.is_finished() { + thread.join().expect("Main thread panicked"); + } + } +} + +xcb::atoms_struct! { + struct Atoms { + wm_protocols => b"WM_PROTOCOLS", + wm_delete_window => b"WM_DELETE_WINDOW", + } +} + +static INIT: Once = Once::new(); +impl Fixture { + #[track_caller] + fn new() -> Self { + INIT.call_once(|| { + env_logger::builder() + .is_test(true) + .filter_level(log::LevelFilter::Debug) + .init(); + }); + + let (a, b) = UnixStream::pair().unwrap(); + let mut testwl = testwl::Server::new(false); + testwl.connect(a); + + let (send, recv) = mpsc::channel(); + let thread = std::thread::spawn(move || xwls::main(Some(b), Some(send))); + + // wait for connection + let fd = unsafe { BorrowedFd::borrow_raw(testwl.poll_fd().as_raw_fd()) }; + let pollfd = PollFd::from_borrowed_fd(fd, PollFlags::IN); + assert!(poll(&mut [pollfd.clone()], 100).unwrap() > 0); + testwl.dispatch(); + + let wait = Duration::from_secs(1); + assert_eq!( + recv.recv_timeout(wait), + Ok(xwls::StateEvent::CreatedServer), + "creating server" + ); + assert_eq!( + recv.recv_timeout(wait), + Ok(xwls::StateEvent::ConnectedServer), + "connecting to server" + ); + + let mut f = [pollfd.clone()]; + let start = std::time::Instant::now(); + // Give Xwayland time to do its thing + while start.elapsed() < Duration::from_millis(500) { + let n = poll(&mut f, 100).unwrap(); + if n > 0 { + testwl.dispatch(); + } + } + + assert_eq!( + recv.try_recv(), + Ok(xwls::StateEvent::XwaylandReady), + "connecting to xwayland" + ); + + Self { + testwl, + thread: ManuallyDrop::new(thread), + pollfd, + } + } + + #[track_caller] + fn wait_and_dispatch(&mut self) { + let mut pollfd = [self.pollfd.clone()]; + assert!( + poll(&mut pollfd, 50).unwrap() > 0, + "Did not receive any events" + ); + self.pollfd.clear_revents(); + self.testwl.dispatch(); + + while poll(&mut pollfd, 50).unwrap() > 0 { + self.testwl.dispatch(); + self.pollfd.clear_revents(); + } + } + + fn create_window( + &mut self, + connection: &xcb::Connection, + override_redirect: bool, + x: i16, + y: i16, + width: u16, + height: u16, + ) -> (x::Window, testwl::SurfaceId) { + let screen = connection.get_setup().roots().next().unwrap(); + let wid = connection.generate_id(); + let req = x::CreateWindow { + depth: x::COPY_FROM_PARENT as _, + wid, + parent: screen.root(), + x, + y, + width, + height, + border_width: 0, + class: x::WindowClass::InputOutput, + visual: screen.root_visual(), + value_list: &[ + x::Cw::BackPixel(screen.white_pixel()), + x::Cw::OverrideRedirect(override_redirect), + ], + }; + connection.send_and_check_request(&req).unwrap(); + + let req = x::MapWindow { window: wid }; + connection.send_and_check_request(&req).unwrap(); + self.wait_and_dispatch(); + + let id = self + .testwl + .last_created_surface_id() + .expect("No surface created for window"); + + (wid, id) + } + + fn create_toplevel( + &mut self, + connection: &xcb::Connection, + width: u16, + height: u16, + ) -> (x::Window, testwl::SurfaceId) { + let (window, surface) = self.create_window(connection, false, 0, 0, width, height); + let data = self + .testwl + .get_surface_data(surface) + .expect("No surface data"); + assert!( + matches!(data.role, Some(testwl::SurfaceRole::Toplevel(_))), + "surface role was wrong: {:?}", + data.role + ); + + self.testwl + .configure_toplevel(surface, 100, 100, vec![xdg_toplevel::State::Activated]); + self.wait_and_dispatch(); + let geometry = connection + .wait_for_reply(connection.send_request(&x::GetGeometry { + drawable: x::Drawable::Window(window), + })) + .unwrap(); + + assert_eq!(geometry.x(), 0); + assert_eq!(geometry.y(), 0); + assert_eq!(geometry.width(), 100); + assert_eq!(geometry.height(), 100); + + (window, surface) + } + + /// Triggers a Wayland side toplevel Close event and processes the corresponding + /// X11 side WM_DELETE_WINDOW client message + fn close_toplevel( + &mut self, + connection: &mut Connection, + window: x::Window, + surface: testwl::SurfaceId, + ) { + self.testwl.close_toplevel(surface); + connection.await_event(); + let event = connection + .inner + .poll_for_event() + .unwrap() + .expect("No close event"); + + let xcb::Event::X(x::Event::ClientMessage(event)) = event else { + panic!("Expected ClientMessage event, got {event:?}"); + }; + + assert_eq!(event.window(), window); + assert_eq!(event.format(), 32); + assert_eq!(event.r#type(), connection.atoms.wm_protocols); + match event.data() { + x::ClientMessageData::Data32(d) => { + assert_eq!(d[0], connection.atoms.wm_delete_window.resource_id()) + } + other => panic!("wrong data type: {other:?}"), + } + } +} + +struct Connection { + inner: xcb::Connection, + pollfd: PollFd<'static>, + atoms: Atoms, +} + +impl std::ops::Deref for Connection { + type Target = xcb::Connection; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl Connection { + fn new() -> Self { + // TODO: this will not work if there is an Xserver at 1024, or whenever we add multiple + // tests. + let (inner, _) = xcb::Connection::connect(Some(":0")).unwrap(); + let fd = unsafe { BorrowedFd::borrow_raw(inner.as_raw_fd()) }; + let pollfd = PollFd::from_borrowed_fd(fd, PollFlags::IN); + let atoms = Atoms::intern_all(&inner).unwrap(); + + Self { + inner, + pollfd, + atoms, + } + } + + #[track_caller] + fn await_event(&mut self) { + assert!( + poll(&mut [self.pollfd.clone()], 100).expect("poll failed") > 0, + "Did not get any X11 events" + ); + } +} + +#[test] +fn toplevel_flow() { + let mut f = Fixture::new(); + let mut connection = Connection::new(); + let (window, surface) = f.create_toplevel(&connection.inner, 200, 200); + f.close_toplevel(&mut connection, window, surface); + + // Simulate killing client + drop(connection); + f.wait_and_dispatch(); + + let data = f.testwl.get_surface_data(surface).expect("No surface data"); + assert!(!data.toplevel().toplevel.is_alive()); +} diff --git a/testwl/Cargo.lock b/testwl/Cargo.lock new file mode 100644 index 0000000..2d8a749 --- /dev/null +++ b/testwl/Cargo.lock @@ -0,0 +1,273 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustix" +version = "0.38.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "testwl" +version = "0.1.0" +dependencies = [ + "wayland-server", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "wayland-backend" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-server" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e6e4d5c285bc24ba4ed2d5a4bd4febd5fd904451f465973225c8e99772fdb7" +dependencies = [ + "bitflags", + "downcast-rs", + "io-lifetimes", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-sys" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" diff --git a/testwl/Cargo.toml b/testwl/Cargo.toml new file mode 100644 index 0000000..bd97ce6 --- /dev/null +++ b/testwl/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "testwl" +version = "0.1.0" +edition = "2021" + +[dependencies] +wayland-protocols = { version = "0.31.2", features = ["server", "unstable"] } +wayland-server = "0.31.1" +wl_drm = { path = "../wl_drm" } diff --git a/testwl/src/lib.rs b/testwl/src/lib.rs new file mode 100644 index 0000000..bc5da9a --- /dev/null +++ b/testwl/src/lib.rs @@ -0,0 +1,839 @@ +use std::collections::{hash_map, HashMap, HashSet}; +use std::os::fd::BorrowedFd; +use std::os::unix::net::UnixStream; +use std::time::Instant; +use wayland_protocols::{ + wp::{ + linux_dmabuf::zv1::server::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, + relative_pointer::zv1::server::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, + viewporter::server::wp_viewporter::WpViewporter, + }, + xdg::{ + shell::server::{ + xdg_popup::{self, XdgPopup}, + xdg_positioner::{self, XdgPositioner}, + xdg_surface::XdgSurface, + xdg_toplevel::{self, XdgToplevel}, + xdg_wm_base::{self, XdgWmBase}, + }, + xdg_output::zv1::server::zxdg_output_manager_v1::ZxdgOutputManagerV1, + }, +}; +use wayland_server::{ + backend::protocol::ProtocolError, + protocol::{ + self as proto, + wl_buffer::WlBuffer, + wl_callback::WlCallback, + wl_compositor::WlCompositor, + wl_output::WlOutput, + wl_pointer::{self, WlPointer}, + wl_seat::{self, WlSeat}, + wl_shm::WlShm, + wl_shm_pool::WlShmPool, + wl_surface::WlSurface, + }, + Client, Dispatch, Display, DisplayHandle, GlobalDispatch, Resource, +}; +use wl_drm::server::wl_drm::WlDrm; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct BufferDamage { + pub x: i32, + pub y: i32, + pub width: i32, + pub height: i32, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct SurfaceData { + pub surface: WlSurface, + pub buffer: Option, + pub last_damage: Option, + pub role: Option, +} + +impl SurfaceData { + pub fn xdg(&self) -> &XdgSurfaceData { + match self.role.as_ref().expect("Surface missing role") { + SurfaceRole::Toplevel(ref t) => &t.xdg, + SurfaceRole::Popup(ref p) => &p.xdg, + SurfaceRole::Cursor => panic!("cursor surface doesn't have an XdgSurface"), + } + } + + pub fn toplevel(&self) -> &Toplevel { + match self.role.as_ref().expect("Surface missing role") { + SurfaceRole::Toplevel(ref t) => t, + other => panic!("Surface role was not toplevel: {other:?}"), + } + } + pub fn popup(&self) -> &Popup { + match self.role.as_ref().expect("Surface missing role") { + SurfaceRole::Popup(ref p) => p, + other => panic!("Surface role was not popup: {other:?}"), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum SurfaceRole { + Toplevel(Toplevel), + Popup(Popup), + Cursor, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Toplevel { + pub xdg: XdgSurfaceData, + pub toplevel: XdgToplevel, + pub min_size: Option, + pub max_size: Option, + pub states: Vec, + pub closed: bool, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Popup { + pub xdg: XdgSurfaceData, + pub parent: XdgSurface, + pub popup: XdgPopup, + pub positioner_state: PositionerState, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] +pub struct Vec2 { + pub x: i32, + pub y: i32, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct XdgSurfaceData { + pub surface: XdgSurface, + pub last_configure_serial: u32, +} + +impl XdgSurfaceData { + fn new(surface: XdgSurface) -> Self { + Self { + surface, + last_configure_serial: 0, + } + } + + fn configure(&mut self, serial: u32) { + self.surface.configure(serial); + self.last_configure_serial = serial; + } +} + +#[derive(Debug, Hash, Clone, Copy, Eq, PartialEq)] +pub struct SurfaceId(u32); + +#[derive(Hash, Clone, Copy, Eq, PartialEq)] +struct PositionerId(u32); + +struct State { + surfaces: HashMap, + positioners: HashMap, + buffers: HashSet, + begin: Instant, + last_surface_id: Option, + callbacks: Vec, + pointer: Option, + configure_serial: u32, +} + +impl State { + #[track_caller] + fn configure_toplevel( + &mut self, + surface_id: SurfaceId, + width: i32, + height: i32, + states: Vec, + ) { + let last_serial = self.configure_serial; + let toplevel = self.get_toplevel(surface_id); + toplevel.states = states.clone(); + let states = states + .into_iter() + .map(|state| u32::from(state) as u8) + .collect(); + toplevel.toplevel.configure(width, height, states); + toplevel.xdg.configure(last_serial); + self.configure_serial += 1; + } + + #[track_caller] + fn get_toplevel(&mut self, surface_id: SurfaceId) -> &mut Toplevel { + let surface = self.surfaces.get_mut(&surface_id).unwrap(); + match &mut surface.role { + Some(SurfaceRole::Toplevel(t)) => t, + other => panic!("Surface does not have toplevel role: {:?}", other), + } + } +} + +impl Default for State { + fn default() -> Self { + Self { + surfaces: Default::default(), + buffers: Default::default(), + positioners: Default::default(), + begin: Instant::now(), + last_surface_id: None, + callbacks: Vec::new(), + pointer: None, + configure_serial: 0, + } + } +} + +macro_rules! simple_global_dispatch { + ($type:ty) => { + impl GlobalDispatch<$type, ()> for State { + fn bind( + _: &mut Self, + _: &DisplayHandle, + _: &wayland_server::Client, + resource: wayland_server::New<$type>, + _: &(), + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + data_init.init(resource, ()); + } + } + }; +} + +pub struct Server { + display: Display, + dh: DisplayHandle, + state: State, + client: Option, + configure_serial: u32, +} + +impl Server { + pub fn new(noops: bool) -> Self { + let display = Display::new().unwrap(); + let dh = display.handle(); + + macro_rules! global_noop { + ($type:ty) => { + if noops { + dh.create_global::(1, ()); + } + simple_global_dispatch!($type); + impl Dispatch<$type, ()> for State { + fn request( + _: &mut Self, + _: &Client, + _: &$type, + _: <$type as Resource>::Request, + _: &(), + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + todo!("Dispatch for {} is no-op", stringify!($type)); + } + } + }; + } + dh.create_global::(6, ()); + dh.create_global::(1, ()); + dh.create_global::(6, ()); + dh.create_global::(5, ()); + global_noop!(WlOutput); + global_noop!(ZwpLinuxDmabufV1); + global_noop!(ZwpRelativePointerManagerV1); + global_noop!(ZxdgOutputManagerV1); + global_noop!(WpViewporter); + global_noop!(WlDrm); + + Self { + display, + dh, + state: State::default(), + client: None, + configure_serial: 1, + } + } + + pub fn poll_fd(&mut self) -> BorrowedFd<'_> { + self.display.backend().poll_fd() + } + + pub fn connect(&mut self, stream: UnixStream) { + let client = self + .dh + .insert_client(stream, std::sync::Arc::new(())) + .unwrap(); + assert!( + self.client.replace(client).is_none(), + "Client already connected to test server" + ); + //self.dispatch(); + } + + pub fn dispatch(&mut self) { + self.display.dispatch_clients(&mut self.state).unwrap(); + for callback in std::mem::take(&mut self.state.callbacks) { + callback.done(self.state.begin.elapsed().as_millis().try_into().unwrap()); + } + self.display.flush_clients().unwrap(); + } + + pub fn get_surface_data(&self, surface_id: SurfaceId) -> Option<&SurfaceData> { + self.state.surfaces.get(&surface_id) + } + + pub fn last_created_surface_id(&self) -> Option { + self.state.last_surface_id + } + + pub fn get_object( + &self, + id: SurfaceId, + ) -> Result { + let client = self.client.as_ref().unwrap(); + client.object_from_protocol_id::(&self.display.handle(), id.0) + } + + #[track_caller] + pub fn configure_toplevel( + &mut self, + surface_id: SurfaceId, + width: i32, + height: i32, + states: Vec, + ) { + self.state + .configure_toplevel(surface_id, width, height, states); + self.display.flush_clients().unwrap(); + } + + #[track_caller] + pub fn configure_popup(&mut self, surface_id: SurfaceId) { + let surface = self.state.surfaces.get_mut(&surface_id).unwrap(); + let Some(SurfaceRole::Popup(p)) = &mut surface.role else { + panic!("Surface does not have popup role: {:?}", surface.role); + }; + let PositionerState { size, offset, .. } = &p.positioner_state; + let size = size.unwrap(); + p.popup.configure(offset.x, offset.y, size.x, size.y); + p.xdg.configure(self.configure_serial); + self.configure_serial += 1; + self.dispatch(); + } + + #[track_caller] + pub fn close_toplevel(&mut self, surface_id: SurfaceId) { + let toplevel = self.state.get_toplevel(surface_id); + toplevel.toplevel.close(); + self.dispatch(); + } + + #[track_caller] + pub fn pointer(&self) -> &WlPointer { + self.state.pointer.as_ref().unwrap() + } +} + +simple_global_dispatch!(WlShm); +simple_global_dispatch!(WlCompositor); +simple_global_dispatch!(XdgWmBase); + +impl GlobalDispatch for State { + fn bind( + _: &mut Self, + _: &DisplayHandle, + _: &Client, + resource: wayland_server::New, + _: &(), + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + let seat = data_init.init(resource, ()); + seat.capabilities(wl_seat::Capability::Pointer); + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &Client, + _: &WlSeat, + request: ::Request, + _: &(), + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + wl_seat::Request::GetPointer { id } => { + state.pointer = Some(data_init.init(id, ())); + } + wl_seat::Request::Release => {} + other => todo!("unhandled request {other:?}"), + } + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &Client, + _: &WlPointer, + request: ::Request, + _: &(), + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + wl_pointer::Request::SetCursor { surface, .. } => { + if let Some(surface) = surface { + let data = state + .surfaces + .get_mut(&SurfaceId(surface.id().protocol_id())) + .unwrap(); + + assert!( + data.role.replace(SurfaceRole::Cursor).is_none(), + "Surface already had a role!" + ); + } + } + wl_pointer::Request::Release => { + state.pointer.take(); + } + other => todo!("unhandled pointer request: {other:?}"), + } + } +} + +impl Dispatch for State { + fn request( + _: &mut Self, + _: &Client, + _: &XdgPopup, + request: ::Request, + _: &SurfaceId, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + xdg_popup::Request::Destroy => {} + other => todo!("unhandled request {other:?}"), + } + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &XdgToplevel, + request: ::Request, + surface_id: &SurfaceId, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + xdg_toplevel::Request::SetMinSize { width, height } => { + let data = state.surfaces.get_mut(surface_id).unwrap(); + let Some(SurfaceRole::Toplevel(toplevel)) = &mut data.role else { + unreachable!(); + }; + toplevel.min_size = Some(Vec2 { + x: width, + y: height, + }); + } + xdg_toplevel::Request::SetMaxSize { width, height } => { + let data = state.surfaces.get_mut(surface_id).unwrap(); + let Some(SurfaceRole::Toplevel(toplevel)) = &mut data.role else { + unreachable!(); + }; + toplevel.max_size = Some(Vec2 { + x: width, + y: height, + }); + } + xdg_toplevel::Request::SetFullscreen { .. } => { + let data = state.surfaces.get_mut(surface_id).unwrap(); + let Some(SurfaceRole::Toplevel(toplevel)) = &mut data.role else { + unreachable!(); + }; + toplevel.states.push(xdg_toplevel::State::Fullscreen); + let states = toplevel.states.clone(); + state.configure_toplevel(*surface_id, 100, 100, states); + } + xdg_toplevel::Request::UnsetFullscreen { .. } => { + let data = state.surfaces.get_mut(surface_id).unwrap(); + let Some(SurfaceRole::Toplevel(toplevel)) = &mut data.role else { + unreachable!(); + }; + let Some(pos) = toplevel + .states + .iter() + .copied() + .position(|p| p == xdg_toplevel::State::Fullscreen) + else { + return; + }; + toplevel.states.swap_remove(pos); + let states = toplevel.states.clone(); + state.configure_toplevel(*surface_id, 100, 100, states); + } + xdg_toplevel::Request::Destroy => {} + other => todo!("unhandled request {other:?}"), + } + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + client: &wayland_server::Client, + resource: &XdgSurface, + request: ::Request, + surface_id: &SurfaceId, + dh: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + use wayland_protocols::xdg::shell::server::xdg_surface; + + match request { + xdg_surface::Request::GetToplevel { id } => { + let toplevel = data_init.init(id, *surface_id); + let t = Toplevel { + xdg: XdgSurfaceData::new(resource.clone()), + toplevel, + min_size: None, + max_size: None, + states: Vec::new(), + closed: false, + }; + let data = state.surfaces.get_mut(surface_id).unwrap(); + data.role = Some(SurfaceRole::Toplevel(t)); + } + xdg_surface::Request::GetPopup { + id, + parent, + positioner, + } => { + let popup = data_init.init(id, *surface_id); + let p = Popup { + xdg: XdgSurfaceData::new(resource.clone()), + popup, + parent: parent.unwrap(), + positioner_state: state.positioners + [&PositionerId(positioner.id().protocol_id())] + .clone(), + }; + let data = state.surfaces.get_mut(surface_id).unwrap(); + data.role = Some(SurfaceRole::Popup(p)); + } + xdg_surface::Request::AckConfigure { serial } => { + let data = state.surfaces.get_mut(surface_id).unwrap(); + assert_eq!(data.xdg().last_configure_serial, serial); + } + xdg_surface::Request::Destroy => { + let data = state.surfaces.get_mut(surface_id).unwrap(); + let role_alive = data.role.is_none() + || match data.role.as_ref().unwrap() { + SurfaceRole::Toplevel(t) => t.toplevel.is_alive(), + SurfaceRole::Popup(p) => p.popup.is_alive(), + SurfaceRole::Cursor => false, + }; + if role_alive { + client.kill( + dh, + ProtocolError { + code: xdg_surface::Error::DefunctRoleObject.into(), + object_id: resource.id().protocol_id(), + object_interface: XdgSurface::interface().name.to_string(), + message: "destroyed xdg surface before role".to_string(), + }, + ); + } + } + other => todo!("unhandled request {other:?}"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Rect { + pub size: Vec2, + pub offset: Vec2, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PositionerState { + pub size: Option, + pub anchor_rect: Option, + pub offset: Vec2, + pub anchor: xdg_positioner::Anchor, + pub gravity: xdg_positioner::Gravity, +} + +impl Default for PositionerState { + fn default() -> Self { + Self { + size: None, + anchor_rect: None, + offset: Vec2 { x: 0, y: 0 }, + anchor: xdg_positioner::Anchor::None, + gravity: xdg_positioner::Gravity::None, + } + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &Client, + resource: &XdgPositioner, + request: ::Request, + _: &(), + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + let hash_map::Entry::Occupied(mut data) = state + .positioners + .entry(PositionerId(resource.id().protocol_id())) + else { + unreachable!(); + }; + match request { + xdg_positioner::Request::SetSize { width, height } => { + data.get_mut().size = Some(Vec2 { + x: width, + y: height, + }); + } + xdg_positioner::Request::SetAnchorRect { + x, + y, + width, + height, + } => { + data.get_mut().anchor_rect = Some(Rect { + size: Vec2 { + x: width, + y: height, + }, + offset: Vec2 { x, y }, + }); + } + xdg_positioner::Request::SetOffset { x, y } => { + data.get_mut().offset = Vec2 { x, y }; + } + xdg_positioner::Request::SetAnchor { anchor } => { + data.get_mut().anchor = anchor.into_result().unwrap(); + } + xdg_positioner::Request::SetGravity { gravity } => { + data.get_mut().gravity = gravity.into_result().unwrap(); + } + xdg_positioner::Request::Destroy => { + data.remove(); + } + other => todo!("unhandled positioner request {other:?}"), + } + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + client: &wayland_server::Client, + _: &XdgWmBase, + request: ::Request, + _: &(), + dhandle: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + xdg_wm_base::Request::GetXdgSurface { id, surface } => { + let surface_id = SurfaceId(surface.id().protocol_id()); + let data = state.surfaces.get(&surface_id).unwrap(); + if data.buffer.is_some() { + client.kill( + dhandle, + ProtocolError { + code: xdg_wm_base::Error::InvalidSurfaceState.into(), + object_id: surface_id.0, + object_interface: XdgWmBase::interface().name.to_string(), + message: "Buffer already attached to surface".to_string(), + }, + ); + return; + } + data_init.init(id, surface_id); + } + xdg_wm_base::Request::CreatePositioner { id } => { + let pos = data_init.init(id, ()); + state.positioners.insert( + PositionerId(pos.id().protocol_id()), + PositionerState::default(), + ); + } + other => todo!("unhandled request {other:?}"), + } + } +} + +impl Dispatch for State { + fn request( + _: &mut Self, + _: &wayland_server::Client, + _: &WlShm, + request: ::Request, + _: &(), + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + proto::wl_shm::Request::CreatePool { id, .. } => { + data_init.init(id, ()); + } + _ => unreachable!(), + } + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &WlShmPool, + request: ::Request, + _: &(), + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + use proto::wl_shm_pool::Request::*; + match request { + CreateBuffer { id, .. } => { + let buf = data_init.init(id, ()); + state.buffers.insert(buf); + } + Destroy => {} + other => todo!("unhandled request {other:?}"), + } + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &wayland_server::Client, + resource: &WlBuffer, + request: ::Request, + _: &(), + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + proto::wl_buffer::Request::Destroy => { + state.buffers.remove(resource); + } + _ => unreachable!(), + } + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &WlCompositor, + request: ::Request, + _: &(), + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + proto::wl_compositor::Request::CreateSurface { id } => { + let surface = data_init.init(id, ()); + let id = surface.id().protocol_id(); + state.surfaces.insert( + SurfaceId(id), + SurfaceData { + surface, + buffer: None, + last_damage: None, + role: None, + }, + ); + state.last_surface_id = Some(SurfaceId(id)); + } + _ => unreachable!(), + } + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &wayland_server::Client, + resource: &WlSurface, + request: ::Request, + _: &(), + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + use proto::wl_surface::Request::*; + + let data = state + .surfaces + .get_mut(&SurfaceId(resource.id().protocol_id())) + .unwrap_or_else(|| panic!("{:?} missing from surface map", resource)); + + match request { + Attach { buffer, .. } => { + data.buffer = buffer; + } + Frame { callback } => { + // XXX: calling done immediately will cause wayland_backend to panic, + // report upstream + state.callbacks.push(data_init.init(callback, ())); + } + DamageBuffer { + x, + y, + width, + height, + } => { + data.last_damage = Some(BufferDamage { + x, + y, + width, + height, + }); + } + Commit => {} + Destroy => { + state + .surfaces + .remove(&SurfaceId(resource.id().protocol_id())); + } + other => todo!("unhandled request {other:?}"), + } + } +} + +impl Dispatch for State { + fn request( + _: &mut Self, + _: &wayland_server::Client, + _: &WlCallback, + _: ::Request, + _: &(), + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + unreachable!() + } +} diff --git a/wl_drm/Cargo.toml b/wl_drm/Cargo.toml new file mode 100644 index 0000000..5addbf5 --- /dev/null +++ b/wl_drm/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "wl_drm" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +wayland-client = "0.31.2" +wayland-scanner = "0.31.1" +wayland-server = "0.31.1" diff --git a/wl_drm/src/drm.xml b/wl_drm/src/drm.xml new file mode 100644 index 0000000..5e64622 --- /dev/null +++ b/wl_drm/src/drm.xml @@ -0,0 +1,185 @@ + + + + + Copyright © 2008-2011 Kristian Høgsberg + Copyright © 2010-2011 Intel Corporation + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that\n the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Bitmask of capabilities. + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wl_drm/src/lib.rs b/wl_drm/src/lib.rs new file mode 100644 index 0000000..31058d2 --- /dev/null +++ b/wl_drm/src/lib.rs @@ -0,0 +1,19 @@ +#![allow(non_camel_case_types, non_upper_case_globals)] +pub mod client { + use wayland_client::{self, protocol::*}; + pub mod __interfaces { + use wayland_client::protocol::__interfaces::*; + use wayland_client::backend as wayland_backend; + wayland_scanner::generate_interfaces!("src/drm.xml"); + } + use self::__interfaces::*; + wayland_scanner::generate_client_code!("src/drm.xml"); +} + +pub mod server { + use wayland_server::{self, protocol::*}; + pub use super::client::__interfaces; + use self::__interfaces::*; + wayland_scanner::generate_server_code!("src/drm.xml"); +} +