git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitd72c3b91172bcfa9daf8803976ade9cb3114b23b
parentc7d134ff54
authorLucas Galante <[email protected]>
date2026-05-27 23:13
feat: Port compositor core to Rust up to Phase 14 (Keyboard Grouping)

 .gitignore                        |    2 +
 Cargo.lock                        |  637 ++++++++++++
 Cargo.toml                        |   14 +-
 build.rs                          |  162 +++
 src/cursor.rs                     |  963 ++++++++++++++++++
 src/drag_icon.rs                  |   92 ++
 src/ffi.rs                        |   19 +
 src/idle_inhibit_manager.rs       |  144 +++
 src/input_device.rs               |  102 ++
 src/input_manager.rs              |  120 +++
 src/input_popup.rs                |  231 +++++
 src/input_relay.rs                |  283 +++++
 src/keyboard.rs                   |  251 +++++
 src/keyboard_group.rs             |  453 +++++++++
 src/layer_shell.rs                |  861 ++++++++++++++++
 src/libinput_config.rs            |  194 ++++
 src/lock_manager.rs               |  430 ++++++++
 src/main.rs                       |  267 ++++-
 src/output.rs                     |  422 ++++++++
 src/output_manager.rs             |  648 ++++++++++++
 src/pointer_binding.rs            |  172 ++++
 src/pointer_constraint.rs         |  259 +++++
 src/process.rs                    |   55 +
 src/scene.rs                      |  213 ++++
 src/scene_node_data.rs            |   83 ++
 src/seat.rs                       | 1036 +++++++++++++++++++
 src/server.rs                     |  697 +++++++++++++
 src/shell_surface.rs              |  266 +++++
 src/slotmap.rs                    |  195 ++++
 src/tablet.rs                     |   51 +
 src/tablet_tool.rs                |  366 +++++++
 src/text_input.rs                 |  154 +++
 src/util.rs                       |   21 +
 src/window.rs                     | 2038 +++++++++++++++++++++++++++++++++++++
 src/window_manager.rs             |  727 +++++++++++++
 src/wlroots_log_wrapper.c         |  650 ++++++++++++
 src/wm_node.rs                    |  244 +++++
 src/xdg_popup.rs                  |  142 +++
 src/xdg_toplevel.rs               |  651 ++++++++++++
 src/xkb_bindings.rs               |  520 ++++++++++
 src/xkb_config.rs                 |  325 ++++++
 src/xwayland_override_redirect.rs |  280 +++++
 src/xwayland_window.rs            |  418 ++++++++
 wrapper.h                         |  257 +++++
 44 files changed, 16113 insertions(+), 2 deletions(-)

diff --git a/.gitignore b/.gitignore
index ea8c4bf..b1df094 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
 /target
+*.log
+check_errors.txt
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..40a8df4
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,637 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.69.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "itertools",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn",
+ "which",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "cc"
+version = "1.2.62"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "clear-river-rust"
+version = "0.1.0"
+dependencies = [
+ "bindgen",
+ "bitflags",
+ "cc",
+ "clap",
+ "env_logger",
+ "libc",
+ "log",
+ "nix",
+ "pkg-config",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "either"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+
+[[package]]
+name = "env_filter"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "jiff",
+ "log",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "home"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "jiff"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "392c70591e8749fe235ddaf513e6f58b26bce3dcc16524cecc8936f75afa161e"
+dependencies = [
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b605b0c050d845fc355bb11eb3f9a8deddc218ea60c76e61aa1f2adfb2c96a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[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.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libloading"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "log"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
+
+[[package]]
+name = "memchr"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
+[[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.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "portable-atomic"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[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.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[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 = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
diff --git a/Cargo.toml b/Cargo.toml
index aa6c7a9..a7d070b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,18 @@
 [package]
 name = "clear-river-rust"
 version = "0.1.0"
-edition = "2024"
+edition = "2021"
 
 [dependencies]
+libc = "0.2"
+nix = { version = "0.29", features = ["signal", "process", "fs", "poll", "resource"] }
+bitflags = "2"
+clap = { version = "4.4", features = ["derive"] }
+log = "0.4"
+env_logger = "0.11"
+
+[build-dependencies]
+bindgen = "0.69"
+cc = "1.0"
+pkg-config = "0.3"
+
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..774937f
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,162 @@
+use std::env;
+use std::path::PathBuf;
+
+fn main() {
+    println!("cargo:rerun-if-changed=src/wlroots_log_wrapper.c");
+    println!("cargo:rerun-if-changed=wrapper.h");
+
+    // Probe system libraries
+    let wlroots = pkg_config::Config::new()
+        .atleast_version("0.20.0")
+        .probe("wlroots-0.20")
+        .expect("wlroots-0.20 is required");
+
+    let wl_server = pkg_config::probe_library("wayland-server")
+        .expect("wayland-server is required");
+
+    let xkb = pkg_config::probe_library("xkbcommon")
+        .expect("xkbcommon is required");
+
+    let pixman = pkg_config::probe_library("pixman-1")
+        .expect("pixman-1 is required");
+
+    let libinput = pkg_config::probe_library("libinput")
+        .expect("libinput is required");
+
+    let libevdev = pkg_config::probe_library("libevdev")
+        .expect("libevdev is required");
+
+    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
+
+    // Helper to fix XML files starting with comments instead of the XML declaration
+    let clean_xml = |src: &str, dst: &std::path::Path| {
+        let content = std::fs::read_to_string(src).expect("Failed to read XML source");
+        let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
+        if lines.len() > 1 && lines[0].starts_with("<!--") && lines[1].starts_with("<?xml") {
+            lines.swap(0, 1);
+        }
+        let cleaned = lines.join("\n");
+        std::fs::write(dst, cleaned).expect("Failed to write cleaned XML");
+    };
+
+    // Generate upstream protocol headers (header-only)
+    let upstream_protocols = vec![
+        ("wlr-layer-shell-unstable-v1.xml", "../clear-river/protocol/upstream/wlr-layer-shell-unstable-v1.xml"),
+        ("wlr-output-power-management-unstable-v1.xml", "../clear-river/protocol/upstream/wlr-output-power-management-unstable-v1.xml"),
+        ("virtual-keyboard-unstable-v1.xml", "../clear-river/protocol/upstream/virtual-keyboard-unstable-v1.xml"),
+    ];
+
+    for (name, path) in upstream_protocols {
+        let temp_xml = out_dir.join(format!("{}-temp.xml", name));
+        clean_xml(path, &temp_xml);
+        let header_name = name.replace(".xml", "-protocol.h");
+        let status = std::process::Command::new("wayland-scanner")
+            .args(&[
+                "server-header",
+                temp_xml.to_str().unwrap(),
+                out_dir.join(&header_name).to_str().unwrap(),
+            ])
+            .status()
+            .expect("failed to execute wayland-scanner");
+        assert!(status.success(), "wayland-scanner server-header failed for {}", name);
+    }
+
+    // Generate custom river protocol headers and private-code C files
+    let custom_protocols = vec![
+        ("river-window-management-v1.xml", "../clear-river/protocol/river-window-management-v1.xml"),
+        ("river-xkb-bindings-v1.xml", "../clear-river/protocol/river-xkb-bindings-v1.xml"),
+        ("river-layer-shell-v1.xml", "../clear-river/protocol/river-layer-shell-v1.xml"),
+        ("river-input-management-v1.xml", "../clear-river/protocol/river-input-management-v1.xml"),
+        ("river-libinput-config-v1.xml", "../clear-river/protocol/river-libinput-config-v1.xml"),
+        ("river-xkb-config-v1.xml", "../clear-river/protocol/river-xkb-config-v1.xml"),
+    ];
+
+    let mut generated_c_files = Vec::new();
+
+    for (name, path) in custom_protocols {
+        let temp_xml = out_dir.join(format!("{}-temp.xml", name));
+        clean_xml(path, &temp_xml);
+
+        let header_name = name.replace(".xml", "-protocol.h");
+        let status_h = std::process::Command::new("wayland-scanner")
+            .args(&[
+                "server-header",
+                temp_xml.to_str().unwrap(),
+                out_dir.join(&header_name).to_str().unwrap(),
+            ])
+            .status()
+            .expect("failed to execute wayland-scanner");
+        assert!(status_h.success(), "wayland-scanner server-header failed for {}", name);
+
+        let code_name = name.replace(".xml", "-protocol.c");
+        let code_path = out_dir.join(&code_name);
+        let status_c = std::process::Command::new("wayland-scanner")
+            .args(&[
+                "private-code",
+                temp_xml.to_str().unwrap(),
+                code_path.to_str().unwrap(),
+            ])
+            .status()
+            .expect("failed to execute wayland-scanner");
+        assert!(status_c.success(), "wayland-scanner private-code failed for {}", name);
+
+        generated_c_files.push(code_path);
+    }
+
+    // Compile the C wrapper and protocol C files
+    let mut build = cc::Build::new();
+    build.file("src/wlroots_log_wrapper.c")
+        .define("WLR_USE_UNSTABLE", None)
+        .flag("-std=c99")
+        .flag("-O2")
+        .include(&out_dir);
+
+    for c_file in generated_c_files {
+        build.file(c_file);
+    }
+
+    // Add include paths for wlroots and wayland-server
+    for path in &wlroots.include_paths {
+        build.include(path);
+    }
+    for path in &wl_server.include_paths {
+        build.include(path);
+    }
+    build.compile("wlroots_log_wrapper");
+
+
+    // Setup bindgen
+    let mut builder = bindgen::Builder::default()
+        .header("wrapper.h")
+        .clang_arg("-DWLR_USE_UNSTABLE")
+        .layout_tests(false)
+        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()));
+
+    // Pass include paths to bindgen clang argument parser
+    let mut include_paths = vec![out_dir.clone()];
+    include_paths.extend(wlroots.include_paths.clone());
+    include_paths.extend(wl_server.include_paths.clone());
+    include_paths.extend(xkb.include_paths.clone());
+    include_paths.extend(pixman.include_paths.clone());
+    include_paths.extend(libinput.include_paths.clone());
+    include_paths.extend(libevdev.include_paths.clone());
+
+    for path in include_paths {
+        builder = builder.clang_arg(format!("-I{}", path.display()));
+    }
+
+    let bindings = builder
+        .blocklist_item("FP_NAN")
+        .blocklist_item("FP_INFINITE")
+        .blocklist_item("FP_ZERO")
+        .blocklist_item("FP_SUBNORMAL")
+        .blocklist_item("FP_NORMAL")
+        .blocklist_item("wl_listener")
+        .generate()
+        .expect("Unable to generate bindings");
+
+    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+    bindings
+        .write_to_file(out_path.join("bindings.rs"))
+        .expect("Couldn't write bindings!");
+}
diff --git a/src/cursor.rs b/src/cursor.rs
new file mode 100644
index 0000000..757a0ab
--- /dev/null
+++ b/src/cursor.rs
@@ -0,0 +1,963 @@
+use crate::ffi;
+use crate::seat::{Seat, Focus};
+use crate::server::{WlListener, wl_listener_remove, wl_signal_add, WlList};
+use crate::scene_node_data::SceneNodeDataVal;
+use crate::drag_icon::DragIcon;
+use std::collections::HashMap;
+
+pub struct Cursor {
+    pub seat: *mut Seat,
+    pub wlr_cursor: *mut ffi::wlr_cursor,
+    pub xcursor_manager: *mut ffi::wlr_xcursor_manager,
+    pub constraint: *mut crate::pointer_constraint::PointerConstraint,
+
+    pub motion_listener: ffi::wl_listener,
+    pub motion_absolute_listener: ffi::wl_listener,
+    pub button_listener: ffi::wl_listener,
+    pub axis_listener: ffi::wl_listener,
+    pub frame_listener: ffi::wl_listener,
+
+    pub tablet_tool_axis_listener: ffi::wl_listener,
+    pub tablet_tool_proximity_listener: ffi::wl_listener,
+    pub tablet_tool_tip_listener: ffi::wl_listener,
+    pub tablet_tool_button_listener: ffi::wl_listener,
+
+    pub touch_points: HashMap<i32, (f64, f64)>,
+    pub pressed: HashMap<u32, Option<*mut crate::pointer_binding::PointerBinding>>,
+
+    pub touch_down_listener: ffi::wl_listener,
+    pub touch_motion_listener: ffi::wl_listener,
+    pub touch_up_listener: ffi::wl_listener,
+    pub touch_cancel_listener: ffi::wl_listener,
+    pub touch_frame_listener: ffi::wl_listener,
+
+    pub swipe_begin_listener: ffi::wl_listener,
+    pub swipe_update_listener: ffi::wl_listener,
+    pub swipe_end_listener: ffi::wl_listener,
+
+    pub pinch_begin_listener: ffi::wl_listener,
+    pub pinch_update_listener: ffi::wl_listener,
+    pub pinch_end_listener: ffi::wl_listener,
+
+    pub hold_begin_listener: ffi::wl_listener,
+    pub hold_end_listener: ffi::wl_listener,
+}
+
+impl Default for Cursor {
+    fn default() -> Self {
+        Self {
+            seat: std::ptr::null_mut(),
+            wlr_cursor: std::ptr::null_mut(),
+            xcursor_manager: std::ptr::null_mut(),
+            constraint: std::ptr::null_mut(),
+            motion_listener: unsafe { std::mem::zeroed() },
+            motion_absolute_listener: unsafe { std::mem::zeroed() },
+            button_listener: unsafe { std::mem::zeroed() },
+            axis_listener: unsafe { std::mem::zeroed() },
+            frame_listener: unsafe { std::mem::zeroed() },
+            tablet_tool_axis_listener: unsafe { std::mem::zeroed() },
+            tablet_tool_proximity_listener: unsafe { std::mem::zeroed() },
+            tablet_tool_tip_listener: unsafe { std::mem::zeroed() },
+            tablet_tool_button_listener: unsafe { std::mem::zeroed() },
+
+            touch_points: HashMap::new(),
+            pressed: HashMap::new(),
+
+            touch_down_listener: unsafe { std::mem::zeroed() },
+            touch_motion_listener: unsafe { std::mem::zeroed() },
+            touch_up_listener: unsafe { std::mem::zeroed() },
+            touch_cancel_listener: unsafe { std::mem::zeroed() },
+            touch_frame_listener: unsafe { std::mem::zeroed() },
+
+            swipe_begin_listener: unsafe { std::mem::zeroed() },
+            swipe_update_listener: unsafe { std::mem::zeroed() },
+            swipe_end_listener: unsafe { std::mem::zeroed() },
+
+            pinch_begin_listener: unsafe { std::mem::zeroed() },
+            pinch_update_listener: unsafe { std::mem::zeroed() },
+            pinch_end_listener: unsafe { std::mem::zeroed() },
+
+            hold_begin_listener: unsafe { std::mem::zeroed() },
+            hold_end_listener: unsafe { std::mem::zeroed() },
+        }
+    }
+}
+
+impl Cursor {
+    pub unsafe fn init(
+        &mut self,
+        seat: *mut Seat,
+        output_layout: *mut ffi::wlr_output_layout,
+    ) -> Result<(), &'static str> {
+        let wlr_cursor = ffi::wlr_cursor_create();
+        if wlr_cursor.is_null() {
+            return Err("Failed to create wlr_cursor");
+        }
+        ffi::wlr_cursor_attach_output_layout(wlr_cursor, output_layout);
+
+        let xcursor_manager = ffi::wlr_xcursor_manager_create(std::ptr::null(), 24);
+        if xcursor_manager.is_null() {
+            ffi::wlr_cursor_destroy(wlr_cursor);
+            return Err("Failed to create wlr_xcursor_manager");
+        }
+
+        self.seat = seat;
+        self.wlr_cursor = wlr_cursor;
+        self.xcursor_manager = xcursor_manager;
+
+        // Load default cursor theme
+        ffi::wlr_xcursor_manager_load(xcursor_manager, 1.0);
+        ffi::wlr_cursor_set_xcursor(wlr_cursor, xcursor_manager, b"default\0".as_ptr() as *const _);
+
+        // Setup listeners
+        let motion_ptr = &mut self.motion_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*motion_ptr).notify = Some(handle_motion);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_motion_signal(wlr_cursor),
+            &mut self.motion_listener,
+        );
+
+        let absolute_ptr = &mut self.motion_absolute_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*absolute_ptr).notify = Some(handle_motion_absolute);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_motion_absolute_signal(wlr_cursor),
+            &mut self.motion_absolute_listener,
+        );
+
+        let button_ptr = &mut self.button_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*button_ptr).notify = Some(handle_button);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_button_signal(wlr_cursor),
+            &mut self.button_listener,
+        );
+
+        let axis_ptr = &mut self.axis_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*axis_ptr).notify = Some(handle_axis);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_axis_signal(wlr_cursor),
+            &mut self.axis_listener,
+        );
+
+        let frame_ptr = &mut self.frame_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*frame_ptr).notify = Some(handle_frame);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_frame_signal(wlr_cursor),
+            &mut self.frame_listener,
+        );
+
+        let tablet_axis_ptr = &mut self.tablet_tool_axis_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*tablet_axis_ptr).notify = Some(handle_tablet_tool_axis);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_tablet_tool_axis_signal(wlr_cursor),
+            &mut self.tablet_tool_axis_listener,
+        );
+
+        let tablet_proximity_ptr = &mut self.tablet_tool_proximity_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*tablet_proximity_ptr).notify = Some(handle_tablet_tool_proximity);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_tablet_tool_proximity_signal(wlr_cursor),
+            &mut self.tablet_tool_proximity_listener,
+        );
+
+        let tablet_tip_ptr = &mut self.tablet_tool_tip_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*tablet_tip_ptr).notify = Some(handle_tablet_tool_tip);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_tablet_tool_tip_signal(wlr_cursor),
+            &mut self.tablet_tool_tip_listener,
+        );
+
+        let tablet_button_ptr = &mut self.tablet_tool_button_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*tablet_button_ptr).notify = Some(handle_tablet_tool_button);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_tablet_tool_button_signal(wlr_cursor),
+            &mut self.tablet_tool_button_listener,
+        );
+
+        let touch_down_ptr = &mut self.touch_down_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*touch_down_ptr).notify = Some(handle_touch_down);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_touch_down_signal(wlr_cursor),
+            &mut self.touch_down_listener,
+        );
+
+        let touch_motion_ptr = &mut self.touch_motion_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*touch_motion_ptr).notify = Some(handle_touch_motion);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_touch_motion_signal(wlr_cursor),
+            &mut self.touch_motion_listener,
+        );
+
+        let touch_up_ptr = &mut self.touch_up_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*touch_up_ptr).notify = Some(handle_touch_up);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_touch_up_signal(wlr_cursor),
+            &mut self.touch_up_listener,
+        );
+
+        let touch_cancel_ptr = &mut self.touch_cancel_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*touch_cancel_ptr).notify = Some(handle_touch_cancel);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_touch_cancel_signal(wlr_cursor),
+            &mut self.touch_cancel_listener,
+        );
+
+        let touch_frame_ptr = &mut self.touch_frame_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*touch_frame_ptr).notify = Some(handle_touch_frame);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_touch_frame_signal(wlr_cursor),
+            &mut self.touch_frame_listener,
+        );
+
+        let swipe_begin_ptr = &mut self.swipe_begin_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*swipe_begin_ptr).notify = Some(handle_swipe_begin);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_swipe_begin_signal(wlr_cursor),
+            &mut self.swipe_begin_listener,
+        );
+
+        let swipe_update_ptr = &mut self.swipe_update_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*swipe_update_ptr).notify = Some(handle_swipe_update);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_swipe_update_signal(wlr_cursor),
+            &mut self.swipe_update_listener,
+        );
+
+        let swipe_end_ptr = &mut self.swipe_end_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*swipe_end_ptr).notify = Some(handle_swipe_end);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_swipe_end_signal(wlr_cursor),
+            &mut self.swipe_end_listener,
+        );
+
+        let pinch_begin_ptr = &mut self.pinch_begin_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*pinch_begin_ptr).notify = Some(handle_pinch_begin);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_pinch_begin_signal(wlr_cursor),
+            &mut self.pinch_begin_listener,
+        );
+
+        let pinch_update_ptr = &mut self.pinch_update_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*pinch_update_ptr).notify = Some(handle_pinch_update);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_pinch_update_signal(wlr_cursor),
+            &mut self.pinch_update_listener,
+        );
+
+        let pinch_end_ptr = &mut self.pinch_end_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*pinch_end_ptr).notify = Some(handle_pinch_end);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_pinch_end_signal(wlr_cursor),
+            &mut self.pinch_end_listener,
+        );
+
+        let hold_begin_ptr = &mut self.hold_begin_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*hold_begin_ptr).notify = Some(handle_hold_begin);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_hold_begin_signal(wlr_cursor),
+            &mut self.hold_begin_listener,
+        );
+
+        let hold_end_ptr = &mut self.hold_end_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*hold_end_ptr).notify = Some(handle_hold_end);
+        wl_signal_add(
+            ffi::river_wlr_cursor_get_hold_end_signal(wlr_cursor),
+            &mut self.hold_end_listener,
+        );
+
+        Ok(())
+    }
+
+    pub unsafe fn deinit(&mut self) {
+        wl_listener_remove(&mut self.tablet_tool_axis_listener);
+        wl_listener_remove(&mut self.tablet_tool_proximity_listener);
+        wl_listener_remove(&mut self.tablet_tool_tip_listener);
+        wl_listener_remove(&mut self.tablet_tool_button_listener);
+
+        wl_listener_remove(&mut self.touch_down_listener);
+        wl_listener_remove(&mut self.touch_motion_listener);
+        wl_listener_remove(&mut self.touch_up_listener);
+        wl_listener_remove(&mut self.touch_cancel_listener);
+        wl_listener_remove(&mut self.touch_frame_listener);
+
+        wl_listener_remove(&mut self.swipe_begin_listener);
+        wl_listener_remove(&mut self.swipe_update_listener);
+        wl_listener_remove(&mut self.swipe_end_listener);
+
+        wl_listener_remove(&mut self.pinch_begin_listener);
+        wl_listener_remove(&mut self.pinch_update_listener);
+        wl_listener_remove(&mut self.pinch_end_listener);
+
+        wl_listener_remove(&mut self.hold_begin_listener);
+        wl_listener_remove(&mut self.hold_end_listener);
+
+        if !self.xcursor_manager.is_null() {
+            ffi::wlr_xcursor_manager_destroy(self.xcursor_manager);
+            self.xcursor_manager = std::ptr::null_mut();
+        }
+        if !self.wlr_cursor.is_null() {
+            ffi::wlr_cursor_destroy(self.wlr_cursor);
+            self.wlr_cursor = std::ptr::null_mut();
+        }
+    }
+
+    pub unsafe fn x(&self) -> f64 {
+        ffi::river_wlr_cursor_get_x(self.wlr_cursor)
+    }
+
+    pub unsafe fn y(&self) -> f64 {
+        ffi::river_wlr_cursor_get_y(self.wlr_cursor)
+    }
+
+    pub unsafe fn update_hovered(&mut self) {
+        let lx = self.x();
+        let ly = self.y();
+        let server = (*self.seat).server;
+
+        let at_res = (*server).scene.at(lx, ly);
+        let current_focused = &(*self.seat).focused;
+
+        if let Some(result) = at_res {
+            // Find window/surface under cursor and focus
+            match result.data {
+                SceneNodeDataVal::Window(_) => {
+                    // Update hover or click focus
+                }
+                SceneNodeDataVal::LayerSurface(_) => {
+                    let surface = result.surface;
+                    if !matches!(current_focused, Focus::LayerSurface(s) if *s == surface) {
+                        (*self.seat).focus(Focus::LayerSurface(surface));
+                    }
+                }
+                SceneNodeDataVal::LockSurface(lock_surf) => {
+                    if !matches!(current_focused, Focus::LockSurface(s) if *s == lock_surf) {
+                        (*self.seat).focus(Focus::LockSurface(lock_surf));
+                    }
+                }
+                _ => {}
+            }
+        }
+    }
+
+    pub unsafe fn update_state(&mut self) {
+        if !self.constraint.is_null() {
+            (*self.constraint).update_state();
+        }
+        self.update_hovered();
+    }
+
+    pub unsafe fn update_drag_icons(&mut self) {
+        let drag_icons_tree = (*(*self.seat).server).scene.drag_icons;
+        let children_head = ffi::river_scene_tree_get_children(drag_icons_tree) as *mut WlList;
+        let mut curr = (*children_head).next;
+        while curr != children_head {
+            let next = (*curr).next;
+            let node = ffi::river_scene_node_from_children_link(curr as *mut ffi::wl_list);
+            let drag_icon_raw = ffi::river_scene_node_get_data(node) as *mut DragIcon;
+            if !drag_icon_raw.is_null() {
+                let drag_icon = &mut *drag_icon_raw;
+                let drag_seat = ffi::river_wlr_drag_get_seat((*drag_icon.wlr_drag_icon).drag);
+                if drag_seat == (*self.seat).wlr_seat {
+                    drag_icon.update_position(self);
+                }
+            }
+            curr = next;
+        }
+    }
+
+    pub unsafe fn set_theme(
+        &mut self,
+        theme: *const std::os::raw::c_char,
+        size: u32,
+    ) -> Result<(), &'static str> {
+        let size = if size == 0 { 24 } else { size };
+
+        let xcursor_manager = ffi::wlr_xcursor_manager_create(theme, size);
+        if xcursor_manager.is_null() {
+            return Err("Failed to create wlr_xcursor_manager");
+        }
+
+        // If this cursor belongs to the default seat, update the Xwayland cursor to match the theme.
+        let default_seat = (*(*self.seat).server).input_manager.default_seat;
+        if self.seat == default_seat {
+            if !(*(*self.seat).server).xwayland.is_null() {
+                ffi::wlr_xcursor_manager_load(xcursor_manager, 1.0);
+                let wlr_xcursor = ffi::wlr_xcursor_manager_get_xcursor(
+                    xcursor_manager,
+                    b"default\0".as_ptr() as *const _,
+                    1.0,
+                );
+                if !wlr_xcursor.is_null() && (*wlr_xcursor).image_count > 0 {
+                    let image = *(*wlr_xcursor).images;
+                    if !image.is_null() {
+                        ffi::wlr_xwayland_set_cursor(
+                            (*(*self.seat).server).xwayland,
+                            ffi::wlr_xcursor_image_get_buffer(image),
+                            (*image).hotspot_x as i32,
+                            (*image).hotspot_y as i32,
+                        );
+                    }
+                }
+            }
+        }
+
+        if !self.xcursor_manager.is_null() {
+            ffi::wlr_xcursor_manager_destroy(self.xcursor_manager);
+        }
+        self.xcursor_manager = xcursor_manager;
+
+        ffi::wlr_xcursor_manager_load(self.xcursor_manager, 1.0);
+        ffi::wlr_cursor_set_xcursor(
+            self.wlr_cursor,
+            self.xcursor_manager,
+            b"default\0".as_ptr() as *const _,
+        );
+
+        Ok(())
+    }
+
+    pub unsafe fn op_start_pointer(&mut self) {
+        if !self.constraint.is_null() {
+            if let crate::pointer_constraint::PointerConstraintState::Active { .. } = (*self.constraint).state {
+                (*self.constraint).deactivate();
+            }
+        }
+
+        log::debug!("entering cursor mode op");
+        ffi::wlr_seat_pointer_notify_clear_focus((*self.seat).wlr_seat);
+    }
+
+    pub unsafe fn op_end_pointer(&mut self) {
+        if self.pressed.is_empty() {
+            log::debug!("entering cursor mode passthrough");
+            self.update_state();
+        } else {
+            log::debug!("entering cursor mode ignore");
+        }
+    }
+}
+
+unsafe extern "C" fn handle_motion(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, motion_listener);
+    let event = data as *mut ffi::wlr_pointer_motion_event;
+    
+    let mut dx = (*event).delta_x;
+    let mut dy = (*event).delta_y;
+
+    if !cursor.constraint.is_null() {
+        (*cursor.constraint).confine(&mut dx, &mut dy);
+    }
+
+    ffi::wlr_cursor_move(cursor.wlr_cursor, std::ptr::null_mut(), dx, dy);
+    cursor.update_hovered();
+    cursor.update_drag_icons();
+
+    let seat = &mut *cursor.seat;
+    if (*seat).op.is_some() {
+        let lx = (*cursor.wlr_cursor).x as i32;
+        let ly = (*cursor.wlr_cursor).y as i32;
+        (*seat).op_update(lx, ly);
+        return;
+    }
+
+    let focused_client = ffi::river_wlr_seat_get_pointer_focused_client(seat.wlr_seat);
+    if !focused_client.is_null() {
+        let lx = cursor.x();
+        let ly = cursor.y();
+        let server = seat.server;
+        if let Some(result) = (*server).scene.at(lx, ly) {
+            ffi::wlr_seat_pointer_notify_motion(
+                seat.wlr_seat,
+                (*event).time_msec,
+                result.sx,
+                result.sy,
+            );
+        }
+    }
+}
+
+unsafe extern "C" fn handle_motion_absolute(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, motion_absolute_listener);
+    let event = data as *mut ffi::wlr_pointer_motion_absolute_event;
+    
+    ffi::wlr_cursor_warp_absolute(cursor.wlr_cursor, std::ptr::null_mut(), (*event).x, (*event).y);
+    cursor.update_hovered();
+    cursor.update_drag_icons();
+
+    let seat = &mut *cursor.seat;
+    if (*seat).op.is_some() {
+        let lx = (*cursor.wlr_cursor).x as i32;
+        let ly = (*cursor.wlr_cursor).y as i32;
+        (*seat).op_update(lx, ly);
+        return;
+    }
+
+    let focused_client = ffi::river_wlr_seat_get_pointer_focused_client(seat.wlr_seat);
+    if !focused_client.is_null() {
+        let lx = cursor.x();
+        let ly = cursor.y();
+        let server = seat.server;
+        if let Some(result) = (*server).scene.at(lx, ly) {
+            ffi::wlr_seat_pointer_notify_motion(
+                seat.wlr_seat,
+                (*event).time_msec,
+                result.sx,
+                result.sy,
+            );
+        }
+    }
+}
+
+unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, button_listener);
+    let event = data as *mut ffi::wlr_pointer_button_event;
+    
+    let seat = &mut *cursor.seat;
+    
+    if (*event).state == ffi::wl_pointer_button_state_WL_POINTER_BUTTON_STATE_PRESSED {
+        if cursor.pressed.contains_key(&(*event).button) {
+            log::error!("ignoring duplicate pointer button {} press", (*event).button);
+            return;
+        }
+
+        if let Some(binding) = seat.match_pointer_binding((*event).button) {
+            cursor.pressed.insert((*event).button, Some(binding));
+            (*binding).pressed();
+            return;
+        }
+
+        cursor.pressed.insert((*event).button, None);
+
+        ffi::wlr_seat_pointer_notify_button(
+            seat.wlr_seat,
+            (*event).time_msec,
+            (*event).button,
+            (*event).state,
+        );
+
+        // If pressed, update focus to window under cursor
+        let lx = cursor.x();
+        let ly = cursor.y();
+        let server = seat.server;
+        if let Some(result) = (*server).scene.at(lx, ly) {
+            match result.data {
+                SceneNodeDataVal::LayerSurface(_) => {
+                    seat.focus(Focus::LayerSurface(result.surface));
+                }
+                _ => {}
+            }
+        }
+    } else {
+        assert_eq!((*event).state, ffi::wl_pointer_button_state_WL_POINTER_BUTTON_STATE_RELEASED);
+        if let Some(binding_opt) = cursor.pressed.remove(&(*event).button) {
+            if let Some(binding) = binding_opt {
+                (*binding).released();
+                if cursor.pressed.is_empty() && seat.op.is_some() {
+                    seat.op_release = true;
+                    (*(*seat).server).wm.dirty_windowing();
+                }
+                return;
+            }
+
+            ffi::wlr_seat_pointer_notify_button(
+                seat.wlr_seat,
+                (*event).time_msec,
+                (*event).button,
+                (*event).state,
+            );
+
+            if cursor.pressed.is_empty() && seat.op.is_some() {
+                seat.op_release = true;
+                (*(*seat).server).wm.dirty_windowing();
+            }
+        } else {
+            log::error!("ignoring duplicate pointer button {} release", (*event).button);
+        }
+    }
+}
+
+unsafe extern "C" fn handle_axis(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, axis_listener);
+    let event = data as *mut ffi::wlr_pointer_axis_event;
+    
+    let seat = &mut *cursor.seat;
+    ffi::wlr_seat_pointer_notify_axis(
+        seat.wlr_seat,
+        (*event).time_msec,
+        (*event).orientation,
+        (*event).delta,
+        (*event).delta_discrete,
+        (*event).source,
+        (*event).relative_direction,
+    );
+}
+
+unsafe extern "C" fn handle_frame(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, frame_listener);
+    
+    let seat = &mut *cursor.seat;
+    ffi::wlr_seat_pointer_notify_frame(seat.wlr_seat);
+}
+
+unsafe extern "C" fn handle_tablet_tool_axis(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, tablet_tool_axis_listener);
+    let event = data as *mut ffi::wlr_tablet_tool_axis_event;
+
+    let wlr_tablet = (*event).tablet;
+    let wlr_device = &mut (*wlr_tablet).base as *mut ffi::wlr_input_device;
+    let device = ffi::river_wlr_input_device_get_data(wlr_device) as *mut crate::input_device::InputDevice;
+    if device.is_null() {
+        return;
+    }
+    let seat = (*device).seat;
+    (*seat).handle_activity();
+
+    let tablet = (*device).destroy_data as *mut crate::tablet::Tablet;
+    if tablet.is_null() {
+        return;
+    }
+
+    if let Ok(tool) = crate::tablet_tool::TabletTool::get((*seat).wlr_seat, (*event).tool) {
+        (*tool).axis(tablet, event);
+    }
+}
+
+unsafe extern "C" fn handle_tablet_tool_proximity(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, tablet_tool_proximity_listener);
+    let event = data as *mut ffi::wlr_tablet_tool_proximity_event;
+
+    let wlr_tablet = (*event).tablet;
+    let wlr_device = &mut (*wlr_tablet).base as *mut ffi::wlr_input_device;
+    let device = ffi::river_wlr_input_device_get_data(wlr_device) as *mut crate::input_device::InputDevice;
+    if device.is_null() {
+        return;
+    }
+    let seat = (*device).seat;
+    (*seat).handle_activity();
+
+    let tablet = (*device).destroy_data as *mut crate::tablet::Tablet;
+    if tablet.is_null() {
+        return;
+    }
+
+    if let Ok(tool) = crate::tablet_tool::TabletTool::get((*seat).wlr_seat, (*event).tool) {
+        (*tool).proximity(tablet, event);
+    }
+}
+
+unsafe extern "C" fn handle_tablet_tool_tip(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, tablet_tool_tip_listener);
+    let event = data as *mut ffi::wlr_tablet_tool_tip_event;
+
+    let wlr_tablet = (*event).tablet;
+    let wlr_device = &mut (*wlr_tablet).base as *mut ffi::wlr_input_device;
+    let device = ffi::river_wlr_input_device_get_data(wlr_device) as *mut crate::input_device::InputDevice;
+    if device.is_null() {
+        return;
+    }
+    let seat = (*device).seat;
+    (*seat).handle_activity();
+
+    let tablet = (*device).destroy_data as *mut crate::tablet::Tablet;
+    if tablet.is_null() {
+        return;
+    }
+
+    if let Ok(tool) = crate::tablet_tool::TabletTool::get((*seat).wlr_seat, (*event).tool) {
+        (*tool).tip(tablet, event);
+    }
+}
+
+unsafe extern "C" fn handle_tablet_tool_button(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, tablet_tool_button_listener);
+    let event = data as *mut ffi::wlr_tablet_tool_button_event;
+
+    let wlr_tablet = (*event).tablet;
+    let wlr_device = &mut (*wlr_tablet).base as *mut ffi::wlr_input_device;
+    let device = ffi::river_wlr_input_device_get_data(wlr_device) as *mut crate::input_device::InputDevice;
+    if device.is_null() {
+        return;
+    }
+    let seat = (*device).seat;
+    (*seat).handle_activity();
+
+    let tablet = (*device).destroy_data as *mut crate::tablet::Tablet;
+    if tablet.is_null() {
+        return;
+    }
+
+    if let Ok(tool) = crate::tablet_tool::TabletTool::get((*seat).wlr_seat, (*event).tool) {
+        (*tool).button(tablet, event);
+    }
+}
+
+unsafe extern "C" fn handle_touch_down(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, touch_down_listener);
+    let event = data as *mut ffi::wlr_touch_down_event;
+
+    let seat = &mut *cursor.seat;
+    seat.handle_activity();
+
+    let mut lx = 0.0;
+    let mut ly = 0.0;
+    let wlr_device = &mut (*(*event).touch).base as *mut ffi::wlr_input_device;
+    ffi::wlr_cursor_absolute_to_layout_coords(
+        cursor.wlr_cursor,
+        wlr_device,
+        (*event).x,
+        (*event).y,
+        &mut lx,
+        &mut ly,
+    );
+
+    cursor.touch_points.insert((*event).touch_id, (lx, ly));
+
+    let server = seat.server;
+    if let Some(result) = (*server).scene.at(lx, ly) {
+        match result.data {
+            SceneNodeDataVal::LayerSurface(_) => {
+                seat.focus(Focus::LayerSurface(result.surface));
+            }
+            _ => {}
+        }
+        if !result.surface.is_null() {
+            ffi::wlr_seat_touch_notify_down(
+                seat.wlr_seat,
+                result.surface,
+                (*event).time_msec,
+                (*event).touch_id,
+                result.sx,
+                result.sy,
+            );
+        }
+    }
+}
+
+unsafe extern "C" fn handle_touch_motion(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, touch_motion_listener);
+    let event = data as *mut ffi::wlr_touch_motion_event;
+
+    let seat = &mut *cursor.seat;
+    seat.handle_activity();
+
+    if cursor.touch_points.contains_key(&(*event).touch_id) {
+        let wlr_device = &mut (*(*event).touch).base as *mut ffi::wlr_input_device;
+        let mut lx: f64 = 0.0;
+        let mut ly: f64 = 0.0;
+        ffi::wlr_cursor_absolute_to_layout_coords(
+            cursor.wlr_cursor,
+            wlr_device,
+            (*event).x,
+            (*event).y,
+            &mut lx,
+            &mut ly,
+        );
+
+        cursor.touch_points.insert((*event).touch_id, (lx, ly));
+
+        cursor.update_drag_icons();
+
+        let server = seat.server;
+        if let Some(result) = (*server).scene.at(lx, ly) {
+            ffi::wlr_seat_touch_notify_motion(
+                seat.wlr_seat,
+                (*event).time_msec,
+                (*event).touch_id,
+                result.sx,
+                result.sy,
+            );
+        }
+    }
+}
+
+unsafe extern "C" fn handle_touch_up(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, touch_up_listener);
+    let event = data as *mut ffi::wlr_touch_up_event;
+
+    let seat = &mut *cursor.seat;
+    seat.handle_activity();
+
+    if cursor.touch_points.remove(&(*event).touch_id).is_some() {
+        ffi::wlr_seat_touch_notify_up(
+            seat.wlr_seat,
+            (*event).time_msec,
+            (*event).touch_id,
+        );
+    }
+}
+
+unsafe extern "C" fn handle_touch_cancel(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, touch_cancel_listener);
+    
+    let seat = &mut *cursor.seat;
+    seat.handle_activity();
+
+    cursor.touch_points.clear();
+
+    ffi::river_wlr_seat_touch_cancel_all(seat.wlr_seat);
+}
+
+unsafe extern "C" fn handle_touch_frame(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, touch_frame_listener);
+    
+    let seat = &mut *cursor.seat;
+    seat.handle_activity();
+
+    ffi::wlr_seat_touch_notify_frame(seat.wlr_seat);
+}
+
+unsafe extern "C" fn handle_swipe_begin(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, swipe_begin_listener);
+    let event = data as *mut ffi::wlr_pointer_swipe_begin_event;
+
+    let seat = &mut *cursor.seat;
+    seat.handle_activity();
+
+    let server = seat.server;
+    let pointer_gestures = (*server).input_manager.pointer_gestures;
+    if !pointer_gestures.is_null() {
+        ffi::wlr_pointer_gestures_v1_send_swipe_begin(
+            pointer_gestures,
+            seat.wlr_seat,
+            (*event).time_msec,
+            (*event).fingers,
+        );
+    }
+}
+
+unsafe extern "C" fn handle_swipe_update(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, swipe_update_listener);
+    let event = data as *mut ffi::wlr_pointer_swipe_update_event;
+
+    let seat = &mut *cursor.seat;
+    seat.handle_activity();
+
+    let server = seat.server;
+    let pointer_gestures = (*server).input_manager.pointer_gestures;
+    if !pointer_gestures.is_null() {
+        ffi::wlr_pointer_gestures_v1_send_swipe_update(
+            pointer_gestures,
+            seat.wlr_seat,
+            (*event).time_msec,
+            (*event).dx,
+            (*event).dy,
+        );
+    }
+}
+
+unsafe extern "C" fn handle_swipe_end(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, swipe_end_listener);
+    let event = data as *mut ffi::wlr_pointer_swipe_end_event;
+
+    let seat = &mut *cursor.seat;
+    seat.handle_activity();
+
+    let server = seat.server;
+    let pointer_gestures = (*server).input_manager.pointer_gestures;
+    if !pointer_gestures.is_null() {
+        ffi::wlr_pointer_gestures_v1_send_swipe_end(
+            pointer_gestures,
+            seat.wlr_seat,
+            (*event).time_msec,
+            (*event).cancelled,
+        );
+    }
+}
+
+unsafe extern "C" fn handle_pinch_begin(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, pinch_begin_listener);
+    let event = data as *mut ffi::wlr_pointer_pinch_begin_event;
+
+    let seat = &mut *cursor.seat;
+    seat.handle_activity();
+
+    let server = seat.server;
+    let pointer_gestures = (*server).input_manager.pointer_gestures;
+    if !pointer_gestures.is_null() {
+        ffi::wlr_pointer_gestures_v1_send_pinch_begin(
+            pointer_gestures,
+            seat.wlr_seat,
+            (*event).time_msec,
+            (*event).fingers,
+        );
+    }
+}
+
+unsafe extern "C" fn handle_pinch_update(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, pinch_update_listener);
+    let event = data as *mut ffi::wlr_pointer_pinch_update_event;
+
+    let seat = &mut *cursor.seat;
+    seat.handle_activity();
+
+    let server = seat.server;
+    let pointer_gestures = (*server).input_manager.pointer_gestures;
+    if !pointer_gestures.is_null() {
+        ffi::wlr_pointer_gestures_v1_send_pinch_update(
+            pointer_gestures,
+            seat.wlr_seat,
+            (*event).time_msec,
+            (*event).dx,
+            (*event).dy,
+            (*event).scale,
+            (*event).rotation,
+        );
+    }
+}
+
+unsafe extern "C" fn handle_pinch_end(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, pinch_end_listener);
+    let event = data as *mut ffi::wlr_pointer_pinch_end_event;
+
+    let seat = &mut *cursor.seat;
+    seat.handle_activity();
+
+    let server = seat.server;
+    let pointer_gestures = (*server).input_manager.pointer_gestures;
+    if !pointer_gestures.is_null() {
+        ffi::wlr_pointer_gestures_v1_send_pinch_end(
+            pointer_gestures,
+            seat.wlr_seat,
+            (*event).time_msec,
+            (*event).cancelled,
+        );
+    }
+}
+
+unsafe extern "C" fn handle_hold_begin(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, hold_begin_listener);
+    let event = data as *mut ffi::wlr_pointer_hold_begin_event;
+
+    let seat = &mut *cursor.seat;
+    seat.handle_activity();
+
+    let server = seat.server;
+    let pointer_gestures = (*server).input_manager.pointer_gestures;
+    if !pointer_gestures.is_null() {
+        ffi::wlr_pointer_gestures_v1_send_hold_begin(
+            pointer_gestures,
+            seat.wlr_seat,
+            (*event).time_msec,
+            (*event).fingers,
+        );
+    }
+}
+
+unsafe extern "C" fn handle_hold_end(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let cursor = &mut *crate::container_of!(listener, Cursor, hold_end_listener);
+    let event = data as *mut ffi::wlr_pointer_hold_end_event;
+
+    let seat = &mut *cursor.seat;
+    seat.handle_activity();
+
+    let server = seat.server;
+    let pointer_gestures = (*server).input_manager.pointer_gestures;
+    if !pointer_gestures.is_null() {
+        ffi::wlr_pointer_gestures_v1_send_hold_end(
+            pointer_gestures,
+            seat.wlr_seat,
+            (*event).time_msec,
+            (*event).cancelled,
+        );
+    }
+}
+
diff --git a/src/drag_icon.rs b/src/drag_icon.rs
new file mode 100644
index 0000000..67422a9
--- /dev/null
+++ b/src/drag_icon.rs
@@ -0,0 +1,92 @@
+// SPDX-FileCopyrightText: © 2024 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::cursor::Cursor;
+use crate::server::{WlListener, wl_listener_remove, wl_signal_add};
+
+#[repr(C)]
+pub struct DragIcon {
+    pub wlr_drag_icon: *mut ffi::wlr_drag_icon,
+    pub scene_drag_icon: *mut ffi::wlr_scene_tree,
+    pub destroy: ffi::wl_listener,
+}
+
+impl DragIcon {
+    pub unsafe fn create(
+        wlr_drag_icon: *mut ffi::wlr_drag_icon,
+        cursor: *mut Cursor,
+    ) -> Result<(), &'static str> {
+        let server = (*(*cursor).seat).server;
+        
+        let scene_drag_icon = ffi::wlr_scene_drag_icon_create((*server).scene.drag_icons, wlr_drag_icon);
+        if scene_drag_icon.is_null() {
+            return Err("Failed to create scene drag icon");
+        }
+
+        let drag_icon = Box::new(Self {
+            wlr_drag_icon,
+            scene_drag_icon,
+            destroy: std::mem::zeroed(),
+        });
+        let raw = Box::into_raw(drag_icon);
+
+        ffi::river_scene_node_set_data(
+            scene_drag_icon as *mut ffi::wlr_scene_node,
+            raw as *mut std::ffi::c_void,
+        );
+
+        let drag_icon_ref = &mut *raw;
+        drag_icon_ref.update_position(cursor);
+
+        let destroy_listener = &mut drag_icon_ref.destroy as *mut ffi::wl_listener as *mut WlListener;
+        (*destroy_listener).notify = Some(handle_destroy);
+        wl_signal_add(
+            &mut (*wlr_drag_icon).events.destroy as *mut ffi::wl_signal,
+            &mut drag_icon_ref.destroy,
+        );
+
+        Ok(())
+    }
+
+    pub unsafe fn update_position(&mut self, cursor: *mut Cursor) {
+        let grab_type = ffi::river_wlr_drag_get_grab_type((*self.wlr_drag_icon).drag);
+        match grab_type {
+            ffi::wlr_drag_grab_type_WLR_DRAG_GRAB_KEYBOARD => {
+                // unreachable
+            }
+            ffi::wlr_drag_grab_type_WLR_DRAG_GRAB_KEYBOARD_POINTER => {
+                let x = (*cursor).x();
+                let y = (*cursor).y();
+                ffi::wlr_scene_node_set_position(
+                    self.scene_drag_icon as *mut ffi::wlr_scene_node,
+                    x as i32,
+                    y as i32,
+                );
+            }
+            ffi::wlr_drag_grab_type_WLR_DRAG_GRAB_KEYBOARD_TOUCH => {
+                let touch_id = ffi::river_wlr_drag_get_touch_id((*self.wlr_drag_icon).drag);
+                if let Some(&(lx, ly)) = (*cursor).touch_points.get(&touch_id) {
+                    ffi::wlr_scene_node_set_position(
+                        self.scene_drag_icon as *mut ffi::wlr_scene_node,
+                        lx as i32,
+                        ly as i32,
+                    );
+                }
+            }
+            _ => {}
+        }
+    }
+}
+
+unsafe extern "C" fn handle_destroy(
+    listener: *mut ffi::wl_listener,
+    _data: *mut std::ffi::c_void,
+) {
+    let drag_icon_ptr = crate::container_of!(listener, DragIcon, destroy);
+    let mut drag_icon = Box::from_raw(drag_icon_ptr);
+    wl_listener_remove(&mut drag_icon.destroy);
+
+    // Destroy the scene node hierarchy.
+    ffi::wlr_scene_node_destroy(drag_icon.scene_drag_icon as *mut ffi::wlr_scene_node);
+}
diff --git a/src/ffi.rs b/src/ffi.rs
new file mode 100644
index 0000000..acbd2c3
--- /dev/null
+++ b/src/ffi.rs
@@ -0,0 +1,19 @@
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+#![allow(dead_code)]
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct wl_listener {
+    pub link: wl_list,
+    pub notify: ::std::option::Option<
+        unsafe extern "C" fn(listener: *mut wl_listener, data: *mut ::std::os::raw::c_void),
+    >,
+}
+
+include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
+
+extern "C" {
+    pub fn river_init_wlroots_log(importance: wlr_log_importance);
+}
diff --git a/src/idle_inhibit_manager.rs b/src/idle_inhibit_manager.rs
new file mode 100644
index 0000000..e2d9d27
--- /dev/null
+++ b/src/idle_inhibit_manager.rs
@@ -0,0 +1,144 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::server::{Server, WlListener, wl_signal_add, wl_listener_remove};
+use crate::scene_node_data::SceneNodeData;
+
+pub struct IdleInhibitManager {
+    pub wlr_manager: *mut ffi::wlr_idle_inhibit_manager_v1,
+    pub new_idle_inhibitor: ffi::wl_listener,
+    pub inhibitors: ffi::wl_list,
+    pub server: *mut Server,
+}
+
+impl IdleInhibitManager {
+    pub unsafe fn init(&mut self, server: *mut Server) -> Result<(), &'static str> {
+        self.server = server;
+        ffi::wl_list_init(&mut self.inhibitors);
+
+        let wlr_manager = ffi::wlr_idle_inhibit_v1_create((*server).wl_server);
+        if wlr_manager.is_null() {
+            return Err("Failed to create wlr_idle_inhibit_manager_v1");
+        }
+        self.wlr_manager = wlr_manager;
+
+        let new_inhibitor_listener = &mut self.new_idle_inhibitor as *mut ffi::wl_listener as *mut WlListener;
+        (*new_inhibitor_listener).notify = Some(handle_new_idle_inhibitor);
+        wl_signal_add(
+            &mut (*self.wlr_manager).events.new_inhibitor,
+            &mut self.new_idle_inhibitor,
+        );
+
+        Ok(())
+    }
+
+    pub unsafe fn deinit(&mut self) {
+        wl_listener_remove(&mut self.new_idle_inhibitor);
+        
+        let inhibitors_head = &mut self.inhibitors as *mut ffi::wl_list as *mut crate::server::WlList;
+        let mut curr = (*inhibitors_head).next;
+        while curr != inhibitors_head {
+            let next = (*curr).next;
+            let inhibitor = crate::container_of!(curr, IdleInhibitor, link);
+            IdleInhibitor::destroy(inhibitor);
+            curr = next;
+        }
+    }
+
+    pub unsafe fn check_active(&self) {
+        let mut inhibited = false;
+        
+        let inhibitors_head = &self.inhibitors as *const ffi::wl_list as *mut crate::server::WlList;
+        let mut curr = (*inhibitors_head).next;
+        while curr != inhibitors_head {
+            let next = (*curr).next;
+            let inhibitor = crate::container_of!(curr, IdleInhibitor, link);
+            
+            let surface = (*(*inhibitor).wlr_inhibitor).surface;
+            if let Some(node_data) = SceneNodeData::from_surface(surface) {
+                match node_data.data {
+                    crate::scene_node_data::SceneNodeDataVal::Window(_) |
+                    crate::scene_node_data::SceneNodeDataVal::ShellSurface(_) |
+                    crate::scene_node_data::SceneNodeDataVal::LockSurface(_) |
+                    crate::scene_node_data::SceneNodeDataVal::LayerSurface(_) |
+                    crate::scene_node_data::SceneNodeDataVal::OverrideRedirect(_) => {
+                        inhibited = true;
+                        break;
+                    }
+                }
+            }
+            curr = next;
+        }
+
+        let notifier = (*self.server).input_manager.idle_notifier;
+        if !notifier.is_null() {
+            ffi::wlr_idle_notifier_v1_set_inhibited(notifier, inhibited);
+        }
+    }
+}
+
+pub struct IdleInhibitor {
+    pub inhibit_manager: *mut IdleInhibitManager,
+    pub wlr_inhibitor: *mut ffi::wlr_idle_inhibitor_v1,
+    pub listen_destroy: ffi::wl_listener,
+    pub link: ffi::wl_list,
+}
+
+impl IdleInhibitor {
+    pub unsafe fn create(
+        wlr_inhibitor: *mut ffi::wlr_idle_inhibitor_v1,
+        inhibit_manager: *mut IdleInhibitManager,
+    ) -> Result<*mut Self, &'static str> {
+        let inhibitor = Box::into_raw(Box::new(Self {
+            inhibit_manager,
+            wlr_inhibitor,
+            listen_destroy: std::mem::zeroed(),
+            link: std::mem::zeroed(),
+        }));
+
+        let destroy_listener = &mut (*inhibitor).listen_destroy as *mut ffi::wl_listener as *mut WlListener;
+        (*destroy_listener).notify = Some(handle_inhibitor_destroy);
+        wl_signal_add(
+            &mut (*wlr_inhibitor).events.destroy,
+            &mut (*inhibitor).listen_destroy,
+        );
+
+        let list_head = &mut (*inhibit_manager).inhibitors as *mut ffi::wl_list as *mut crate::server::WlList;
+        crate::server::wl_list_insert((*list_head).prev, &mut (*inhibitor).link as *mut ffi::wl_list as *mut crate::server::WlList);
+
+        (*inhibit_manager).check_active();
+
+        Ok(inhibitor)
+    }
+
+    pub unsafe fn destroy(inhibitor: *mut Self) {
+        wl_listener_remove(&mut (*inhibitor).listen_destroy);
+        crate::server::wl_list_remove(&mut (*inhibitor).link as *mut ffi::wl_list as *mut crate::server::WlList);
+        
+        let manager = (*inhibitor).inhibit_manager;
+        let _boxed = Box::from_raw(inhibitor);
+        
+        (*manager).check_active();
+    }
+}
+
+unsafe extern "C" fn handle_new_idle_inhibitor(
+    listener: *mut ffi::wl_listener,
+    data: *mut std::ffi::c_void,
+) {
+    let manager = &mut *crate::container_of!(listener, IdleInhibitManager, new_idle_inhibitor);
+    let inhibitor = data as *mut ffi::wlr_idle_inhibitor_v1;
+
+    if IdleInhibitor::create(inhibitor, manager).is_err() {
+        log::error!("failed to create idle inhibitor");
+    }
+}
+
+unsafe extern "C" fn handle_inhibitor_destroy(
+    listener: *mut ffi::wl_listener,
+    _data: *mut std::ffi::c_void,
+) {
+    let inhibitor = crate::container_of!(listener, IdleInhibitor, listen_destroy);
+    IdleInhibitor::destroy(inhibitor);
+}
diff --git a/src/input_device.rs b/src/input_device.rs
new file mode 100644
index 0000000..40d455e
--- /dev/null
+++ b/src/input_device.rs
@@ -0,0 +1,102 @@
+use crate::ffi;
+use crate::seat::Seat;
+use crate::server::{WlListener, wl_listener_remove};
+
+pub struct InputDeviceConfig {
+    pub scroll_factor: f64,
+    pub map_to_output: *mut ffi::wlr_output,
+    pub map_to_rectangle: ffi::wlr_box,
+}
+
+pub struct InputDevice {
+    pub seat: *mut Seat,
+    pub wlr_device: *mut ffi::wlr_input_device,
+    pub virtual_device: bool,
+    pub destroy_listener: ffi::wl_listener,
+    pub config: InputDeviceConfig,
+    pub destroy_fn: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
+    pub destroy_data: *mut std::ffi::c_void,
+}
+
+impl InputDevice {
+    pub unsafe fn new(
+        seat: *mut Seat,
+        wlr_device: *mut ffi::wlr_input_device,
+        virtual_device: bool,
+    ) -> *mut Self {
+        let device = Box::into_raw(Box::new(Self {
+            seat,
+            wlr_device,
+            virtual_device,
+            destroy_listener: std::mem::zeroed(),
+            config: InputDeviceConfig {
+                scroll_factor: 1.0,
+                map_to_output: std::ptr::null_mut(),
+                map_to_rectangle: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
+            },
+            destroy_fn: None,
+            destroy_data: std::ptr::null_mut(),
+        }));
+
+        ffi::river_wlr_input_device_set_data(wlr_device, device as *mut _);
+
+        let destroy_listener_ptr = &mut (*device).destroy_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*destroy_listener_ptr).notify = Some(handle_device_destroy);
+        
+        let destroy_signal = ffi::river_wlr_input_device_get_destroy_signal(wlr_device);
+        crate::server::wl_signal_add(destroy_signal, &mut (*device).destroy_listener);
+
+        device
+    }
+
+    pub unsafe fn assign_to_seat(&mut self, new_seat: *mut Seat) {
+        if self.seat == new_seat {
+            return;
+        }
+        if !self.seat.is_null() {
+            (*self.seat).detach_device(self);
+        }
+        self.seat = new_seat;
+        if !self.seat.is_null() {
+            (*self.seat).attach_device(self);
+        }
+    }
+
+    pub unsafe fn active_mapping(&self) -> ffi::wlr_box {
+        let mut mapping = self.config.map_to_rectangle;
+        if !ffi::wlr_box_empty(&mapping) {
+            return mapping;
+        }
+        if !self.config.map_to_output.is_null() {
+            let server = (*self.seat).server;
+            ffi::wlr_output_layout_get_box((*server).om.output_layout, self.config.map_to_output, &mut mapping);
+        }
+        mapping
+    }
+}
+
+unsafe extern "C" fn handle_device_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let device_ptr = crate::container_of!(listener, InputDevice, destroy_listener) as *mut InputDevice;
+    let device = &mut *device_ptr;
+    
+    log::debug!(
+        "removed input device: {:?}",
+        ffi::river_wlr_input_device_get_type(device.wlr_device)
+    );
+
+    // Call custom destroy callback if set (e.g. to clean up Tablet/Keyboard wrappers)
+    if let Some(destroy_fn) = device.destroy_fn {
+        destroy_fn(device.destroy_data);
+    }
+
+    // Remove destroy listener
+    wl_listener_remove(&mut device.destroy_listener);
+
+    // Detach from seat if attached
+    if !device.seat.is_null() {
+        (*device.seat).detach_device(device);
+    }
+
+    // Free wrapper memory
+    let _boxed = Box::from_raw(device_ptr);
+}
diff --git a/src/input_manager.rs b/src/input_manager.rs
new file mode 100644
index 0000000..b6db300
--- /dev/null
+++ b/src/input_manager.rs
@@ -0,0 +1,120 @@
+// SPDX-FileCopyrightText: © 2020 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::server::{Server, wl_signal_add, WlListener, wl_listener_remove};
+use crate::seat::Seat;
+
+pub struct InputManager {
+    pub server: *mut Server,
+    pub default_seat: *mut Seat,
+    pub seats: ffi::wl_list,
+    
+    pub idle_notifier: *mut ffi::wlr_idle_notifier_v1,
+    pub relative_pointer_manager: *mut ffi::wlr_relative_pointer_manager_v1,
+    pub pointer_gestures: *mut ffi::wlr_pointer_gestures_v1,
+    pub virtual_pointer_manager: *mut ffi::wlr_virtual_pointer_manager_v1,
+    pub virtual_keyboard_manager: *mut ffi::wlr_virtual_keyboard_manager_v1,
+    pub pointer_constraints: *mut ffi::wlr_pointer_constraints_v1,
+    pub input_method_manager: *mut ffi::wlr_input_method_manager_v2,
+    pub text_input_manager: *mut ffi::wlr_text_input_manager_v3,
+    pub tablet_manager: *mut ffi::wlr_tablet_manager_v2,
+
+    pub new_input_listener: ffi::wl_listener,
+    pub new_text_input: ffi::wl_listener,
+    pub new_input_method: ffi::wl_listener,
+}
+
+impl Default for InputManager {
+    fn default() -> Self {
+        unsafe { std::mem::zeroed() }
+    }
+}
+
+impl InputManager {
+    pub unsafe fn init(&mut self, server: *mut Server) -> Result<(), &'static str> {
+        self.server = server;
+        let wl_server = (*server).wl_server;
+
+        ffi::wl_list_init(&mut self.seats);
+
+        self.idle_notifier = ffi::wlr_idle_notifier_v1_create(wl_server);
+        self.relative_pointer_manager = ffi::wlr_relative_pointer_manager_v1_create(wl_server);
+        self.pointer_gestures = ffi::wlr_pointer_gestures_v1_create(wl_server);
+        self.virtual_pointer_manager = ffi::wlr_virtual_pointer_manager_v1_create(wl_server);
+        self.virtual_keyboard_manager = ffi::wlr_virtual_keyboard_manager_v1_create(wl_server);
+        self.pointer_constraints = ffi::wlr_pointer_constraints_v1_create(wl_server);
+        self.input_method_manager = ffi::wlr_input_method_manager_v2_create(wl_server);
+        self.text_input_manager = ffi::wlr_text_input_manager_v3_create(wl_server);
+        self.tablet_manager = ffi::wlr_tablet_v2_create(wl_server);
+
+        // Create default seat
+        self.default_seat = Seat::create(server, "default")?;
+
+        let new_input_ptr = &mut self.new_input_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*new_input_ptr).notify = Some(handle_new_input);
+        
+        let signal = ffi::river_wlr_backend_get_new_input_signal((*server).backend);
+        wl_signal_add(signal, &mut self.new_input_listener);
+
+        // Connect new_text_input listener
+        let new_text_input_ptr = &mut self.new_text_input as *mut ffi::wl_listener as *mut WlListener;
+        (*new_text_input_ptr).notify = Some(handle_new_text_input);
+        wl_signal_add(&mut (*self.text_input_manager).events.new_text_input, &mut self.new_text_input);
+
+        // Connect new_input_method listener
+        let new_input_method_ptr = &mut self.new_input_method as *mut ffi::wl_listener as *mut WlListener;
+        (*new_input_method_ptr).notify = Some(handle_new_input_method);
+        wl_signal_add(&mut (*self.input_method_manager).events.new_input_method, &mut self.new_input_method);
+
+        Ok(())
+    }
+
+    pub unsafe fn deinit(&mut self) {
+        if !self.default_seat.is_null() {
+            Seat::destroy(self.default_seat);
+            self.default_seat = std::ptr::null_mut();
+        }
+        wl_listener_remove(&mut self.new_input_listener);
+        wl_listener_remove(&mut self.new_text_input);
+        wl_listener_remove(&mut self.new_input_method);
+    }
+}
+
+unsafe extern "C" fn handle_new_input(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let im = &mut *crate::container_of!(listener, InputManager, new_input_listener);
+    let wlr_device = data as *mut ffi::wlr_input_device;
+
+    log::info!("new input device connected");
+
+    let device = crate::input_device::InputDevice::new(im.default_seat, wlr_device, false);
+    
+    let dev_type = ffi::river_wlr_input_device_get_type(wlr_device);
+    if dev_type == ffi::wlr_input_device_type_WLR_INPUT_DEVICE_KEYBOARD {
+        crate::keyboard::Keyboard::create(device);
+    } else if dev_type == ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TABLET {
+        let _ = crate::tablet::Tablet::create(im.default_seat, wlr_device, false);
+    }
+}
+
+unsafe extern "C" fn handle_new_text_input(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let _im = crate::container_of!(listener, InputManager, new_text_input);
+    let wlr_text_input = data as *mut ffi::wlr_text_input_v3;
+
+    if let Err(e) = crate::text_input::TextInput::create(wlr_text_input) {
+        log::error!("failed to create text input: {}", e);
+        ffi::wl_resource_post_no_memory((*wlr_text_input).resource);
+    }
+}
+
+unsafe extern "C" fn handle_new_input_method(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let im = &mut *crate::container_of!(listener, InputManager, new_input_method);
+    let input_method = data as *mut ffi::wlr_input_method_v2;
+    let seat = ffi::river_wlr_seat_get_data((*input_method).seat) as *mut Seat;
+    if !seat.is_null() {
+        (*seat).relay.new_input_method(input_method);
+    } else {
+        // Fallback to default seat if input_method.seat has no data set yet
+        (*im.default_seat).relay.new_input_method(input_method);
+    }
+}
diff --git a/src/input_popup.rs b/src/input_popup.rs
new file mode 100644
index 0000000..c07e908
--- /dev/null
+++ b/src/input_popup.rs
@@ -0,0 +1,231 @@
+// SPDX-FileCopyrightText: © 2024 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::server::{WlListener, wl_listener_remove, wl_signal_add, WlList};
+use crate::input_relay::InputRelay;
+use crate::scene_node_data::{SceneNodeData, SceneNodeDataVal};
+
+#[repr(C)]
+pub struct InputPopup {
+    pub link: ffi::wl_list,
+    pub input_relay: *mut InputRelay,
+    pub wlr_popup: *mut ffi::wlr_input_popup_surface_v2,
+    pub surface_tree: *mut ffi::wlr_scene_tree,
+
+    pub destroy: ffi::wl_listener,
+    pub map: ffi::wl_listener,
+    pub unmap: ffi::wl_listener,
+    pub commit: ffi::wl_listener,
+}
+
+unsafe fn connect_listener(
+    signal: *mut ffi::wl_signal,
+    listener: *mut ffi::wl_listener,
+    callback: unsafe extern "C" fn(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void),
+) {
+    let wl_lis = listener as *mut WlListener;
+    (*wl_lis).notify = Some(callback);
+    wl_signal_add(signal, listener);
+}
+
+unsafe fn wl_listener_remove_safe(listener: *mut ffi::wl_listener) {
+    let prev = (*listener).link.prev;
+    let next = (*listener).link.next;
+    if !prev.is_null() && !next.is_null() && prev != listener as *mut ffi::wl_list && next != listener as *mut ffi::wl_list {
+        ffi::wl_list_remove(&mut (*listener).link);
+        (*listener).link.prev = std::ptr::null_mut();
+        (*listener).link.next = std::ptr::null_mut();
+    }
+}
+
+impl InputPopup {
+    pub unsafe fn create(
+        wlr_popup: *mut ffi::wlr_input_popup_surface_v2,
+        input_relay: *mut InputRelay,
+    ) -> Result<(), &'static str> {
+        let server = (*(*input_relay).seat).server;
+        let hidden_tree = (*server).scene.hidden_tree;
+
+        let surface_tree = ffi::wlr_scene_subsurface_tree_create(hidden_tree, (*wlr_popup).surface);
+        if surface_tree.is_null() {
+            return Err("Failed to create subsurface tree for input popup");
+        }
+
+        let mut input_popup = Box::new(InputPopup {
+            link: std::mem::zeroed(),
+            input_relay,
+            wlr_popup,
+            surface_tree,
+            destroy: std::mem::zeroed(),
+            map: std::mem::zeroed(),
+            unmap: std::mem::zeroed(),
+            commit: std::mem::zeroed(),
+        });
+
+        ffi::wl_list_init(&mut input_popup.link);
+
+        let raw = Box::into_raw(input_popup);
+
+        // Append to input_relay.input_popups
+        let popups_list = &mut (*input_relay).input_popups as *mut ffi::wl_list as *mut WlList;
+        crate::server::wl_list_insert((*popups_list).prev, &mut (*raw).link as *mut ffi::wl_list as *mut WlList);
+
+        connect_listener(&mut (*wlr_popup).events.destroy, &mut (*raw).destroy, handle_destroy);
+        connect_listener(
+            ffi::river_wlr_surface_get_map_signal((*wlr_popup).surface),
+            &mut (*raw).map,
+            handle_map,
+        );
+        connect_listener(
+            ffi::river_wlr_surface_get_unmap_signal((*wlr_popup).surface),
+            &mut (*raw).unmap,
+            handle_unmap,
+        );
+        connect_listener(
+            ffi::river_wlr_surface_get_commit_signal((*wlr_popup).surface),
+            &mut (*raw).commit,
+            handle_commit,
+        );
+
+        (*raw).update();
+
+        Ok(())
+    }
+
+    pub unsafe fn update(&mut self) {
+        let text_input = (*self.input_relay).text_input;
+        if text_input.is_null() {
+            let server = (*(*self.input_relay).seat).server;
+            let hidden_tree = (*server).scene.hidden_tree;
+            ffi::wlr_scene_node_reparent(
+                self.surface_tree as *mut ffi::wlr_scene_node,
+                hidden_tree,
+            );
+            return;
+        }
+
+        if !ffi::river_wlr_surface_is_mapped((*self.wlr_popup).surface) {
+            return;
+        }
+
+        let focused_surface = (*(*text_input).wlr_text_input).focused_surface;
+        if focused_surface.is_null() {
+            return;
+        }
+
+        assert_eq!(ffi::wlr_surface_get_root_surface(focused_surface), focused_surface);
+
+        let focused = match SceneNodeData::from_surface(focused_surface) {
+            Some(f) => f,
+            None => return,
+        };
+
+        let server = (*(*self.input_relay).seat).server;
+
+        let popup_tree = match focused.data {
+            SceneNodeDataVal::Window(window) => (*window).popup_tree,
+            SceneNodeDataVal::ShellSurface(shell_surface) => (*shell_surface).popup_tree,
+            SceneNodeDataVal::LockSurface(_) => (*server).scene.layers.popups,
+            SceneNodeDataVal::LayerSurface(layer_surface) => (*layer_surface).popup_tree,
+            SceneNodeDataVal::OverrideRedirect(_) => panic!("Xwayland doesn't use text-input protocol"),
+        };
+
+        ffi::wlr_scene_node_reparent(self.surface_tree as *mut ffi::wlr_scene_node, popup_tree);
+
+        // cursor_rectangle features check: check if WLR_TEXT_INPUT_V3_FEATURE_CURSOR_RECTANGLE is active
+        let active_features = (*(*text_input).wlr_text_input).active_features;
+        let feature_cursor_rect = ffi::wlr_text_input_v3_features_WLR_TEXT_INPUT_V3_FEATURE_CURSOR_RECTANGLE;
+        if (active_features & feature_cursor_rect) == 0 {
+            ffi::wlr_scene_node_set_position(self.surface_tree as *mut ffi::wlr_scene_node, 0, 0);
+            return;
+        }
+
+        let mut focused_x: std::os::raw::c_int = 0;
+        let mut focused_y: std::os::raw::c_int = 0;
+        ffi::wlr_scene_node_coords(focused.node, &mut focused_x, &mut focused_y);
+
+        let mut cursor_box = (*(*text_input).wlr_text_input).current.cursor_rectangle;
+
+        let wlr_output = (*server).om.output_at(
+            (focused_x + cursor_box.x) as f64,
+            (focused_y + cursor_box.y) as f64,
+        );
+        if wlr_output.is_null() {
+            return;
+        }
+
+        let mut output_box = ffi::wlr_box {
+            x: 0,
+            y: 0,
+            width: 0,
+            height: 0,
+        };
+        ffi::wlr_output_layout_get_box((*server).om.output_layout, wlr_output, &mut output_box);
+
+        cursor_box.x += focused_x - output_box.x;
+        cursor_box.y += focused_y - output_box.y;
+
+        let popup_width = ffi::river_wlr_surface_get_width((*self.wlr_popup).surface);
+        let popup_height = ffi::river_wlr_surface_get_height((*self.wlr_popup).surface);
+
+        let popup_x = if output_box.width - cursor_box.x >= popup_width {
+            cursor_box.x
+        } else {
+            cursor_box.x + cursor_box.width - popup_width
+        };
+
+        let popup_y = if output_box.height - (cursor_box.y + cursor_box.height) >= popup_height {
+            cursor_box.y + cursor_box.height
+        } else {
+            cursor_box.y - popup_height
+        };
+
+        ffi::wlr_scene_node_set_position(
+            self.surface_tree as *mut ffi::wlr_scene_node,
+            popup_x - focused_x + output_box.x,
+            popup_y - focused_y + output_box.y,
+        );
+
+        cursor_box.x -= popup_x;
+        cursor_box.y -= popup_y;
+        ffi::wlr_input_popup_surface_v2_send_text_input_rectangle(self.wlr_popup, &mut cursor_box);
+    }
+}
+
+unsafe extern "C" fn handle_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let input_popup = crate::container_of!(listener, InputPopup, destroy);
+
+    wl_listener_remove_safe(&mut (*input_popup).destroy);
+    wl_listener_remove_safe(&mut (*input_popup).map);
+    wl_listener_remove_safe(&mut (*input_popup).unmap);
+    wl_listener_remove_safe(&mut (*input_popup).commit);
+
+    ffi::wl_list_remove(&mut (*input_popup).link);
+
+    let _ = Box::from_raw(input_popup);
+}
+
+unsafe extern "C" fn handle_map(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let input_popup = crate::container_of!(listener, InputPopup, map);
+    (*input_popup).update();
+}
+
+unsafe extern "C" fn handle_unmap(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let input_popup = crate::container_of!(listener, InputPopup, unmap);
+    let server = (*(*(*input_popup).input_relay).seat).server;
+    let hidden_tree = (*server).scene.hidden_tree;
+
+    ffi::wlr_scene_node_reparent(
+        (*input_popup).surface_tree as *mut ffi::wlr_scene_node,
+        hidden_tree,
+    );
+}
+
+unsafe extern "C" fn handle_commit(
+    listener: *mut ffi::wl_listener,
+    _data: *mut std::ffi::c_void,
+) {
+    let input_popup = crate::container_of!(listener, InputPopup, commit);
+    (*input_popup).update();
+}
diff --git a/src/input_relay.rs b/src/input_relay.rs
new file mode 100644
index 0000000..7ada558
--- /dev/null
+++ b/src/input_relay.rs
@@ -0,0 +1,283 @@
+// SPDX-FileCopyrightText: © 2021 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::server::{WlListener, wl_listener_remove, wl_signal_add};
+use crate::text_input::TextInput;
+use crate::seat::Seat;
+
+#[repr(C)]
+pub struct InputRelay {
+    pub seat: *mut Seat,
+    pub text_inputs: ffi::wl_list,
+    pub input_method: *mut ffi::wlr_input_method_v2,
+    pub input_popups: ffi::wl_list,
+    pub text_input: *mut TextInput,
+
+    pub input_method_commit: ffi::wl_listener,
+    pub grab_keyboard: ffi::wl_listener,
+    pub input_method_destroy: ffi::wl_listener,
+    pub input_method_new_popup: ffi::wl_listener,
+
+    pub grab_keyboard_destroy: ffi::wl_listener,
+}
+
+unsafe fn connect_listener(
+    signal: *mut ffi::wl_signal,
+    listener: *mut ffi::wl_listener,
+    callback: unsafe extern "C" fn(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void),
+) {
+    let wl_lis = listener as *mut WlListener;
+    (*wl_lis).notify = Some(callback);
+    wl_signal_add(signal, listener);
+}
+
+unsafe fn wl_listener_remove_safe(listener: *mut ffi::wl_listener) {
+    let prev = (*listener).link.prev;
+    let next = (*listener).link.next;
+    if !prev.is_null() && !next.is_null() && prev != listener as *mut ffi::wl_list && next != listener as *mut ffi::wl_list {
+        ffi::wl_list_remove(&mut (*listener).link);
+        (*listener).link.prev = std::ptr::null_mut();
+        (*listener).link.next = std::ptr::null_mut();
+    }
+}
+
+impl InputRelay {
+    pub unsafe fn init(&mut self, seat: *mut Seat) {
+        self.seat = seat;
+        ffi::wl_list_init(&mut self.text_inputs);
+        ffi::wl_list_init(&mut self.input_popups);
+        self.input_method = std::ptr::null_mut();
+        self.text_input = std::ptr::null_mut();
+        self.input_method_commit = std::mem::zeroed();
+        self.grab_keyboard = std::mem::zeroed();
+        self.input_method_destroy = std::mem::zeroed();
+        self.input_method_new_popup = std::mem::zeroed();
+        self.grab_keyboard_destroy = std::mem::zeroed();
+    }
+
+    pub unsafe fn new_input_method(&mut self, input_method: *mut ffi::wlr_input_method_v2) {
+        let seat = crate::container_of!(self, Seat, relay);
+        let seat_name = std::ffi::CStr::from_ptr(ffi::river_wlr_seat_get_name((*seat).wlr_seat))
+            .to_str()
+            .unwrap_or("unknown");
+        log::debug!("new input method on seat {}", seat_name);
+
+        if !self.input_method.is_null() {
+            log::info!("seat {} already has an input method", seat_name);
+            ffi::wlr_input_method_v2_send_unavailable(input_method);
+            return;
+        }
+
+        self.input_method = input_method;
+
+        connect_listener(&mut (*input_method).events.commit, &mut self.input_method_commit, handle_input_method_commit);
+        connect_listener(&mut (*input_method).events.grab_keyboard, &mut self.grab_keyboard, handle_input_method_grab_keyboard);
+        connect_listener(&mut (*input_method).events.destroy, &mut self.input_method_destroy, handle_input_method_destroy);
+        connect_listener(&mut (*input_method).events.new_popup_surface, &mut self.input_method_new_popup, handle_input_method_new_popup);
+
+        let focused_surface = (*seat).focused.surface();
+        if !focused_surface.is_null() {
+            self.focus(focused_surface);
+        }
+    }
+
+    pub unsafe fn disable_text_input(&mut self) {
+        assert!(!self.text_input.is_null());
+        self.text_input = std::ptr::null_mut();
+
+        if !self.input_method.is_null() {
+            let mut pos = self.input_popups.next;
+            let head_ptr = &self.input_popups as *const ffi::wl_list;
+            while pos != head_ptr as *mut ffi::wl_list {
+                let next_pos = (*pos).next;
+                let popup = crate::container_of!(pos, crate::input_popup::InputPopup, link);
+                (*popup).update();
+                pos = next_pos;
+            }
+
+            ffi::wlr_input_method_v2_send_deactivate(self.input_method);
+            ffi::wlr_input_method_v2_send_done(self.input_method);
+        }
+    }
+
+    pub unsafe fn send_input_method_state(&mut self) {
+        let input_method = self.input_method;
+        let wlr_text_input = (*self.text_input).wlr_text_input;
+
+        let active_features = (*wlr_text_input).active_features;
+        let feature_surrounding = ffi::wlr_text_input_v3_features_WLR_TEXT_INPUT_V3_FEATURE_SURROUNDING_TEXT;
+        let feature_content = ffi::wlr_text_input_v3_features_WLR_TEXT_INPUT_V3_FEATURE_CONTENT_TYPE;
+
+        if (active_features & feature_surrounding) != 0 {
+            let text = (*wlr_text_input).current.surrounding.text;
+            if !text.is_null() {
+                ffi::wlr_input_method_v2_send_surrounding_text(
+                    input_method,
+                    text,
+                    (*wlr_text_input).current.surrounding.cursor,
+                    (*wlr_text_input).current.surrounding.anchor,
+                );
+            }
+        }
+
+        ffi::wlr_input_method_v2_send_text_change_cause(input_method, (*wlr_text_input).current.text_change_cause);
+
+        if (active_features & feature_content) != 0 {
+            ffi::wlr_input_method_v2_send_content_type(
+                input_method,
+                (*wlr_text_input).current.content_type.hint,
+                (*wlr_text_input).current.content_type.purpose,
+            );
+        }
+
+        let mut pos = self.input_popups.next;
+        let head_ptr = &self.input_popups as *const ffi::wl_list;
+        while pos != head_ptr as *mut ffi::wl_list {
+            let next_pos = (*pos).next;
+            let popup = crate::container_of!(pos, crate::input_popup::InputPopup, link);
+            (*popup).update();
+            pos = next_pos;
+        }
+
+        ffi::wlr_input_method_v2_send_done(input_method);
+    }
+
+    pub unsafe fn focus(&mut self, new_focus: *mut ffi::wlr_surface) {
+        // Send leave events
+        let mut pos = self.text_inputs.next;
+        let head_ptr = &self.text_inputs as *const ffi::wl_list;
+        while pos != head_ptr as *mut ffi::wl_list {
+            let next_pos = (*pos).next;
+            let text_input = crate::container_of!(pos, TextInput, link);
+            let focused = (*(*text_input).wlr_text_input).focused_surface;
+            if !focused.is_null() {
+                assert!(focused != new_focus);
+                ffi::wlr_text_input_v3_send_leave((*text_input).wlr_text_input);
+            }
+            pos = next_pos;
+        }
+
+        // Clear currently enabled text input
+        if !self.text_input.is_null() {
+            self.disable_text_input();
+        }
+
+        // Send enter events if we have an input method
+        if !new_focus.is_null() && !self.input_method.is_null() {
+            let new_client = ffi::wl_resource_get_client(ffi::river_wlr_surface_get_resource(new_focus));
+            let mut pos = self.text_inputs.next;
+            while pos != head_ptr as *mut ffi::wl_list {
+                let next_pos = (*pos).next;
+                let text_input = crate::container_of!(pos, TextInput, link);
+                let text_input_resource = (*(*text_input).wlr_text_input).resource;
+                let client = ffi::wl_resource_get_client(text_input_resource);
+                if client == new_client {
+                    ffi::wlr_text_input_v3_send_enter((*text_input).wlr_text_input, new_focus);
+                }
+                pos = next_pos;
+            }
+        }
+    }
+}
+
+unsafe extern "C" fn handle_input_method_commit(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let relay = crate::container_of!(listener, InputRelay, input_method_commit);
+    let input_method = (*relay).input_method;
+
+    if !(*input_method).client_active {
+        return;
+    }
+
+    let text_input = (*relay).text_input;
+    if text_input.is_null() {
+        return;
+    }
+
+    let preedit_text = (*input_method).current.preedit.text;
+    if !preedit_text.is_null() {
+        ffi::wlr_text_input_v3_send_preedit_string(
+            (*text_input).wlr_text_input,
+            preedit_text,
+            (*input_method).current.preedit.cursor_begin,
+            (*input_method).current.preedit.cursor_end,
+        );
+    }
+
+    let commit_text = (*input_method).current.commit_text;
+    if !commit_text.is_null() {
+        ffi::wlr_text_input_v3_send_commit_string((*text_input).wlr_text_input, commit_text);
+    }
+
+    if (*input_method).current.delete.before_length != 0
+        || (*input_method).current.delete.after_length != 0
+    {
+        ffi::wlr_text_input_v3_send_delete_surrounding_text(
+            (*text_input).wlr_text_input,
+            (*input_method).current.delete.before_length,
+            (*input_method).current.delete.after_length,
+        );
+    }
+
+    ffi::wlr_text_input_v3_send_done((*text_input).wlr_text_input);
+}
+
+unsafe extern "C" fn handle_input_method_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let relay = crate::container_of!(listener, InputRelay, input_method_destroy);
+
+    wl_listener_remove_safe(&mut (*relay).input_method_commit);
+    wl_listener_remove_safe(&mut (*relay).grab_keyboard);
+    wl_listener_remove_safe(&mut (*relay).input_method_destroy);
+    wl_listener_remove_safe(&mut (*relay).input_method_new_popup);
+    (*relay).input_method = std::ptr::null_mut();
+
+    (*relay).focus(std::ptr::null_mut());
+
+    assert!((*relay).text_input.is_null());
+}
+
+unsafe extern "C" fn handle_input_method_grab_keyboard(
+    listener: *mut ffi::wl_listener,
+    data: *mut std::ffi::c_void,
+) {
+    let relay = crate::container_of!(listener, InputRelay, grab_keyboard);
+    let keyboard_grab = data as *mut ffi::wlr_input_method_keyboard_grab_v2;
+    let seat = crate::container_of!(relay, Seat, relay);
+
+    let active_keyboard = ffi::river_wlr_seat_get_keyboard((*seat).wlr_seat);
+    ffi::wlr_input_method_keyboard_grab_v2_set_keyboard(keyboard_grab, active_keyboard);
+
+    connect_listener(
+        ffi::river_wlr_input_method_keyboard_grab_v2_get_destroy_signal(keyboard_grab),
+        &mut (*relay).grab_keyboard_destroy,
+        handle_input_method_grab_keyboard_destroy,
+    );
+}
+
+unsafe extern "C" fn handle_input_method_new_popup(
+    listener: *mut ffi::wl_listener,
+    data: *mut std::ffi::c_void,
+) {
+    let relay = crate::container_of!(listener, InputRelay, input_method_new_popup);
+    let wlr_popup = data as *mut ffi::wlr_input_popup_surface_v2;
+
+    if let Err(e) = crate::input_popup::InputPopup::create(wlr_popup, relay) {
+        log::error!("failed to create input popup: {}", e);
+    }
+}
+
+unsafe extern "C" fn handle_input_method_grab_keyboard_destroy(
+    listener: *mut ffi::wl_listener,
+    _data: *mut std::ffi::c_void,
+) {
+    let relay = crate::container_of!(listener, InputRelay, grab_keyboard_destroy);
+    let input_method = (*relay).input_method;
+    let keyboard_grab = (*input_method).keyboard_grab;
+    wl_listener_remove_safe(&mut (*relay).grab_keyboard_destroy);
+
+    let keyboard = ffi::river_wlr_input_method_keyboard_grab_v2_get_keyboard(keyboard_grab);
+    if !keyboard.is_null() {
+        let modifiers = ffi::river_wlr_keyboard_get_modifiers(keyboard);
+        ffi::wlr_seat_keyboard_notify_modifiers((*input_method).seat, modifiers);
+    }
+}
diff --git a/src/keyboard.rs b/src/keyboard.rs
new file mode 100644
index 0000000..45b6317
--- /dev/null
+++ b/src/keyboard.rs
@@ -0,0 +1,251 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::input_device::InputDevice;
+use crate::server::{WlListener, wl_listener_remove, wl_signal_add};
+use crate::keyboard_group::KeyboardGroup;
+use std::collections::HashSet;
+
+#[derive(Clone, Copy)]
+pub struct KeyboardConfig {
+    pub keymap: *mut ffi::xkb_keymap,
+    pub repeat_rate: i32,
+    pub repeat_delay: i32,
+}
+
+pub struct Keyboard {
+    pub device: *mut InputDevice,
+    pub wlr_keyboard: *mut ffi::wlr_keyboard,
+    pub key_listener: ffi::wl_listener,
+    pub modifiers_listener: ffi::wl_listener,
+    pub pressed: HashSet<u32>,
+    pub group: *mut KeyboardGroup,
+    pub group_link: ffi::wl_list,
+    pub config: KeyboardConfig,
+}
+
+impl Keyboard {
+    pub unsafe fn create(device: *mut InputDevice) -> *mut Self {
+        let wlr_keyboard = ffi::wlr_keyboard_from_input_device((*device).wlr_device);
+        
+        let virtual_device = (*device).virtual_device;
+        let mut keymap = std::ptr::null_mut();
+        if virtual_device {
+            let kbd_keymap = ffi::river_wlr_keyboard_get_keymap(wlr_keyboard);
+            if !kbd_keymap.is_null() {
+                keymap = kbd_keymap;
+                ffi::xkb_keymap_ref(keymap);
+            }
+        } else {
+            let server = (*(*device).seat).server;
+            keymap = (*server).xkb_config.default_keymap;
+            if !keymap.is_null() {
+                ffi::xkb_keymap_ref(keymap);
+            }
+        }
+
+        let keyboard = Box::into_raw(Box::new(Self {
+            device,
+            wlr_keyboard,
+            key_listener: std::mem::zeroed(),
+            modifiers_listener: std::mem::zeroed(),
+            pressed: HashSet::new(),
+            group: std::ptr::null_mut(),
+            group_link: std::mem::zeroed(),
+            config: KeyboardConfig {
+                keymap,
+                repeat_rate: 40,
+                repeat_delay: 400,
+            },
+        }));
+
+        ffi::wl_list_init(&mut (*keyboard).group_link);
+        ffi::river_wlr_keyboard_set_data(wlr_keyboard, keyboard as *mut _);
+
+        let key_listener_ptr = &mut (*keyboard).key_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*key_listener_ptr).notify = Some(handle_key);
+
+        let modifiers_listener_ptr = &mut (*keyboard).modifiers_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*modifiers_listener_ptr).notify = Some(handle_modifiers);
+
+        let key_signal = ffi::river_wlr_keyboard_get_key_signal(wlr_keyboard);
+        wl_signal_add(key_signal, &mut (*keyboard).key_listener);
+
+        let modifiers_signal = ffi::river_wlr_keyboard_get_modifiers_signal(wlr_keyboard);
+        wl_signal_add(modifiers_signal, &mut (*keyboard).modifiers_listener);
+
+        if !virtual_device && should_set_keymap((*(*device).seat).server) {
+            if !keymap.is_null() {
+                ffi::wlr_keyboard_set_keymap(wlr_keyboard, keymap);
+            }
+        }
+
+        (*device).destroy_fn = Some(destroy_callback);
+        (*device).destroy_data = keyboard as *mut _;
+
+        keyboard
+    }
+
+    pub unsafe fn destroy(keyboard: *mut Self) {
+        wl_listener_remove(&mut (*keyboard).key_listener);
+        wl_listener_remove(&mut (*keyboard).modifiers_listener);
+
+        if !(*keyboard).group.is_null() {
+            let keys: Vec<u32> = (*keyboard).pressed.iter().cloned().collect();
+            (*(*keyboard).group).unref(&keys);
+            (*keyboard).group = std::ptr::null_mut();
+        }
+
+        if !(*keyboard).config.keymap.is_null() {
+            ffi::xkb_keymap_unref((*keyboard).config.keymap);
+        }
+
+        let _boxed = Box::from_raw(keyboard);
+    }
+
+    pub unsafe fn set_group(&mut self) {
+        assert!(self.group.is_null());
+        let seat = (*self.device).seat;
+
+        if !(*self.device).virtual_device {
+            let seat_groups_head = &mut (*seat).keyboard_groups as *mut ffi::wl_list as *mut crate::server::WlList;
+            let mut curr = (*seat_groups_head).next;
+            while curr != seat_groups_head {
+                let next = (*curr).next;
+                let group = crate::container_of!(curr, KeyboardGroup, link);
+                let config_ptr = &mut self.config as *mut KeyboardConfig;
+                if (*group).match_config(config_ptr) {
+                    self.group = (*group).ref_group();
+                    let keyboards_head = &mut (*self.group).keyboards as *mut ffi::wl_list as *mut crate::server::WlList;
+                    crate::server::wl_list_insert(keyboards_head, &mut self.group_link as *mut ffi::wl_list as *mut crate::server::WlList);
+                    return;
+                }
+                curr = next;
+            }
+        }
+
+        match KeyboardGroup::create(seat, self.config, (*self.device).virtual_device) {
+            Ok(group_ptr) => {
+                self.group = group_ptr;
+                let keyboards_head = &mut (*self.group).keyboards as *mut ffi::wl_list as *mut crate::server::WlList;
+                crate::server::wl_list_insert(keyboards_head, &mut self.group_link as *mut ffi::wl_list as *mut crate::server::WlList);
+            }
+            Err(err) => {
+                log::error!("failed to create KeyboardGroup: {}", err);
+            }
+        }
+    }
+
+    pub unsafe fn set_repeat_info(&mut self, rate: i32, delay: i32) {
+        assert!(!(*self.device).virtual_device);
+        self.config.repeat_rate = rate;
+        self.config.repeat_delay = delay;
+        if !self.group.is_null() {
+            let keys: Vec<u32> = self.pressed.iter().cloned().collect();
+            crate::server::wl_list_remove(&mut self.group_link as *mut ffi::wl_list as *mut crate::server::WlList);
+            (*self.group).unref(&keys);
+            self.group = std::ptr::null_mut();
+        }
+        self.set_group();
+    }
+
+    pub unsafe fn set_keymap(&mut self, keymap: *mut ffi::xkb_keymap) {
+        assert!(!(*self.device).virtual_device);
+        if should_set_keymap((*(*self.device).seat).server) {
+            ffi::wlr_keyboard_set_keymap(self.wlr_keyboard, keymap);
+        }
+        if !self.config.keymap.is_null() {
+            ffi::xkb_keymap_unref(self.config.keymap);
+        }
+        self.config.keymap = keymap;
+        if !keymap.is_null() {
+            ffi::xkb_keymap_ref(keymap);
+        }
+        if !self.group.is_null() {
+            let keys: Vec<u32> = self.pressed.iter().cloned().collect();
+            crate::server::wl_list_remove(&mut self.group_link as *mut ffi::wl_list as *mut crate::server::WlList);
+            (*self.group).unref(&keys);
+            self.group = std::ptr::null_mut();
+        }
+        self.set_group();
+    }
+}
+
+unsafe extern "C" fn destroy_callback(data: *mut std::ffi::c_void) {
+    Keyboard::destroy(data as *mut Keyboard);
+}
+
+unsafe fn should_set_keymap(server: *mut crate::server::Server) -> bool {
+    let backend = (*server).backend;
+    ffi::wlr_backend_is_wl(backend) || ffi::wlr_backend_is_x11(backend)
+}
+
+pub unsafe fn keysym_is_modifier(sym: u32) -> bool {
+    match sym {
+        ffi::XKB_KEY_Shift_L |
+        ffi::XKB_KEY_Shift_R |
+        ffi::XKB_KEY_Control_L |
+        ffi::XKB_KEY_Control_R |
+        ffi::XKB_KEY_Caps_Lock |
+        ffi::XKB_KEY_Shift_Lock |
+
+        ffi::XKB_KEY_Meta_L |
+        ffi::XKB_KEY_Meta_R |
+        ffi::XKB_KEY_Alt_L |
+        ffi::XKB_KEY_Alt_R |
+        ffi::XKB_KEY_Super_L |
+        ffi::XKB_KEY_Super_R |
+        ffi::XKB_KEY_Hyper_L |
+        ffi::XKB_KEY_Hyper_R |
+
+        ffi::XKB_KEY_Num_Lock |
+
+        ffi::XKB_KEY_ISO_Lock |
+        ffi::XKB_KEY_ISO_Level2_Latch |
+        ffi::XKB_KEY_ISO_Level3_Shift |
+        ffi::XKB_KEY_ISO_Level3_Latch |
+        ffi::XKB_KEY_ISO_Level3_Lock |
+        ffi::XKB_KEY_ISO_Level5_Shift |
+        ffi::XKB_KEY_ISO_Level5_Latch |
+        ffi::XKB_KEY_ISO_Level5_Lock |
+        ffi::XKB_KEY_ISO_Group_Shift |
+        ffi::XKB_KEY_ISO_Group_Latch |
+        ffi::XKB_KEY_ISO_Group_Lock |
+        ffi::XKB_KEY_ISO_Next_Group |
+        ffi::XKB_KEY_ISO_Next_Group_Lock |
+        ffi::XKB_KEY_ISO_Prev_Group |
+        ffi::XKB_KEY_ISO_Prev_Group_Lock |
+        ffi::XKB_KEY_ISO_First_Group |
+        ffi::XKB_KEY_ISO_First_Group_Lock |
+        ffi::XKB_KEY_ISO_Last_Group |
+        ffi::XKB_KEY_ISO_Last_Group_Lock => true,
+        _ => false,
+    }
+}
+
+unsafe extern "C" fn handle_key(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let keyboard = &mut *crate::container_of!(listener, Keyboard, key_listener);
+    let event = data as *mut ffi::wlr_keyboard_key_event;
+
+    if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED {
+        keyboard.pressed.remove(&(*event).keycode);
+    } else if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
+        if keyboard.pressed.len() < 32 {
+            keyboard.pressed.insert((*event).keycode);
+        }
+    }
+
+    if !keyboard.group.is_null() {
+        (*keyboard.group).process_key(event);
+    }
+}
+
+unsafe extern "C" fn handle_modifiers(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let keyboard = &mut *crate::container_of!(listener, Keyboard, modifiers_listener);
+
+    if !keyboard.group.is_null() {
+        let modifiers = ffi::river_wlr_keyboard_get_modifiers(keyboard.wlr_keyboard);
+        (*keyboard.group).process_modifiers(*modifiers);
+    }
+}
diff --git a/src/keyboard_group.rs b/src/keyboard_group.rs
new file mode 100644
index 0000000..7d885e7
--- /dev/null
+++ b/src/keyboard_group.rs
@@ -0,0 +1,453 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::seat::Seat;
+use crate::keyboard::{Keyboard, KeyboardConfig};
+use crate::xkb_bindings::XkbBinding;
+use crate::server::wl_listener_remove;
+use std::collections::HashMap;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum KeyConsumer {
+    Builtin,
+    Binding(*mut XkbBinding),
+    EnsureEaten,
+    ImGrab,
+    Focus,
+}
+
+pub struct Press {
+    pub consumer: KeyConsumer,
+    pub count: u32,
+}
+
+pub struct KeyboardGroup {
+    pub ref_count: u32,
+    pub seat: *mut Seat,
+    pub link: ffi::wl_list, // Seat.keyboard_groups
+    pub virtual_device: bool,
+    pub config: KeyboardConfig,
+    pub wlr_keyboard: ffi::wlr_keyboard,
+    pub modifiers_old: u32,
+    pub pressed: HashMap<u32, Press>,
+    pub key_listener: ffi::wl_listener,
+    pub modifiers_listener: ffi::wl_listener,
+    pub keyboards: ffi::wl_list, // list of keyboards in this group
+}
+
+impl KeyboardGroup {
+    pub unsafe fn create(
+        seat: *mut Seat,
+        config: KeyboardConfig,
+        virtual_device: bool,
+    ) -> Result<*mut Self, &'static str> {
+        let mut group = Box::new(Self {
+            ref_count: 1,
+            seat,
+            link: std::mem::zeroed(),
+            virtual_device,
+            config,
+            wlr_keyboard: std::mem::zeroed(),
+            modifiers_old: 0,
+            pressed: HashMap::new(),
+            key_listener: std::mem::zeroed(),
+            modifiers_listener: std::mem::zeroed(),
+            keyboards: std::mem::zeroed(),
+        });
+
+        ffi::wl_list_init(&mut group.keyboards);
+        ffi::wl_list_init(&mut group.link);
+
+        // Add to seat.keyboard_groups
+        let seat_groups_head = &mut (*seat).keyboard_groups as *mut ffi::wl_list as *mut crate::server::WlList;
+        crate::server::wl_list_insert((*seat_groups_head).prev, &mut group.link as *mut ffi::wl_list as *mut crate::server::WlList);
+
+        let group_ptr = Box::into_raw(group);
+
+        ffi::river_wlr_keyboard_init(
+            &mut (*group_ptr).wlr_keyboard,
+            Some(led_update),
+            b"river.KeyboardGroup\0".as_ptr() as *const _,
+        );
+
+        ffi::river_wlr_keyboard_set_data(&mut (*group_ptr).wlr_keyboard, group_ptr as *mut _);
+
+        if !config.keymap.is_null() {
+            ffi::wlr_keyboard_set_keymap(&mut (*group_ptr).wlr_keyboard, config.keymap);
+        }
+        ffi::wlr_keyboard_set_repeat_info(&mut (*group_ptr).wlr_keyboard, config.repeat_rate, config.repeat_delay);
+
+        let key_listener_ptr = &mut (*group_ptr).key_listener as *mut ffi::wl_listener as *mut crate::server::WlListener;
+        (*key_listener_ptr).notify = Some(handle_group_key);
+        let key_signal = ffi::river_wlr_keyboard_get_key_signal(&mut (*group_ptr).wlr_keyboard);
+        crate::server::wl_signal_add(key_signal, &mut (*group_ptr).key_listener);
+
+        let modifiers_listener_ptr = &mut (*group_ptr).modifiers_listener as *mut ffi::wl_listener as *mut crate::server::WlListener;
+        (*modifiers_listener_ptr).notify = Some(handle_group_modifiers);
+        let modifiers_signal = ffi::river_wlr_keyboard_get_modifiers_signal(&mut (*group_ptr).wlr_keyboard);
+        crate::server::wl_signal_add(modifiers_signal, &mut (*group_ptr).modifiers_listener);
+
+        if !config.keymap.is_null() {
+            ffi::xkb_keymap_ref(config.keymap);
+        }
+
+        Ok(group_ptr)
+    }
+
+    pub unsafe fn ref_group(&mut self) -> *mut Self {
+        self.ref_count += 1;
+        self
+    }
+
+    pub unsafe fn unref(&mut self, to_release: &[u32]) {
+        for &keycode in to_release {
+            let mut event = ffi::wlr_keyboard_key_event {
+                time_msec: crate::util::msec_timestamp(),
+                keycode,
+                update_state: true,
+                state: ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED,
+            };
+            self.process_key(&mut event);
+        }
+
+        self.ref_count -= 1;
+        if self.ref_count > 0 {
+            return;
+        }
+
+        crate::server::wl_list_remove(&mut self.link as *mut ffi::wl_list as *mut crate::server::WlList);
+        wl_listener_remove(&mut self.key_listener);
+        wl_listener_remove(&mut self.modifiers_listener);
+
+        // If the currently active keyboard of a seat is destroyed, we need to set a new active keyboard.
+        let active_wlr_kbd = ffi::river_wlr_seat_get_keyboard((*self.seat).wlr_seat);
+        if active_wlr_kbd == &mut self.wlr_keyboard {
+            let seat_groups_head = &mut (*self.seat).keyboard_groups as *mut ffi::wl_list as *mut crate::server::WlList;
+            let first_node = (*seat_groups_head).next;
+            if first_node != seat_groups_head {
+                let other_group = crate::container_of!(first_node, KeyboardGroup, link);
+                ffi::wlr_seat_set_keyboard((*self.seat).wlr_seat, &mut (*other_group).wlr_keyboard);
+            } else {
+                ffi::wlr_seat_set_keyboard((*self.seat).wlr_seat, std::ptr::null_mut());
+            }
+        }
+
+        ffi::wlr_keyboard_finish(&mut self.wlr_keyboard);
+
+        if !self.config.keymap.is_null() {
+            ffi::xkb_keymap_unref(self.config.keymap);
+        }
+
+        let _boxed = Box::from_raw(self);
+    }
+
+    pub unsafe fn match_config(&self, config: *mut KeyboardConfig) -> bool {
+        if self.config.repeat_rate != (*config).repeat_rate {
+            return false;
+        }
+        if self.config.repeat_delay != (*config).repeat_delay {
+            return false;
+        }
+        if self.config.keymap == (*config).keymap {
+            return true;
+        }
+        if self.config.keymap.is_null() || (*config).keymap.is_null() {
+            return false;
+        }
+
+        let a_string_ptr = ffi::xkb_keymap_get_as_string(self.config.keymap, ffi::xkb_keymap_format_XKB_KEYMAP_FORMAT_TEXT_V1);
+        if a_string_ptr.is_null() {
+            return false;
+        }
+        let b_string_ptr = ffi::xkb_keymap_get_as_string((*config).keymap, ffi::xkb_keymap_format_XKB_KEYMAP_FORMAT_TEXT_V1);
+        if b_string_ptr.is_null() {
+            libc::free(a_string_ptr as *mut _);
+            return false;
+        }
+
+        let a_str = std::ffi::CStr::from_ptr(a_string_ptr);
+        let b_str = std::ffi::CStr::from_ptr(b_string_ptr);
+        let matched = a_str == b_str;
+
+        libc::free(a_string_ptr as *mut _);
+        libc::free(b_string_ptr as *mut _);
+
+        if matched {
+            ffi::xkb_keymap_unref((*config).keymap);
+            (*config).keymap = self.config.keymap;
+            ffi::xkb_keymap_ref((*config).keymap);
+        }
+
+        matched
+    }
+
+    pub unsafe fn process_key(&mut self, event: *const ffi::wlr_keyboard_key_event) {
+        if let Some(key) = self.pressed.get_mut(&(*event).keycode) {
+            assert!(key.count > 0);
+            if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
+                key.count += 1;
+            } else {
+                key.count -= 1;
+                if key.count == 0 {
+                    let mut key_event = ffi::wlr_keyboard_key_event {
+                        time_msec: (*event).time_msec,
+                        keycode: (*event).keycode,
+                        update_state: true,
+                        state: ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED,
+                    };
+                    ffi::wlr_keyboard_notify_key(&mut self.wlr_keyboard, &mut key_event);
+                }
+            }
+        } else if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
+            if self.pressed.len() < 32 {
+                let mut key_event = ffi::wlr_keyboard_key_event {
+                    time_msec: (*event).time_msec,
+                    keycode: (*event).keycode,
+                    update_state: true,
+                    state: ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED,
+                };
+                ffi::wlr_keyboard_notify_key(&mut self.wlr_keyboard, &mut key_event);
+            }
+        }
+    }
+
+    pub unsafe fn process_modifiers(&mut self, modifiers: ffi::wlr_keyboard_modifiers) {
+        ffi::wlr_keyboard_notify_modifiers(
+            &mut self.wlr_keyboard,
+            modifiers.depressed,
+            modifiers.latched,
+            modifiers.locked,
+            modifiers.group,
+        );
+    }
+
+    pub unsafe fn process_keymap(&mut self, keymap: *mut ffi::xkb_keymap) {
+        ffi::wlr_keyboard_set_keymap(&mut self.wlr_keyboard, keymap);
+    }
+
+    pub unsafe fn get_input_method_grab(&self) -> *mut ffi::wlr_input_method_keyboard_grab_v2 {
+        if self.virtual_device {
+            return std::ptr::null_mut();
+        }
+        let input_method = (*self.seat).relay.input_method;
+        if !input_method.is_null() {
+            return (*input_method).keyboard_grab;
+        }
+        std::ptr::null_mut()
+    }
+
+    pub unsafe fn send_state(&mut self) {
+        let keymap = self.config.keymap;
+        if keymap.is_null() {
+            return;
+        }
+        let layout_index = self.wlr_keyboard.modifiers.group;
+        let _layout_name = ffi::xkb_keymap_layout_get_name(keymap, layout_index);
+        let caps_idx = ffi::xkb_keymap_mod_get_index(keymap, b"Caps Lock\0".as_ptr() as *const _);
+        let _capslock = if caps_idx != ffi::XKB_MOD_INVALID {
+            let caps_mask = 1 << caps_idx;
+            (self.wlr_keyboard.modifiers.locked & caps_mask) != 0
+        } else {
+            false
+        };
+        let num_idx = ffi::xkb_keymap_mod_get_index(keymap, b"Num Lock\0".as_ptr() as *const _);
+        let _numlock = if num_idx != ffi::XKB_MOD_INVALID {
+            let num_mask = 1 << num_idx;
+            (self.wlr_keyboard.modifiers.locked & num_mask) != 0
+        } else {
+            false
+        };
+
+        // Note: Layout configurations broadcasting (river_xkb_keyboard_v1) is stubbed out
+        // in xkb_config.rs and will be fully ported in a later phase.
+    }
+}
+
+unsafe fn handle_builtin_binding(seat: *mut Seat, keysym: u32) -> bool {
+    match keysym {
+        ffi::XKB_KEY_XF86Switch_VT_1..=ffi::XKB_KEY_XF86Switch_VT_12 => {
+            log::debug!("switch VT keysym received");
+            let server = (*seat).server;
+            let session = (*server).session;
+            if !session.is_null() {
+                let vt = keysym - ffi::XKB_KEY_XF86Switch_VT_1 + 1;
+                log::info!("switching to VT {}", vt);
+                ffi::wlr_session_change_vt(session, vt);
+            }
+            true
+        }
+        _ => false,
+    }
+}
+
+unsafe extern "C" fn led_update(wlr_keyboard: *mut ffi::wlr_keyboard, leds: u32) {
+    let group = ffi::river_wlr_keyboard_get_data(wlr_keyboard) as *mut KeyboardGroup;
+    if group.is_null() {
+        return;
+    }
+
+    let keyboards_head = &mut (*group).keyboards as *mut ffi::wl_list as *mut crate::server::WlList;
+    let mut curr = (*keyboards_head).next;
+    while curr != keyboards_head {
+        let next = (*curr).next;
+        let keyboard = crate::container_of!(curr, crate::keyboard::Keyboard, group_link);
+        ffi::wlr_keyboard_led_update((*keyboard).wlr_keyboard, leds);
+        curr = next;
+    }
+}
+
+unsafe extern "C" fn handle_group_key(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let group = &mut *crate::container_of!(listener, KeyboardGroup, key_listener);
+    let event = data as *mut ffi::wlr_keyboard_key_event;
+
+    let xkb_state = group.wlr_keyboard.xkb_state;
+    if xkb_state.is_null() {
+        log::error!("no xkb_state available");
+        return;
+    }
+
+    // Cancel active binding repeats
+    let seat_groups_head = &mut (*group.seat).keyboard_groups as *mut ffi::wl_list as *mut crate::server::WlList;
+    let mut curr_g = (*seat_groups_head).next;
+    while curr_g != seat_groups_head {
+        let next_g = (*curr_g).next;
+        let g = crate::container_of!(curr_g, KeyboardGroup, link);
+        for press in (*g).pressed.values() {
+            if let KeyConsumer::Binding(binding) = press.consumer {
+                if !binding.is_null() {
+                    (*binding).stop_repeat();
+                }
+            }
+        }
+        curr_g = next_g;
+    }
+
+    let consumer: KeyConsumer = if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED {
+        if let Some(kv) = group.pressed.remove(&(*event).keycode) {
+            assert!(kv.count == 0);
+            kv.consumer
+        } else {
+            KeyConsumer::Focus
+        }
+    } else {
+        let xkb_keycode = (*event).keycode + 8;
+        
+        let mut matched_builtin = false;
+        let mut syms_ptr: *const ffi::xkb_keysym_t = std::ptr::null();
+        let num_syms = ffi::xkb_state_key_get_syms(xkb_state, xkb_keycode, &mut syms_ptr);
+        if num_syms > 0 && !syms_ptr.is_null() {
+            let syms = std::slice::from_raw_parts(syms_ptr, num_syms as usize);
+            for &sym in syms {
+                if handle_builtin_binding(group.seat, sym) {
+                    matched_builtin = true;
+                    break;
+                }
+            }
+        }
+
+        if matched_builtin {
+            KeyConsumer::Builtin
+        } else if let Some(binding) = (*group.seat).match_xkb_binding(xkb_keycode, &mut group.wlr_keyboard) {
+            log::debug!("matched xkb binding");
+            (*group.seat).xkb_bindings_seat.ensure_next_key_eaten = false;
+            KeyConsumer::Binding(if (*binding).sent_pressed {
+                std::ptr::null_mut()
+            } else {
+                binding
+            })
+        } else if (*group.seat).xkb_bindings_seat.ensure_next_key_eaten {
+            let mut has_non_modifier = false;
+            let mut syms_ptr: *const ffi::xkb_keysym_t = std::ptr::null();
+            let num_syms = ffi::xkb_state_key_get_syms(xkb_state, xkb_keycode, &mut syms_ptr);
+            if num_syms > 0 && !syms_ptr.is_null() {
+                let syms = std::slice::from_raw_parts(syms_ptr, num_syms as usize);
+                for &sym in syms {
+                    if !crate::keyboard::keysym_is_modifier(sym) {
+                        has_non_modifier = true;
+                        break;
+                    }
+                }
+            }
+            if has_non_modifier {
+                (*group.seat).xkb_bindings_seat.ensure_next_key_eaten = false;
+                KeyConsumer::EnsureEaten
+            } else {
+                KeyConsumer::Focus
+            }
+        } else if !group.get_input_method_grab().is_null() {
+            KeyConsumer::ImGrab
+        } else {
+            KeyConsumer::Focus
+        }
+    };
+
+    if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
+        group.pressed.insert(
+            (*event).keycode,
+            Press {
+                consumer,
+                count: 1,
+            },
+        );
+    }
+
+    match consumer {
+        KeyConsumer::Builtin => {}
+        KeyConsumer::Binding(binding) => {
+            if !binding.is_null() {
+                if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
+                    (*binding).pressed();
+                } else {
+                    (*binding).released();
+                }
+            }
+        }
+        KeyConsumer::EnsureEaten => {
+            if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
+                (*group.seat).xkb_bindings_seat.scheduled_ate_unbound_key = true;
+                (*(*group.seat).server).wm.dirty_windowing();
+            }
+        }
+        KeyConsumer::ImGrab => {
+            let grab = group.get_input_method_grab();
+            if !grab.is_null() {
+                ffi::wlr_input_method_keyboard_grab_v2_set_keyboard(grab, &mut group.wlr_keyboard);
+                ffi::wlr_input_method_keyboard_grab_v2_send_key(grab, (*event).time_msec, (*event).keycode, (*event).state);
+            }
+        }
+        KeyConsumer::Focus => {
+            ffi::wlr_seat_set_keyboard((*group.seat).wlr_seat, &mut group.wlr_keyboard);
+            ffi::wlr_seat_keyboard_notify_key((*group.seat).wlr_seat, (*event).time_msec, (*event).keycode, (*event).state);
+        }
+    }
+
+    group.send_state();
+}
+
+unsafe extern "C" fn handle_group_modifiers(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let group = &mut *crate::container_of!(listener, KeyboardGroup, modifiers_listener);
+
+    let old = group.modifiers_old;
+    let new = ffi::wlr_keyboard_get_modifiers(&mut group.wlr_keyboard);
+    let watched = (*group.seat).xkb_bindings_seat.requested_mods_watched;
+    if (old & watched) != (new & watched) {
+        (*group.seat).xkb_bindings_seat.scheduled_mods_update = Some(crate::xkb_bindings::XkbBindingsSeatModsUpdate {
+            old,
+            new,
+        });
+        (*(*group.seat).server).wm.dirty_windowing();
+    }
+    group.modifiers_old = new;
+
+    let grab = group.get_input_method_grab();
+    if !grab.is_null() {
+        ffi::wlr_input_method_keyboard_grab_v2_set_keyboard(grab, &mut group.wlr_keyboard);
+        ffi::wlr_input_method_keyboard_grab_v2_send_modifiers(grab, &mut group.wlr_keyboard.modifiers);
+    } else {
+        ffi::wlr_seat_set_keyboard((*group.seat).wlr_seat, &mut group.wlr_keyboard);
+        ffi::wlr_seat_keyboard_notify_modifiers((*group.seat).wlr_seat, &mut group.wlr_keyboard.modifiers);
+    }
+
+    group.send_state();
+}
diff --git a/src/layer_shell.rs b/src/layer_shell.rs
new file mode 100644
index 0000000..e97644a
--- /dev/null
+++ b/src/layer_shell.rs
@@ -0,0 +1,861 @@
+// SPDX-FileCopyrightText: © 2025 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::ffi::CStr;
+use crate::ffi;
+use crate::server::{Server, WlListener, WlList, wl_list_insert, wl_list_remove, wl_signal_add, wl_listener_remove};
+use crate::slotmap::{SlotMap, Key};
+use crate::output::Output;
+use crate::seat::Seat;
+use crate::scene_node_data::{SceneNodeData, SceneNodeDataVal};
+use crate::xdg_popup::XdgPopup;
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum LayerShellSeatFocus {
+    Exclusive(Key),
+    NonExclusive(Key),
+    None,
+}
+
+pub struct LayerShellObject {
+    pub resource: *mut ffi::wl_resource,
+    pub link: ffi::wl_list,
+}
+
+pub struct LayerShell {
+    pub server: *mut Server,
+    pub global: *mut ffi::wl_global,
+    pub wlr_shell: *mut ffi::wlr_layer_shell_v1,
+    pub objects: ffi::wl_list,
+    pub surfaces: SlotMap<*mut LayerSurface>,
+    pub new_surface: ffi::wl_listener,
+}
+
+impl LayerShell {
+    pub unsafe fn init(&mut self, wl_display: *mut ffi::wl_display) -> Result<(), ()> {
+        self.global = ffi::wl_global_create(
+            wl_display,
+            &ffi::river_layer_shell_v1_interface,
+            1,
+            self as *mut LayerShell as *mut _,
+            Some(bind),
+        );
+        if self.global.is_null() {
+            return Err(());
+        }
+
+        self.wlr_shell = ffi::wlr_layer_shell_v1_create(wl_display, 4);
+        if self.wlr_shell.is_null() {
+            ffi::wl_global_destroy(self.global);
+            self.global = std::ptr::null_mut();
+            return Err(());
+        }
+
+        ffi::wl_list_init(&mut self.objects);
+
+        let new_surface_ptr = &mut self.new_surface as *mut ffi::wl_listener as *mut WlListener;
+        (*new_surface_ptr).notify = Some(handle_new_surface);
+        wl_signal_add(&mut (*self.wlr_shell).events.new_surface, &mut self.new_surface);
+
+        Ok(())
+    }
+
+    pub unsafe fn deinit(&mut self) {
+        if !self.global.is_null() {
+            ffi::wl_global_destroy(self.global);
+            self.global = std::ptr::null_mut();
+        }
+        if !self.new_surface.link.prev.is_null() {
+            wl_listener_remove(&mut self.new_surface);
+        }
+    }
+
+    pub unsafe fn supported(&self) -> bool {
+        let wm_v1 = (*self.server).wm.object;
+        if wm_v1.is_null() {
+            return false;
+        }
+        let wm_client = ffi::wl_resource_get_client(wm_v1);
+
+        let objects_list = &self.objects as *const ffi::wl_list as *mut WlList;
+        let mut curr = (*objects_list).next;
+        while curr != objects_list {
+            let next = (*curr).next;
+            let obj = crate::container_of!(curr, LayerShellObject, link);
+            let obj_client = ffi::wl_resource_get_client((*obj).resource);
+            if obj_client == wm_client {
+                return true;
+            }
+            curr = next;
+        }
+        false
+    }
+
+    pub unsafe fn check_exclusive_focus(&mut self) {
+        let layers = [
+            ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY,
+            ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_TOP,
+        ];
+        let mut to_focus: *mut LayerSurface = std::ptr::null_mut();
+
+        'outer: for &layer in &layers {
+            let tree = (*self.server).scene.layer_surface_tree(layer);
+            let children_head = ffi::river_scene_tree_get_children(tree) as *mut WlList;
+            let mut curr = (*children_head).prev;
+            while curr != children_head {
+                let prev = (*curr).prev;
+                let node = ffi::river_scene_node_from_children_link(curr as *mut ffi::wl_list);
+                if let Some(node_data) = SceneNodeData::from_node(node) {
+                    if let SceneNodeDataVal::LayerSurface(layer_surface) = node_data.data {
+                        let wlr_layer_surface = (*layer_surface).wlr_layer_surface;
+                        if ffi::river_wlr_surface_is_mapped((*wlr_layer_surface).surface) &&
+                           (*wlr_layer_surface).current.keyboard_interactive == ffi::zwlr_layer_surface_v1_keyboard_interactivity_ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE {
+                            to_focus = layer_surface;
+                            break 'outer;
+                        }
+                    }
+                }
+                curr = prev;
+            }
+        }
+
+        let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*seats).next;
+        while curr != seats {
+            let next = (*curr).next;
+            let seat = crate::container_of!(curr, Seat, link);
+            if !to_focus.is_null() {
+                (*seat).layer_shell.scheduled_focus = LayerShellSeatFocus::Exclusive((*to_focus).ref_key);
+            } else if matches!((*seat).layer_shell.scheduled_focus, LayerShellSeatFocus::Exclusive(_)) {
+                (*seat).layer_shell.scheduled_focus = LayerShellSeatFocus::None;
+            }
+            curr = next;
+        }
+    }
+}
+
+unsafe extern "C" fn bind(
+    client: *mut ffi::wl_client,
+    data: *mut std::ffi::c_void,
+    version: u32,
+    id: u32,
+) {
+    let layer_shell = data as *mut LayerShell;
+    if layer_shell.is_null() {
+        return;
+    }
+
+    let resource = ffi::wl_resource_create(client, &ffi::river_layer_shell_v1_interface, version as i32, id);
+    if resource.is_null() {
+        ffi::wl_client_post_no_memory(client);
+        log::error!("out of memory binding river_layer_shell_v1");
+        return;
+    }
+
+    let obj = Box::into_raw(Box::new(LayerShellObject {
+        resource,
+        link: std::mem::zeroed(),
+    }));
+    ffi::wl_list_init(&mut (*obj).link);
+    let objects_list = &mut (*layer_shell).objects as *mut ffi::wl_list as *mut WlList;
+    let link_custom = &mut (*obj).link as *mut ffi::wl_list as *mut WlList;
+    wl_list_insert(objects_list, link_custom);
+
+    ffi::wl_resource_set_implementation(
+        resource,
+        &LAYER_SHELL_INTERFACE as *const _ as *const _,
+        obj as *mut _,
+        Some(handle_destroy_resource),
+    );
+}
+
+unsafe extern "C" fn handle_destroy_resource(resource: *mut ffi::wl_resource) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LayerShellObject;
+    if !obj.is_null() {
+        wl_list_remove(&mut (*obj).link as *mut ffi::wl_list as *mut WlList);
+        let _ = Box::from_raw(obj);
+    }
+}
+
+unsafe extern "C" fn layer_shell_destroy(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    let _ = client;
+    ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn layer_shell_get_output(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    id: u32,
+    output_resource: *mut ffi::wl_resource,
+) {
+    let output = ffi::wl_resource_get_user_data(output_resource) as *mut Output;
+    if output.is_null() {
+        return;
+    }
+    if !(*output).layer_shell.object.is_null() {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_layer_shell_v1_error_RIVER_LAYER_SHELL_V1_ERROR_OBJECT_ALREADY_CREATED,
+            b"river_layer_shell_output_v1 already created\0".as_ptr() as *const _,
+        );
+        return;
+    }
+    let version = ffi::wl_resource_get_version(resource);
+    (*output).layer_shell.create_object(client, version as u32, id, output);
+}
+
+unsafe extern "C" fn layer_shell_get_seat(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    id: u32,
+    seat_resource: *mut ffi::wl_resource,
+) {
+    let seat = ffi::wl_resource_get_user_data(seat_resource) as *mut Seat;
+    if seat.is_null() {
+        return;
+    }
+    if !(*seat).layer_shell.object.is_null() {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_layer_shell_v1_error_RIVER_LAYER_SHELL_V1_ERROR_OBJECT_ALREADY_CREATED,
+            b"river_layer_shell_seat_v1 already created\0".as_ptr() as *const _,
+        );
+        return;
+    }
+    let version = ffi::wl_resource_get_version(resource);
+    (*seat).layer_shell.create_object(client, version as u32, id, seat);
+}
+
+static LAYER_SHELL_INTERFACE: ffi::river_layer_shell_v1_interface = ffi::river_layer_shell_v1_interface {
+    destroy: Some(layer_shell_destroy),
+    get_output: Some(layer_shell_get_output),
+    get_seat: Some(layer_shell_get_seat),
+};
+
+unsafe extern "C" fn handle_new_surface(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let layer_shell = crate::container_of!(listener, LayerShell, new_surface);
+    let wlr_layer_surface = data as *mut ffi::wlr_layer_surface_v1;
+
+    log::debug!(
+        "new layer surface: namespace {:?}, layer {}, anchor {}, size {}x{}, margin: top={}, right={}, bottom={}, left={}, exclusive_zone={}",
+        CStr::from_ptr((*wlr_layer_surface).namespace),
+        (*wlr_layer_surface).current.layer,
+        (*wlr_layer_surface).current.anchor,
+        (*wlr_layer_surface).current.desired_width,
+        (*wlr_layer_surface).current.desired_height,
+        (*wlr_layer_surface).current.margin.top,
+        (*wlr_layer_surface).current.margin.right,
+        (*wlr_layer_surface).current.margin.bottom,
+        (*wlr_layer_surface).current.margin.left,
+        (*wlr_layer_surface).current.exclusive_zone,
+    );
+
+    if !(*layer_shell).supported() {
+        log::info!("window manager did not bind river_layer_shell_v1, closing layer surface");
+        ffi::wlr_layer_surface_v1_destroy(wlr_layer_surface);
+        return;
+    }
+
+    if (*wlr_layer_surface).output.is_null() {
+        let outputs = &mut (*(*layer_shell).server).om.outputs as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*outputs).next;
+        while curr != outputs {
+            let next = (*curr).next;
+            let output = crate::container_of!(curr, Output, link);
+            if (*output).layer_shell.requested.default {
+                (*wlr_layer_surface).output = (*output).wlr_output;
+                break;
+            }
+            curr = next;
+        }
+
+        if (*wlr_layer_surface).output.is_null() {
+            let first_node = (*outputs).next;
+            if first_node != outputs {
+                let output = crate::container_of!(first_node, Output, link);
+                log::info!("window manager did not set default layer surface output, choosing arbitrary output");
+                (*wlr_layer_surface).output = (*output).wlr_output;
+            } else {
+                log::error!("no output available for layer surface {:?}", CStr::from_ptr((*wlr_layer_surface).namespace));
+                ffi::wlr_layer_surface_v1_destroy(wlr_layer_surface);
+                return;
+            }
+        }
+    }
+
+    if let Err(e) = LayerSurface::create(wlr_layer_surface, (*layer_shell).server) {
+        log::error!("Failed to create layer surface: {}", e);
+        ffi::wl_resource_post_no_memory((*wlr_layer_surface).resource);
+    }
+}
+
+pub struct LayerSurface {
+    pub ref_key: Key,
+    pub server: *mut Server,
+    pub wlr_layer_surface: *mut ffi::wlr_layer_surface_v1,
+    pub scene_layer_surface: *mut ffi::wlr_scene_layer_surface_v1,
+    pub popup_tree: *mut ffi::wlr_scene_tree,
+
+    pub destroy: ffi::wl_listener,
+    pub map: ffi::wl_listener,
+    pub unmap: ffi::wl_listener,
+    pub commit: ffi::wl_listener,
+    pub new_popup: ffi::wl_listener,
+}
+
+impl LayerSurface {
+    pub unsafe fn create(
+        wlr_layer_surface: *mut ffi::wlr_layer_surface_v1,
+        server: *mut Server,
+    ) -> Result<*mut Self, &'static str> {
+        let layer_tree = (*server).scene.layer_surface_tree((*wlr_layer_surface).current.layer);
+        let scene_layer_surface = ffi::wlr_scene_layer_surface_v1_create(layer_tree, wlr_layer_surface);
+        if scene_layer_surface.is_null() {
+            return Err("Failed to create wlr_scene_layer_surface_v1");
+        }
+
+        let popup_tree = ffi::wlr_scene_tree_create((*server).scene.layers.popups);
+        if popup_tree.is_null() {
+            ffi::wlr_scene_node_destroy((*scene_layer_surface).tree as *mut ffi::wlr_scene_node);
+            return Err("Failed to create popup_tree");
+        }
+
+        let layer_surface = Box::into_raw(Box::new(LayerSurface {
+            ref_key: Key { generation: 0, index: 0 },
+            server,
+            wlr_layer_surface,
+            scene_layer_surface,
+            popup_tree,
+            destroy: std::mem::zeroed(),
+            map: std::mem::zeroed(),
+            unmap: std::mem::zeroed(),
+            commit: std::mem::zeroed(),
+            new_popup: std::mem::zeroed(),
+        }));
+
+        let key = (*server).layer_shell.surfaces.put(layer_surface);
+        (*layer_surface).ref_key = key;
+
+        SceneNodeData::attach((*scene_layer_surface).tree as *mut _, SceneNodeDataVal::LayerSurface(layer_surface));
+        SceneNodeData::attach(popup_tree as *mut _, SceneNodeDataVal::LayerSurface(layer_surface));
+
+        ffi::river_wlr_surface_set_data((*wlr_layer_surface).surface, (*scene_layer_surface).tree as *mut _);
+
+        let destroy_ptr = &mut (*layer_surface).destroy as *mut ffi::wl_listener as *mut WlListener;
+        (*destroy_ptr).notify = Some(handle_layer_surface_destroy);
+        wl_signal_add(&mut (*wlr_layer_surface).events.destroy, &mut (*layer_surface).destroy);
+
+        let map_ptr = &mut (*layer_surface).map as *mut ffi::wl_listener as *mut WlListener;
+        (*map_ptr).notify = Some(handle_layer_surface_map);
+        wl_signal_add(ffi::river_wlr_surface_get_map_signal((*wlr_layer_surface).surface), &mut (*layer_surface).map);
+
+        let unmap_ptr = &mut (*layer_surface).unmap as *mut ffi::wl_listener as *mut WlListener;
+        (*unmap_ptr).notify = Some(handle_layer_surface_unmap);
+        wl_signal_add(ffi::river_wlr_surface_get_unmap_signal((*wlr_layer_surface).surface), &mut (*layer_surface).unmap);
+
+        let commit_ptr = &mut (*layer_surface).commit as *mut ffi::wl_listener as *mut WlListener;
+        (*commit_ptr).notify = Some(handle_layer_surface_commit);
+        wl_signal_add(ffi::river_wlr_surface_get_commit_signal((*wlr_layer_surface).surface), &mut (*layer_surface).commit);
+
+        let new_popup_ptr = &mut (*layer_surface).new_popup as *mut ffi::wl_listener as *mut WlListener;
+        (*new_popup_ptr).notify = Some(handle_layer_surface_new_popup);
+        wl_signal_add(&mut (*wlr_layer_surface).events.new_popup, &mut (*layer_surface).new_popup);
+
+        Ok(layer_surface)
+    }
+
+    pub unsafe fn destroy_popups(&mut self) {
+        let popups_list = &mut (*self.wlr_layer_surface).popups as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*popups_list).next;
+        while curr != popups_list {
+            let next = (*curr).next;
+            let wlr_xdg_popup = crate::container_of!(curr, ffi::wlr_xdg_popup, link);
+            ffi::wlr_xdg_popup_destroy(wlr_xdg_popup);
+            curr = next;
+        }
+    }
+}
+
+unsafe extern "C" fn handle_layer_surface_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let layer_surface = crate::container_of!(listener, LayerSurface, destroy);
+
+    log::debug!("layer surface {:?} destroyed", CStr::from_ptr((*(*layer_surface).wlr_layer_surface).namespace));
+
+    wl_listener_remove(&mut (*layer_surface).destroy);
+    wl_listener_remove(&mut (*layer_surface).map);
+    wl_listener_remove(&mut (*layer_surface).unmap);
+    wl_listener_remove(&mut (*layer_surface).commit);
+    wl_listener_remove(&mut (*layer_surface).new_popup);
+
+    (*layer_surface).destroy_popups();
+
+    ffi::wlr_scene_node_destroy((*layer_surface).popup_tree as *mut ffi::wlr_scene_node);
+
+    ffi::river_wlr_surface_set_data((*(*layer_surface).wlr_layer_surface).surface, std::ptr::null_mut());
+
+    let server = (*layer_surface).server;
+    (*server).layer_shell.surfaces.remove((*layer_surface).ref_key);
+    let _ = Box::from_raw(layer_surface);
+}
+
+unsafe extern "C" fn handle_layer_surface_map(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let layer_surface = crate::container_of!(listener, LayerSurface, map);
+    let wlr_layer_surface = (*layer_surface).wlr_layer_surface;
+
+    log::debug!("layer surface {:?} mapped", CStr::from_ptr((*wlr_layer_surface).namespace));
+
+    let server = (*layer_surface).server;
+
+    if (*wlr_layer_surface).current.keyboard_interactive == ffi::zwlr_layer_surface_v1_keyboard_interactivity_ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND {
+        let seats = &mut (*server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*seats).next;
+        while curr != seats {
+            let next = (*curr).next;
+            let seat = crate::container_of!(curr, Seat, link);
+            if !matches!((*seat).layer_shell.scheduled_focus, LayerShellSeatFocus::Exclusive(_)) {
+                (*seat).layer_shell.scheduled_focus = LayerShellSeatFocus::NonExclusive((*layer_surface).ref_key);
+            }
+            curr = next;
+        }
+    }
+
+    let output = ffi::river_wlr_output_get_data((*wlr_layer_surface).output) as *mut Output;
+    (*output).layer_shell.arrange(output);
+    (*server).layer_shell.check_exclusive_focus();
+    (*server).wm.dirty_windowing();
+}
+
+unsafe extern "C" fn handle_layer_surface_unmap(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let layer_surface = crate::container_of!(listener, LayerSurface, unmap);
+    let wlr_layer_surface = (*layer_surface).wlr_layer_surface;
+
+    log::debug!("layer surface {:?} unmapped", CStr::from_ptr((*wlr_layer_surface).namespace));
+
+    let server = (*layer_surface).server;
+
+    let seats = &mut (*server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+    let mut curr = (*seats).next;
+    while curr != seats {
+        let next = (*curr).next;
+        let seat = crate::container_of!(curr, Seat, link);
+        if let crate::seat::Focus::LayerSurface(surface) = (*seat).focused {
+            if surface == (*wlr_layer_surface).surface {
+                (*seat).focus(crate::seat::Focus::None);
+            }
+        }
+        if let LayerShellSeatFocus::NonExclusive(key) = (*seat).layer_shell.scheduled_focus {
+            if key == (*layer_surface).ref_key {
+                (*seat).layer_shell.scheduled_focus = LayerShellSeatFocus::None;
+            }
+        }
+        curr = next;
+    }
+
+    let output = ffi::river_wlr_output_get_data((*wlr_layer_surface).output) as *mut Output;
+    (*output).layer_shell.arrange(output);
+    (*server).layer_shell.check_exclusive_focus();
+    (*server).wm.dirty_windowing();
+}
+
+unsafe extern "C" fn handle_layer_surface_commit(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let layer_surface = crate::container_of!(listener, LayerSurface, commit);
+    let wlr_layer_surface = (*layer_surface).wlr_layer_surface;
+
+    assert!(!(*wlr_layer_surface).output.is_null());
+
+    let server = (*layer_surface).server;
+
+    // Check if layer was changed
+    if (*wlr_layer_surface).current.committed & ffi::wlr_layer_surface_v1_state_field_WLR_LAYER_SURFACE_V1_STATE_LAYER != 0 {
+        let tree = (*server).scene.layer_surface_tree((*wlr_layer_surface).current.layer);
+        ffi::wlr_scene_node_reparent(
+            (*(*layer_surface).scene_layer_surface).tree as *mut ffi::wlr_scene_node,
+            tree,
+        );
+    }
+
+    if (*wlr_layer_surface).initial_commit || ((*wlr_layer_surface).current.committed != 0) {
+        let output = ffi::river_wlr_output_get_data((*wlr_layer_surface).output) as *mut Output;
+        (*output).layer_shell.arrange(output);
+        (*server).layer_shell.check_exclusive_focus();
+        (*server).wm.dirty_windowing();
+    }
+}
+
+unsafe extern "C" fn handle_layer_surface_new_popup(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let layer_surface = crate::container_of!(listener, LayerSurface, new_popup);
+    let wlr_xdg_popup = data as *mut ffi::wlr_xdg_popup;
+
+    if let Err(e) = XdgPopup::create(wlr_xdg_popup, (*layer_surface).popup_tree, std::ptr::null_mut()) {
+        log::error!("Failed to create layer surface popup: {}", e);
+        ffi::wl_resource_post_no_memory((*wlr_xdg_popup).resource);
+    }
+}
+
+#[derive(Clone, Copy)]
+pub struct LayerShellOutputScheduled {
+    pub non_exclusive_area: ffi::wlr_box,
+}
+
+#[derive(Clone, Copy)]
+pub struct LayerShellOutputSent {
+    pub non_exclusive_area: Option<ffi::wlr_box>,
+}
+
+#[derive(Clone, Copy)]
+pub struct LayerShellOutputRequested {
+    pub default: bool,
+}
+
+pub struct LayerShellOutput {
+    pub object: *mut ffi::wl_resource, // river_layer_shell_output_v1
+    pub scheduled: LayerShellOutputScheduled,
+    pub sent: LayerShellOutputSent,
+    pub requested: LayerShellOutputRequested,
+}
+
+impl Default for LayerShellOutput {
+    fn default() -> Self {
+        Self {
+            object: std::ptr::null_mut(),
+            scheduled: LayerShellOutputScheduled {
+                non_exclusive_area: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
+            },
+            sent: LayerShellOutputSent {
+                non_exclusive_area: None,
+            },
+            requested: LayerShellOutputRequested {
+                default: false,
+            },
+        }
+    }
+}
+
+impl LayerShellOutput {
+    pub unsafe fn create_object(&mut self, client: *mut ffi::wl_client, version: u32, id: u32, output: *mut Output) {
+        assert!(self.object.is_null());
+        let resource = ffi::wl_resource_create(client, &ffi::river_layer_shell_output_v1_interface, version as i32, id);
+        if resource.is_null() {
+            ffi::wl_client_post_no_memory(client);
+            log::error!("out of memory creating river_layer_shell_output_v1");
+            return;
+        }
+
+        ffi::wl_resource_set_implementation(
+            resource,
+            &LAYER_SHELL_OUTPUT_INTERFACE as *const _ as *const _,
+            self as *mut LayerShellOutput as *mut _,
+            Some(handle_layer_shell_output_destroy),
+        );
+        self.object = resource;
+        (*(*output).server).wm.dirty_windowing();
+    }
+
+    pub unsafe fn make_inert(&mut self) {
+        if !self.object.is_null() {
+            ffi::wl_resource_set_implementation(
+                self.object,
+                std::ptr::null(),
+                std::ptr::null_mut(),
+                None,
+            );
+            self.object = std::ptr::null_mut();
+        }
+    }
+
+    pub unsafe fn arrange(&mut self, output: *mut Output) {
+        let (w, h) = (*output).scheduled.dimensions();
+        let box_geom = ffi::wlr_box {
+            x: (*output).scheduled.x,
+            y: (*output).scheduled.y,
+            width: w,
+            height: h,
+        };
+        self.scheduled.non_exclusive_area = box_geom;
+        self.send_configures(output, true);
+        self.send_configures(output, false);
+
+        let area_changed = match self.sent.non_exclusive_area {
+            Some(sent_box) => {
+                sent_box.x != self.scheduled.non_exclusive_area.x ||
+                sent_box.y != self.scheduled.non_exclusive_area.y ||
+                sent_box.width != self.scheduled.non_exclusive_area.width ||
+                sent_box.height != self.scheduled.non_exclusive_area.height
+            }
+            None => true,
+        };
+
+        if area_changed {
+            (*(*output).server).wm.dirty_windowing();
+        }
+    }
+
+    unsafe fn send_configures(&mut self, output: *mut Output, exclusive: bool) {
+        let (output_width, output_height) = (*output).scheduled.dimensions();
+        let output_box = ffi::wlr_box {
+            x: (*output).scheduled.x,
+            y: (*output).scheduled.y,
+            width: output_width,
+            height: output_height,
+        };
+
+        let layers = [
+            ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND,
+            ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM,
+            ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_TOP,
+            ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY,
+        ];
+
+        for &layer in &layers {
+            let tree = (*(*output).server).scene.layer_surface_tree(layer);
+            let children_head = ffi::river_scene_tree_get_children(tree) as *mut WlList;
+            let mut curr = (*children_head).next;
+            while curr != children_head {
+                let next = (*curr).next;
+                let node = ffi::river_scene_node_from_children_link(curr as *mut ffi::wl_list);
+                if let Some(node_data) = SceneNodeData::from_node(node) {
+                    if let SceneNodeDataVal::LayerSurface(layer_surface) = node_data.data {
+                        let wlr_layer_surface = (*layer_surface).wlr_layer_surface;
+                        if !ffi::river_wlr_surface_is_mapped((*wlr_layer_surface).surface) && !(*wlr_layer_surface).initial_commit {
+                            curr = next;
+                            continue;
+                        }
+                        if (*wlr_layer_surface).output != (*output).wlr_output {
+                            curr = next;
+                            continue;
+                        }
+                        let current_exclusive = (*wlr_layer_surface).current.exclusive_zone > 0;
+                        if current_exclusive != exclusive {
+                            curr = next;
+                            continue;
+                        }
+
+                        let mut new_area = self.scheduled.non_exclusive_area;
+                        ffi::wlr_scene_layer_surface_v1_configure(
+                            (*layer_surface).scene_layer_surface,
+                            &output_box,
+                            &mut new_area,
+                        );
+
+                        if new_area.width < (output_width / 2) || new_area.height < (output_height / 2) {
+                            ffi::wlr_layer_surface_v1_destroy(wlr_layer_surface);
+                            curr = next;
+                            continue;
+                        }
+                        self.scheduled.non_exclusive_area = new_area;
+
+                        let x = ffi::river_scene_node_get_x((*(*layer_surface).scene_layer_surface).tree as *mut ffi::wlr_scene_node);
+                        let y = ffi::river_scene_node_get_y((*(*layer_surface).scene_layer_surface).tree as *mut ffi::wlr_scene_node);
+                        ffi::wlr_scene_node_set_position((*layer_surface).popup_tree as *mut ffi::wlr_scene_node, x, y);
+
+                        let clip = ffi::wlr_box {
+                            x: -(x - (*output).scheduled.x),
+                            y: -(y - (*output).scheduled.y),
+                            width: output_width,
+                            height: output_height,
+                        };
+                        ffi::wlr_scene_subsurface_tree_set_clip(
+                            (*(*layer_surface).scene_layer_surface).tree as *mut ffi::wlr_scene_node,
+                            &clip,
+                        );
+                    }
+                }
+                curr = next;
+            }
+        }
+    }
+
+    pub unsafe fn manage_start(&mut self, output: *mut Output) {
+        let state = (*output).scheduled.state;
+        assert!(
+            matches!(state, crate::output::OutputStateValue::Enabled)
+                || matches!(state, crate::output::OutputStateValue::DisabledSoft)
+        );
+
+        let (w, h) = (*output).scheduled.dimensions();
+        let scheduled_box = ffi::wlr_box {
+            x: (*output).scheduled.x,
+            y: (*output).scheduled.y,
+            width: w,
+            height: h,
+        };
+
+        let (w_sent, h_sent) = (*output).sent.dimensions();
+        let sent_box = ffi::wlr_box {
+            x: (*output).sent.x,
+            y: (*output).sent.y,
+            width: w_sent,
+            height: h_sent,
+        };
+
+        let box_changed = scheduled_box.x != sent_box.x ||
+                          scheduled_box.y != sent_box.y ||
+                          scheduled_box.width != sent_box.width ||
+                          scheduled_box.height != sent_box.height;
+
+        if box_changed {
+            self.scheduled.non_exclusive_area = scheduled_box;
+            self.send_configures(output, true);
+            self.send_configures(output, false);
+        }
+
+        let area_changed = match self.sent.non_exclusive_area {
+            Some(sent_box) => {
+                sent_box.x != self.scheduled.non_exclusive_area.x ||
+                sent_box.y != self.scheduled.non_exclusive_area.y ||
+                sent_box.width != self.scheduled.non_exclusive_area.width ||
+                sent_box.height != self.scheduled.non_exclusive_area.height
+            }
+            None => true,
+        };
+
+        if area_changed {
+            if !self.object.is_null() {
+                ffi::wl_resource_post_event(
+                    self.object,
+                    1, // send_non_exclusive_area
+                    self.scheduled.non_exclusive_area.x,
+                    self.scheduled.non_exclusive_area.y,
+                    self.scheduled.non_exclusive_area.width,
+                    self.scheduled.non_exclusive_area.height,
+                );
+                self.sent.non_exclusive_area = Some(self.scheduled.non_exclusive_area);
+            }
+        }
+    }
+}
+
+unsafe extern "C" fn handle_layer_shell_output_destroy(resource: *mut ffi::wl_resource) {
+    let layer_shell_output = ffi::wl_resource_get_user_data(resource) as *mut LayerShellOutput;
+    if !layer_shell_output.is_null() {
+        (*layer_shell_output).object = std::ptr::null_mut();
+        (*layer_shell_output).sent.non_exclusive_area = None;
+        (*layer_shell_output).requested.default = false;
+    }
+}
+
+unsafe extern "C" fn layer_shell_output_destroy(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    let _ = client;
+    ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn layer_shell_output_set_default(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    let _ = client;
+    let layer_shell_output = ffi::wl_resource_get_user_data(resource) as *mut LayerShellOutput;
+    if layer_shell_output.is_null() {
+        return;
+    }
+    let server = if !(*layer_shell_output).object.is_null() {
+        // Find server. We can get it via finding Output from the parent link.
+        // Let's traverse the outputs to set requested.default to false on all outputs
+        let output = container_of_output(layer_shell_output);
+        (*output).server
+    } else {
+        std::ptr::null_mut()
+    };
+
+    if !server.is_null() {
+        let outputs = &mut (*server).om.outputs as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*outputs).next;
+        while curr != outputs {
+            let next = (*curr).next;
+            let output = crate::container_of!(curr, Output, link);
+            (*output).layer_shell.requested.default = false;
+            curr = next;
+        }
+        (*layer_shell_output).requested.default = true;
+    }
+}
+
+static LAYER_SHELL_OUTPUT_INTERFACE: ffi::river_layer_shell_output_v1_interface = ffi::river_layer_shell_output_v1_interface {
+    destroy: Some(layer_shell_output_destroy),
+    set_default: Some(layer_shell_output_set_default),
+};
+
+unsafe fn container_of_output(layer_shell_output: *mut LayerShellOutput) -> *mut Output {
+    crate::container_of!(layer_shell_output, Output, layer_shell)
+}
+
+pub struct LayerShellSeat {
+    pub object: *mut ffi::wl_resource, // river_layer_shell_seat_v1
+    pub scheduled_focus: LayerShellSeatFocus,
+    pub sent_focus: LayerShellSeatFocus,
+}
+
+impl Default for LayerShellSeat {
+    fn default() -> Self {
+        Self {
+            object: std::ptr::null_mut(),
+            scheduled_focus: LayerShellSeatFocus::None,
+            sent_focus: LayerShellSeatFocus::None,
+        }
+    }
+}
+
+impl LayerShellSeat {
+    pub unsafe fn create_object(&mut self, client: *mut ffi::wl_client, version: u32, id: u32, seat: *mut Seat) {
+        assert!(self.object.is_null());
+        let resource = ffi::wl_resource_create(client, &ffi::river_layer_shell_seat_v1_interface, version as i32, id);
+        if resource.is_null() {
+            ffi::wl_client_post_no_memory(client);
+            log::error!("out of memory creating river_layer_shell_seat_v1");
+            return;
+        }
+
+        ffi::wl_resource_set_implementation(
+            resource,
+            &LAYER_SHELL_SEAT_INTERFACE as *const _ as *const _,
+            self as *mut LayerShellSeat as *mut _,
+            Some(handle_layer_shell_seat_destroy),
+        );
+        self.object = resource;
+        (*(*seat).server).wm.dirty_windowing();
+    }
+
+    pub unsafe fn make_inert(&mut self) {
+        if !self.object.is_null() {
+            ffi::wl_resource_set_implementation(
+                self.object,
+                std::ptr::null(),
+                std::ptr::null_mut(),
+                None,
+            );
+            self.object = std::ptr::null_mut();
+        }
+    }
+
+    pub unsafe fn manage_start(&mut self) {
+        if self.scheduled_focus != self.sent_focus {
+            if !self.object.is_null() {
+                match self.scheduled_focus {
+                    LayerShellSeatFocus::Exclusive(_) => {
+                        ffi::wl_resource_post_event(self.object, 1); // sendFocusExclusive
+                    }
+                    LayerShellSeatFocus::NonExclusive(_) => {
+                        ffi::wl_resource_post_event(self.object, 2); // sendFocusNonExclusive
+                    }
+                    LayerShellSeatFocus::None => {
+                        ffi::wl_resource_post_event(self.object, 0); // sendFocusNone
+                    }
+                }
+            }
+            self.sent_focus = self.scheduled_focus;
+        }
+    }
+}
+
+unsafe extern "C" fn handle_layer_shell_seat_destroy(resource: *mut ffi::wl_resource) {
+    let layer_shell_seat = ffi::wl_resource_get_user_data(resource) as *mut LayerShellSeat;
+    if !layer_shell_seat.is_null() {
+        (*layer_shell_seat).object = std::ptr::null_mut();
+    }
+}
+
+unsafe extern "C" fn layer_shell_seat_destroy(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    let _ = client;
+    ffi::wl_resource_destroy(resource);
+}
+
+static LAYER_SHELL_SEAT_INTERFACE: ffi::river_layer_shell_seat_v1_interface = ffi::river_layer_shell_seat_v1_interface {
+    destroy: Some(layer_shell_seat_destroy),
+};
diff --git a/src/libinput_config.rs b/src/libinput_config.rs
new file mode 100644
index 0000000..653ec9a
--- /dev/null
+++ b/src/libinput_config.rs
@@ -0,0 +1,194 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::server::{Server, WlListener, wl_signal_add, wl_listener_remove, WlList, wl_list_insert, wl_list_remove};
+
+pub struct LibinputConfig {
+    pub server: *mut Server,
+    pub global: *mut ffi::wl_global,
+    pub objects: ffi::wl_list,
+}
+
+pub struct LibinputConfigObject {
+    pub config: *mut LibinputConfig,
+    pub resource: *mut ffi::wl_resource,
+    pub link: ffi::wl_list,
+}
+
+impl Default for LibinputConfig {
+    fn default() -> Self {
+        unsafe { std::mem::zeroed() }
+    }
+}
+
+impl LibinputConfig {
+    pub unsafe fn init(&mut self, server: *mut Server) -> Result<(), &'static str> {
+        self.server = server;
+        ffi::wl_list_init(&mut self.objects);
+
+        self.global = ffi::wl_global_create(
+            (*server).wl_server,
+            &ffi::river_libinput_config_v1_interface,
+            2,
+            self as *mut LibinputConfig as *mut _,
+            Some(bind_libinput_config),
+        );
+        if self.global.is_null() {
+            return Err("Failed to create river_libinput_config_v1 global");
+        }
+
+        Ok(())
+    }
+
+    pub unsafe fn deinit(&mut self) {
+        if !self.global.is_null() {
+            ffi::wl_global_destroy(self.global);
+            self.global = std::ptr::null_mut();
+        }
+
+        let objects_head = &mut self.objects as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*objects_head).next;
+        while curr != objects_head {
+            let next = (*curr).next;
+            let obj = crate::container_of!(curr, LibinputConfigObject, link);
+            ffi::wl_resource_destroy((*obj).resource);
+            curr = next;
+        }
+    }
+}
+
+unsafe extern "C" fn bind_libinput_config(
+    client: *mut ffi::wl_client,
+    data: *mut std::ffi::c_void,
+    version: u32,
+    id: u32,
+) {
+    let config = data as *mut LibinputConfig;
+    let resource = ffi::wl_resource_create(client, &ffi::river_libinput_config_v1_interface, version as i32, id);
+    if resource.is_null() {
+        ffi::wl_client_post_no_memory(client);
+        return;
+    }
+
+    let obj = Box::into_raw(Box::new(LibinputConfigObject {
+        config,
+        resource,
+        link: std::mem::zeroed(),
+    }));
+
+    ffi::wl_resource_set_implementation(
+        resource,
+        &LIBINPUT_CONFIG_INTERFACE as *const _ as *const _,
+        obj as *mut _,
+        Some(handle_object_destroy),
+    );
+
+    let list_head = &mut (*config).objects as *mut ffi::wl_list as *mut WlList;
+    wl_list_insert((*list_head).prev, &mut (*obj).link as *mut ffi::wl_list as *mut WlList);
+}
+
+unsafe extern "C" fn handle_object_destroy(resource: *mut ffi::wl_resource) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputConfigObject;
+    if !obj.is_null() {
+        wl_list_remove(&mut (*obj).link as *mut ffi::wl_list as *mut WlList);
+        let _ = Box::from_raw(obj);
+    }
+}
+
+static LIBINPUT_CONFIG_INTERFACE: ffi::river_libinput_config_v1_interface = ffi::river_libinput_config_v1_interface {
+    stop: Some(libinput_config_stop),
+    destroy: Some(libinput_config_destroy),
+    create_accel_config: Some(libinput_config_create_accel_config),
+};
+
+static LIBINPUT_CONFIG_INERT_INTERFACE: ffi::river_libinput_config_v1_interface = ffi::river_libinput_config_v1_interface {
+    stop: None,
+    destroy: Some(libinput_config_inert_destroy),
+    create_accel_config: None,
+};
+
+unsafe extern "C" fn libinput_config_stop(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputConfigObject;
+    if obj.is_null() {
+        return;
+    }
+
+    wl_list_remove(&mut (*obj).link as *mut ffi::wl_list as *mut WlList);
+    ffi::wl_list_init(&mut (*obj).link);
+
+    ffi::wl_resource_post_event(resource, 0); // finished event
+
+    ffi::wl_resource_set_implementation(
+        resource,
+        &LIBINPUT_CONFIG_INERT_INTERFACE as *const _ as *const _,
+        obj as *mut _,
+        Some(handle_object_destroy),
+    );
+}
+
+unsafe extern "C" fn libinput_config_destroy(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    ffi::wl_resource_post_error(
+        resource,
+        ffi::river_libinput_config_v1_error_RIVER_LIBINPUT_CONFIG_V1_ERROR_INVALID_DESTROY,
+        b"destroy before finished event sent\0".as_ptr() as *const _,
+    );
+}
+
+unsafe extern "C" fn libinput_config_inert_destroy(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn libinput_config_create_accel_config(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    id: u32,
+    _profile: u32,
+) {
+    let accel_res = ffi::wl_resource_create(
+        client,
+        &ffi::river_libinput_accel_config_v1_interface,
+        ffi::wl_resource_get_version(resource),
+        id,
+    );
+    if accel_res.is_null() {
+        ffi::wl_client_post_no_memory(client);
+        return;
+    }
+
+    static LIBINPUT_ACCEL_CONFIG_INTERFACE: ffi::river_libinput_accel_config_v1_interface = ffi::river_libinput_accel_config_v1_interface {
+        destroy: Some(libinput_accel_config_destroy),
+        set_points: Some(libinput_accel_config_set_points),
+    };
+
+    ffi::wl_resource_set_implementation(
+        accel_res,
+        &LIBINPUT_ACCEL_CONFIG_INTERFACE as *const _ as *const _,
+        std::ptr::null_mut(),
+        None,
+    );
+}
+
+unsafe extern "C" fn libinput_accel_config_destroy(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn libinput_accel_config_set_points(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    _accel_type: u32,
+    _step: *mut ffi::wl_array,
+    _points: *mut ffi::wl_array,
+) {
+    let result_res = ffi::wl_resource_create(
+        client,
+        &ffi::river_libinput_result_v1_interface,
+        ffi::wl_resource_get_version(resource),
+        result_id,
+    );
+    if !result_res.is_null() {
+        ffi::wl_resource_post_event(result_res, 1); // unsupported event
+        ffi::wl_resource_destroy(result_res);
+    }
+}
diff --git a/src/lock_manager.rs b/src/lock_manager.rs
new file mode 100644
index 0000000..daf9f9b
--- /dev/null
+++ b/src/lock_manager.rs
@@ -0,0 +1,430 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::server::{Server, WlListener, wl_signal_add, wl_listener_remove, WlList, wl_list_insert, wl_list_remove};
+use crate::scene_node_data::{SceneNodeData, SceneNodeDataVal};
+use crate::seat::Focus;
+
+pub struct LockManager {
+    pub wlr_manager: *mut ffi::wlr_session_lock_manager_v1,
+    pub state: LockState,
+    pub lock: *mut ffi::wlr_session_lock_v1,
+    pub lock_surfaces_timer: *mut ffi::wl_event_source,
+    pub server: *mut Server,
+
+    pub new_lock: ffi::wl_listener,
+    pub unlock: ffi::wl_listener,
+    pub destroy: ffi::wl_listener,
+    pub new_surface: ffi::wl_listener,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum LockState {
+    Unlocked,
+    Locked,
+    WaitingForBlank,
+    WaitingForLockSurfaces,
+}
+
+impl Default for LockManager {
+    fn default() -> Self {
+        unsafe { std::mem::zeroed() }
+    }
+}
+
+impl LockManager {
+    pub unsafe fn init(&mut self, server: *mut Server) -> Result<(), &'static str> {
+        self.server = server;
+        self.state = LockState::Unlocked;
+
+        let wlr_manager = ffi::wlr_session_lock_manager_v1_create((*server).wl_server);
+        if wlr_manager.is_null() {
+            return Err("Failed to create wlr_session_lock_manager_v1");
+        }
+        self.wlr_manager = wlr_manager;
+
+        let event_loop = ffi::wl_display_get_event_loop((*server).wl_server);
+        let timer = ffi::wl_event_loop_add_timer(
+            event_loop,
+            Some(handle_lock_surfaces_timeout),
+            self as *mut LockManager as *mut _,
+        );
+        if timer.is_null() {
+            return Err("Failed to create lock surfaces timer");
+        }
+        self.lock_surfaces_timer = timer;
+
+        let new_lock_ptr = &mut self.new_lock as *mut ffi::wl_listener as *mut WlListener;
+        (*new_lock_ptr).notify = Some(handle_new_lock);
+        wl_signal_add(&mut (*self.wlr_manager).events.new_lock, &mut self.new_lock);
+
+        Ok(())
+    }
+
+    pub unsafe fn deinit(&mut self) {
+        if !self.lock_surfaces_timer.is_null() {
+            ffi::wl_event_source_remove(self.lock_surfaces_timer);
+            self.lock_surfaces_timer = std::ptr::null_mut();
+        }
+        wl_listener_remove(&mut self.new_lock);
+    }
+
+    pub unsafe fn lock_surface_from_output(
+        &self,
+        output: *mut crate::output::Output,
+    ) -> Option<*mut LockSurface> {
+        if self.lock.is_null() {
+            return None;
+        }
+
+        let surfaces_head = &mut (*self.lock).surfaces as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*surfaces_head).next;
+        while curr != surfaces_head {
+            let next = (*curr).next;
+            let wlr_lock_surface = crate::container_of!(curr, ffi::wlr_session_lock_surface_v1, link);
+            let lock_surface = (*wlr_lock_surface).data as *mut LockSurface;
+            if !lock_surface.is_null() && (*lock_surface).get_output() == output {
+                return Some(lock_surface);
+            }
+            curr = next;
+        }
+
+        None
+    }
+
+    pub unsafe fn maybe_lock(&mut self) {
+        let mut all_outputs_blanked = true;
+        let mut all_outputs_rendered_lock_surface = true;
+
+        let outputs_head = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*outputs_head).next;
+        while curr != outputs_head {
+            let next = (*curr).next;
+            let output = crate::container_of!(curr, crate::output::Output, link);
+            let wlr_output = (*output).wlr_output;
+            if !wlr_output.is_null() && ffi::river_wlr_output_get_enabled(wlr_output) {
+                match (*output).lock_render_state {
+                    crate::output::LockRenderState::PendingUnlock
+                    | crate::output::LockRenderState::Unlocked
+                    | crate::output::LockRenderState::PendingBlank
+                    | crate::output::LockRenderState::PendingLockSurface => {
+                        all_outputs_blanked = false;
+                        all_outputs_rendered_lock_surface = false;
+                    }
+                    crate::output::LockRenderState::Blanked => {
+                        all_outputs_rendered_lock_surface = false;
+                    }
+                    crate::output::LockRenderState::LockSurface => {}
+                }
+            }
+            curr = next;
+        }
+
+        match self.state {
+            LockState::WaitingForLockSurfaces => {
+                if all_outputs_rendered_lock_surface {
+                    self.send_locked();
+                    ffi::wlr_scene_node_set_enabled((*self.server).scene.normal_tree as *mut ffi::wlr_scene_node, false);
+                    ffi::wl_event_source_timer_update(self.lock_surfaces_timer, 0);
+                }
+            }
+            LockState::WaitingForBlank => {
+                if all_outputs_blanked {
+                    self.send_locked();
+                }
+            }
+            _ => {}
+        }
+    }
+
+    pub unsafe fn send_locked(&mut self) {
+        log::info!("session locked");
+        if !self.lock.is_null() {
+            ffi::wlr_session_lock_v1_send_locked(self.lock);
+        }
+        self.state = LockState::Locked;
+        (*self.server).wm.dirty_windowing();
+    }
+}
+
+pub struct LockSurface {
+    pub tree: *mut ffi::wlr_scene_tree,
+    pub wlr_lock_surface: *mut ffi::wlr_session_lock_surface_v1,
+    pub lock: *mut ffi::wlr_session_lock_v1,
+    pub manager: *mut LockManager,
+
+    pub idle_update_focus: *mut ffi::wl_event_source,
+
+    pub map: ffi::wl_listener,
+    pub surface_destroy: ffi::wl_listener,
+}
+
+impl LockSurface {
+    pub unsafe fn create(
+        wlr_lock_surface: *mut ffi::wlr_session_lock_surface_v1,
+        lock: *mut ffi::wlr_session_lock_v1,
+        manager: *mut LockManager,
+    ) -> Result<*mut Self, &'static str> {
+        let tree = ffi::wlr_scene_subsurface_tree_create(
+            (*(*manager).server).scene.locked_tree,
+            (*wlr_lock_surface).surface,
+        );
+        if tree.is_null() {
+            return Err("Failed to create subsurface tree for lock surface");
+        }
+
+        let lock_surface = Box::into_raw(Box::new(Self {
+            tree,
+            wlr_lock_surface,
+            lock,
+            manager,
+            idle_update_focus: std::ptr::null_mut(),
+            map: std::mem::zeroed(),
+            surface_destroy: std::mem::zeroed(),
+        }));
+
+        (*wlr_lock_surface).data = lock_surface as *mut _;
+
+        SceneNodeData::attach(tree as *mut ffi::wlr_scene_node, SceneNodeDataVal::LockSurface(lock_surface));
+        ffi::river_wlr_surface_set_data((*wlr_lock_surface).surface, tree as *mut ffi::wlr_scene_node as *mut _);
+
+        let map_ptr = &mut (*lock_surface).map as *mut ffi::wl_listener as *mut WlListener;
+        (*map_ptr).notify = Some(handle_lock_surface_map);
+        wl_signal_add(
+            ffi::river_wlr_surface_get_map_signal((*wlr_lock_surface).surface),
+            &mut (*lock_surface).map,
+        );
+
+        let destroy_ptr = &mut (*lock_surface).surface_destroy as *mut ffi::wl_listener as *mut WlListener;
+        (*destroy_ptr).notify = Some(handle_lock_surface_destroy);
+        wl_signal_add(
+            &mut (*wlr_lock_surface).events.destroy,
+            &mut (*lock_surface).surface_destroy,
+        );
+
+        (*lock_surface).configure();
+
+        Ok(lock_surface)
+    }
+
+    pub unsafe fn destroy(lock_surface: *mut Self) {
+        let mut new_focus = Focus::None;
+        let surfaces_head = &mut (*(*lock_surface).lock).surfaces as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*surfaces_head).next;
+        while curr != surfaces_head {
+            let next = (*curr).next;
+            let wlr_lock_surface = crate::container_of!(curr, ffi::wlr_session_lock_surface_v1, link);
+            if wlr_lock_surface != (*lock_surface).wlr_lock_surface {
+                let other_surf = (*wlr_lock_surface).data as *mut LockSurface;
+                if !other_surf.is_null() {
+                    new_focus = Focus::LockSurface(other_surf);
+                    break;
+                }
+            }
+            curr = next;
+        }
+
+        let server = (*(*lock_surface).manager).server;
+        let seats_head = &mut (*server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*seats_head).next;
+        while curr != seats_head {
+            let next = (*curr).next;
+            let seat = crate::container_of!(curr, crate::seat::Seat, link);
+            if let Focus::LockSurface(focused_surf) = (*seat).focused {
+                if focused_surf == lock_surface {
+                    (*seat).focus(new_focus);
+                }
+            }
+            (*seat).cursor.update_state();
+            curr = next;
+        }
+
+        if !(*lock_surface).idle_update_focus.is_null() {
+            ffi::wl_event_source_remove((*lock_surface).idle_update_focus);
+        }
+
+        wl_listener_remove(&mut (*lock_surface).map);
+        wl_listener_remove(&mut (*lock_surface).surface_destroy);
+
+        ffi::river_wlr_surface_set_data((*(*lock_surface).wlr_lock_surface).surface, std::ptr::null_mut());
+
+        let _ = Box::from_raw(lock_surface);
+    }
+
+    pub unsafe fn get_output(&self) -> *mut crate::output::Output {
+        ffi::river_wlr_output_get_data((*self.wlr_lock_surface).output) as *mut crate::output::Output
+    }
+
+    pub unsafe fn configure(&self) {
+        let mut width: i32 = 0;
+        let mut height: i32 = 0;
+        ffi::wlr_output_effective_resolution((*self.wlr_lock_surface).output, &mut width, &mut height);
+        ffi::wlr_session_lock_surface_v1_configure(self.wlr_lock_surface, width as u32, height as u32);
+    }
+}
+
+unsafe extern "C" fn handle_lock_surfaces_timeout(data: *mut std::ffi::c_void) -> std::os::raw::c_int {
+    let manager = &mut *(data as *mut LockManager);
+    log::error!("waiting for lock surfaces timed out, imperfect frames may be shown");
+
+    assert!(manager.state == LockState::WaitingForLockSurfaces);
+    manager.state = LockState::WaitingForBlank;
+
+    ffi::wlr_scene_node_set_enabled((*manager.server).scene.normal_tree as *mut ffi::wlr_scene_node, false);
+
+    manager.maybe_lock();
+
+    0
+}
+
+unsafe extern "C" fn handle_new_lock(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let manager = &mut *crate::container_of!(listener, LockManager, new_lock);
+    let lock = data as *mut ffi::wlr_session_lock_v1;
+
+    log::debug!("session lock client made lock request");
+
+    if !manager.lock.is_null() {
+        log::info!("denying new session lock client, an active one already exists");
+        ffi::wlr_session_lock_v1_destroy(lock);
+        return;
+    }
+
+    manager.lock = lock;
+
+    if manager.state == LockState::Unlocked {
+        manager.state = LockState::WaitingForLockSurfaces;
+
+        ffi::wlr_scene_node_set_enabled((*manager.server).scene.locked_tree as *mut ffi::wlr_scene_node, true);
+
+        ffi::wl_event_source_timer_update(manager.lock_surfaces_timer, 200);
+
+        let seats_head = &mut (*manager.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*seats_head).next;
+        while curr != seats_head {
+            let next = (*curr).next;
+            let seat = crate::container_of!(curr, crate::seat::Seat, link);
+            (*seat).focus(Focus::None);
+            curr = next;
+        }
+    } else {
+        if manager.state == LockState::Locked {
+            ffi::wlr_session_lock_v1_send_locked(lock);
+        }
+        log::info!("new session lock client given control of already locked session");
+    }
+
+    let unlock_ptr = &mut manager.unlock as *mut ffi::wl_listener as *mut WlListener;
+    (*unlock_ptr).notify = Some(handle_unlock);
+    wl_signal_add(&mut (*lock).events.unlock, &mut manager.unlock);
+
+    let destroy_ptr = &mut manager.destroy as *mut ffi::wl_listener as *mut WlListener;
+    (*destroy_ptr).notify = Some(handle_destroy);
+    wl_signal_add(&mut (*lock).events.destroy, &mut manager.destroy);
+
+    let new_surface_ptr = &mut manager.new_surface as *mut ffi::wl_listener as *mut WlListener;
+    (*new_surface_ptr).notify = Some(handle_surface);
+    wl_signal_add(&mut (*lock).events.new_surface, &mut manager.new_surface);
+}
+
+unsafe extern "C" fn handle_unlock(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let manager = &mut *crate::container_of!(listener, LockManager, unlock);
+
+    manager.state = LockState::Unlocked;
+    log::info!("session unlocked");
+
+    ffi::wlr_scene_node_set_enabled((*manager.server).scene.normal_tree as *mut ffi::wlr_scene_node, true);
+    ffi::wlr_scene_node_set_enabled((*manager.server).scene.locked_tree as *mut ffi::wlr_scene_node, false);
+
+    let seats_head = &mut (*manager.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+    let mut curr = (*seats_head).next;
+    while curr != seats_head {
+        let next = (*curr).next;
+        let seat = crate::container_of!(curr, crate::seat::Seat, link);
+        (*seat).focus(Focus::None);
+        curr = next;
+    }
+
+    handle_destroy(&mut manager.destroy, std::ptr::null_mut());
+
+    (*manager.server).wm.dirty_windowing();
+}
+
+unsafe extern "C" fn handle_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let manager = &mut *crate::container_of!(listener, LockManager, destroy);
+
+    log::debug!("ext_session_lock_v1 destroyed");
+
+    wl_listener_remove(&mut manager.new_surface);
+    wl_listener_remove(&mut manager.unlock);
+    wl_listener_remove(&mut manager.destroy);
+
+    manager.lock = std::ptr::null_mut();
+    if manager.state == LockState::WaitingForLockSurfaces {
+        manager.state = LockState::WaitingForBlank;
+        ffi::wl_event_source_timer_update(manager.lock_surfaces_timer, 0);
+    }
+}
+
+unsafe extern "C" fn handle_surface(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let manager = &mut *crate::container_of!(listener, LockManager, new_surface);
+    let wlr_lock_surface = data as *mut ffi::wlr_session_lock_surface_v1;
+
+    log::debug!("new ext_session_lock_surface_v1 created");
+
+    assert!(manager.state != LockState::Unlocked);
+    assert!(!manager.lock.is_null());
+
+    if LockSurface::create(wlr_lock_surface, manager.lock, manager).is_err() {
+        log::error!("out of memory");
+        ffi::wl_resource_post_no_memory((*wlr_lock_surface).resource);
+    }
+}
+
+unsafe extern "C" fn update_focus(data: *mut std::ffi::c_void) {
+    let lock_surface = data as *mut LockSurface;
+    let manager = (*lock_surface).manager;
+
+    let seats_head = &mut (*(*manager).server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+    let mut curr = (*seats_head).next;
+    while curr != seats_head {
+        let next = (*curr).next;
+        let seat = crate::container_of!(curr, crate::seat::Seat, link);
+        if !matches!((*seat).focused, Focus::LockSurface(s) if s == lock_surface) {
+            (*seat).focus(Focus::LockSurface(lock_surface));
+        }
+        (*seat).cursor.update_state();
+        curr = next;
+    }
+
+    (*lock_surface).idle_update_focus = std::ptr::null_mut();
+}
+
+unsafe extern "C" fn handle_lock_surface_map(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let lock_surface = crate::container_of!(listener, LockSurface, map);
+
+    let output = (*lock_surface).get_output();
+    let x = (*output).sent.x;
+    let y = (*output).sent.y;
+    ffi::wlr_scene_node_set_position((*lock_surface).tree as *mut ffi::wlr_scene_node, x, y);
+
+    let server = (*(*lock_surface).manager).server;
+    let event_loop = ffi::wl_display_get_event_loop((*server).wl_server);
+    assert!((*lock_surface).idle_update_focus.is_null());
+
+    let idle = ffi::wl_event_loop_add_idle(
+        event_loop,
+        Some(update_focus),
+        lock_surface as *mut _,
+    );
+    if idle.is_null() {
+        log::error!("Failed to create idle update focus event source");
+        return;
+    }
+    (*lock_surface).idle_update_focus = idle;
+}
+
+unsafe extern "C" fn handle_lock_surface_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let lock_surface = crate::container_of!(listener, LockSurface, surface_destroy);
+    LockSurface::destroy(lock_surface);
+}
diff --git a/src/main.rs b/src/main.rs
index e7a11a9..780802a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,268 @@
+// SPDX-FileCopyrightText: © 2020 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+mod ffi;
+mod server;
+mod process;
+mod util;
+mod slotmap;
+mod window_manager;
+mod xkb_bindings;
+mod layer_shell;
+mod scene;
+mod scene_node_data;
+mod output;
+mod output_manager;
+mod input_manager;
+mod libinput_config;
+mod xkb_config;
+mod idle_inhibit_manager;
+mod lock_manager;
+mod input_device;
+mod pointer_constraint;
+mod keyboard;
+mod cursor;
+mod seat;
+pub mod tablet;
+pub mod tablet_tool;
+pub mod window;
+pub mod xdg_toplevel;
+pub mod xdg_popup;
+pub mod shell_surface;
+pub mod wm_node;
+pub mod xwayland_window;
+pub mod xwayland_override_redirect;
+pub mod text_input;
+pub mod input_relay;
+pub mod input_popup;
+pub mod drag_icon;
+pub mod pointer_binding;
+pub mod keyboard_group;
+
+
+use clap::Parser;
+use std::ffi::{CStr, CString};
+use std::os::raw::c_char;
+
+
+const USAGE: &str = "\
+usage: river [options]
+
+  -h, --help           Print this help message and exit.
+  --version            Print the version number and exit.
+  -c <command>         Run `sh -c <command>` on startup instead of the default init executable.
+  --log-level <level>  Set the log level to error, warning, info, or debug.
+  --no-xwayland        Disable xwayland even if built with support.
+";
+
+#[derive(Parser, Debug)]
+#[command(disable_help_flag = true)]
+struct Args {
+    #[arg(short = 'h', long = "help")]
+    help: bool,
+
+    #[arg(long = "version")]
+    version: bool,
+
+    #[arg(short = 'c')]
+    command: Option<String>,
+
+    #[arg(long = "log-level", default_value = "info")]
+    log_level: String,
+
+    #[arg(long = "no-xwayland")]
+    no_xwayland: bool,
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn river_wlroots_log_callback(
+    importance: ffi::wlr_log_importance,
+    ptr: *const c_char,
+    len: usize,
+) {
+    let message_slice = std::slice::from_raw_parts(ptr as *const u8, len);
+    let message = String::from_utf8_lossy(message_slice);
+    let message_trimmed = message.trim();
+
+    match importance {
+        ffi::wlr_log_importance_WLR_ERROR => log::error!(target: "wlroots", "{}", message_trimmed),
+        ffi::wlr_log_importance_WLR_INFO => log::info!(target: "wlroots", "{}", message_trimmed),
+        ffi::wlr_log_importance_WLR_DEBUG => log::debug!(target: "wlroots", "{}", message_trimmed),
+        _ => {}
+    }
+}
+
+fn default_init_path() -> Option<String> {
+    let path = if let Ok(xdg_config_home) = std::env::var("XDG_CONFIG_HOME") {
+        format!("{}/river/init", xdg_config_home)
+    } else if let Ok(home) = std::env::var("HOME") {
+        format!("{}/.config/river/init", home)
+    } else {
+        return None;
+    };
+
+    if std::path::Path::new(&path).exists() {
+        Some(path)
+    } else {
+        None
+    }
+}
+
+fn detect_classic(path: &str) {
+    if let Ok(content) = std::fs::read_to_string(path) {
+        if content.contains("riverctl") {
+            log::error!(
+                "The init file {} contains the string \"riverctl\".\n\
+                 This version of river does not support riverctl.\n\
+                 See https://isaacfreund.com/software/river for more information.",
+                path
+            );
+            std::process::exit(1);
+        }
+    }
+}
+
 fn main() {
-    println!("Hello, world!");
+    let args = match Args::try_parse() {
+        Ok(a) => a,
+        Err(_) => {
+            eprintln!("{}", USAGE);
+            std::process::exit(1);
+        }
+    };
+
+    if args.help {
+        println!("{}", USAGE);
+        std::process::exit(0);
+    }
+
+    if args.version {
+        println!("0.5.0-dev -xwayland (Rust rewrite starter)");
+        std::process::exit(0);
+    }
+
+    let log_level = match args.log_level.as_str() {
+        "error" => log::LevelFilter::Error,
+        "warning" => log::LevelFilter::Warn,
+        "info" => log::LevelFilter::Info,
+        "debug" => log::LevelFilter::Debug,
+        _ => log::LevelFilter::Info,
+    };
+
+    env_logger::Builder::new()
+        .filter(None, log_level)
+        .init();
+
+    log::info!("initializing river (Rust rewrite)");
+
+    let importance = match log_level {
+        log::LevelFilter::Debug => ffi::wlr_log_importance_WLR_DEBUG,
+        log::LevelFilter::Info => ffi::wlr_log_importance_WLR_INFO,
+        _ => ffi::wlr_log_importance_WLR_ERROR,
+    };
+
+    unsafe {
+        ffi::river_init_wlroots_log(importance);
+    }
+
+    let startup_command = if let Some(cmd) = args.command {
+        Some(cmd)
+    } else {
+        default_init_path()
+    };
+
+    if let Some(ref cmd) = startup_command {
+        detect_classic(cmd);
+    }
+
+    let mut server = server::Server::default();
+    if let Err(e) = server.init(!args.no_xwayland) {
+        log::error!("failed to initialize server: {}", e);
+        std::process::exit(1);
+    }
+
+    process::setup();
+
+    let socket_ptr = unsafe {
+        ffi::wl_display_add_socket_auto(server.wl_server)
+    };
+    if socket_ptr.is_null() {
+        log::error!("failed to add wayland socket");
+        server.deinit();
+        std::process::exit(1);
+    }
+    let socket_str = unsafe { CStr::from_ptr(socket_ptr).to_string_lossy().into_owned() };
+    log::info!("running server on display socket: {}", socket_str);
+
+    let started = unsafe { ffi::wlr_backend_start(server.backend) };
+    if !started {
+        log::error!("failed to start wlr_backend");
+        server.deinit();
+        std::process::exit(1);
+    }
+
+    struct ChildGuard(Option<nix::unistd::Pid>);
+    impl Drop for ChildGuard {
+        fn drop(&mut self) {
+            if let Some(pid) = self.0 {
+                log::info!("sending SIGTERM to child process group {}", pid);
+                let _ = nix::sys::signal::kill(
+                    nix::unistd::Pid::from_raw(-pid.as_raw()),
+                    nix::sys::signal::Signal::SIGTERM,
+                );
+            }
+        }
+    }
+
+    let child_pgid = if let Some(ref cmd) = startup_command {
+        log::info!("running init executable '{}'", cmd);
+        unsafe {
+            match nix::unistd::fork() {
+                Ok(nix::unistd::ForkResult::Child) => {
+                    process::cleanup_child();
+                    std::env::set_var("WAYLAND_DISPLAY", &socket_str);
+
+                    if !args.no_xwayland && !server.xwayland.is_null() {
+                        let xwayland_cast = server.xwayland as *mut server::WlrXwayland;
+                        if !(*xwayland_cast).display_name.is_null() {
+                            let display_name = CStr::from_ptr((*xwayland_cast).display_name)
+                                .to_string_lossy()
+                                .into_owned();
+                            std::env::set_var("DISPLAY", display_name);
+                        }
+                    }
+
+                    let cmd_c = CString::new(cmd.clone()).unwrap();
+                    let sh_c = CString::new("/bin/sh").unwrap();
+                    let c_c = CString::new("-c").unwrap();
+                    let args = [sh_c.as_c_str(), c_c.as_c_str(), cmd_c.as_c_str()];
+                    
+                    let env: Vec<CString> = std::env::vars()
+                        .map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap())
+                        .collect();
+                    let env_ptrs: Vec<&CStr> = env.iter().map(|s| s.as_c_str()).collect();
+
+                    let _ = nix::unistd::execve(&sh_c, &args, &env_ptrs);
+                    std::process::exit(1);
+                }
+                Ok(nix::unistd::ForkResult::Parent { child }) => Some(child),
+                Err(_) => {
+                    log::error!("failed to fork child process");
+                    None
+                }
+            }
+        }
+    } else {
+        None
+    };
+
+    let _guard = ChildGuard(child_pgid);
+
+    log::info!("running server");
+    unsafe {
+        ffi::wl_display_run(server.wl_server);
+    }
+
+    log::info!("shutting down server");
+    server.deinit();
 }
diff --git a/src/output.rs b/src/output.rs
new file mode 100644
index 0000000..fac4ca6
--- /dev/null
+++ b/src/output.rs
@@ -0,0 +1,422 @@
+// SPDX-FileCopyrightText: © 2020 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::server::{Server, WlListener, WlList, wl_signal_add, wl_listener_remove, wl_list_insert, wl_list_remove};
+use crate::layer_shell::LayerShellOutput;
+use crate::lock_manager::{LockSurface, LockState};
+use crate::util;
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum OutputStateValue {
+    Enabled,
+    DisabledSoft,
+    DisabledHard,
+    Destroying,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum OutputMode {
+    Standard(*mut ffi::wlr_output_mode),
+    Custom {
+        width: i32,
+        height: i32,
+        refresh: i32,
+    },
+    None,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct OutputState {
+    pub state: OutputStateValue,
+    pub x: i32,
+    pub y: i32,
+    pub mode: OutputMode,
+    pub scale: f32,
+    pub transform: ffi::wl_output_transform,
+    pub adaptive_sync: bool,
+    pub auto_layout: bool,
+}
+
+impl OutputState {
+    pub fn mode_none(&self) -> bool {
+        matches!(self.mode, OutputMode::None)
+    }
+
+    pub unsafe fn from_head_state(state: *const ffi::wlr_output_head_v1_state) -> Self {
+        assert!((*state).enabled);
+        let mode = if !(*state).mode.is_null() {
+            OutputMode::Standard((*state).mode)
+        } else {
+            OutputMode::Custom {
+                width: (*state).custom_mode.width,
+                height: (*state).custom_mode.height,
+                refresh: (*state).custom_mode.refresh,
+            }
+        };
+
+        // Round to nearest 1/120 to ensure the scale is exactly represented
+        // in the fractional-scale-v1 protocol.
+        let scale = ((*state).scale * 120.0).round() / 120.0;
+
+        Self {
+            state: OutputStateValue::Enabled,
+            mode,
+            x: (*state).x,
+            y: (*state).y,
+            scale,
+            transform: (*state).transform,
+            adaptive_sync: (*state).adaptive_sync_enabled,
+            auto_layout: false,
+        }
+    }
+
+    pub unsafe fn dimensions(&self) -> (i32, i32) {
+        let (mut w, mut h) = match self.mode {
+            OutputMode::Standard(mode) => ((*mode).width, (*mode).height),
+            OutputMode::Custom { width, height, .. } => (width, height),
+            OutputMode::None => (0, 0),
+        };
+        if (self.transform as u32) % 2 != 0 {
+            std::mem::swap(&mut w, &mut h);
+        }
+        (
+            ((w as f32) / self.scale) as i32,
+            ((h as f32) / self.scale) as i32,
+        )
+    }
+
+    pub unsafe fn box_layout(&self) -> ffi::wlr_box {
+        let (w, h) = self.dimensions();
+        ffi::wlr_box {
+            x: self.x,
+            y: self.y,
+            width: w,
+            height: h,
+        }
+    }
+
+    pub unsafe fn apply_no_modeset(&self, wlr_state: *mut ffi::wlr_output_state) {
+        ffi::wlr_output_state_set_scale(wlr_state, self.scale);
+        ffi::wlr_output_state_set_transform(wlr_state, self.transform);
+    }
+
+    pub unsafe fn apply_modeset(&self, wlr_state: *mut ffi::wlr_output_state) {
+        let enabled = self.state == OutputStateValue::Enabled;
+        ffi::wlr_output_state_set_enabled(wlr_state, enabled);
+        if !enabled {
+            return;
+        }
+        self.apply_no_modeset(wlr_state);
+        match self.mode {
+            OutputMode::Standard(mode) => {
+                ffi::wlr_output_state_set_mode(wlr_state, mode);
+            }
+            OutputMode::Custom { width, height, refresh } => {
+                ffi::wlr_output_state_set_custom_mode(wlr_state, width, height, refresh);
+            }
+            OutputMode::None => {}
+        }
+        ffi::wlr_output_state_set_adaptive_sync_enabled(wlr_state, self.adaptive_sync);
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum LockRenderState {
+    PendingUnlock,
+    Unlocked,
+    PendingBlank,
+    Blanked,
+    PendingLockSurface,
+    LockSurface,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct RenderingState {
+    pub tearing: bool,
+}
+
+pub struct Output {
+    pub server: *mut Server,
+    pub wlr_output: *mut ffi::wlr_output,
+    pub scene_output: *mut ffi::wlr_scene_output,
+    pub object: *mut ffi::wl_resource, // river_output_v1 resource
+    pub layer_shell: LayerShellOutput,
+    pub lock_render_state: LockRenderState,
+    pub link: ffi::wl_list,
+    pub link_sent: ffi::wl_list,
+    pub scheduled: OutputState,
+    pub sent: OutputState,
+    pub current: OutputState,
+    pub sent_wl_output: bool,
+    pub rendering_requested: RenderingState,
+    pub rendering_current: RenderingState,
+
+    pub destroy: ffi::wl_listener,
+    pub request_state: ffi::wl_listener,
+    pub frame: ffi::wl_listener,
+    pub present: ffi::wl_listener,
+}
+
+impl Output {
+    pub unsafe fn make_inert(&mut self) {
+        if !self.object.is_null() {
+            ffi::wl_resource_post_event(self.object, 0); // river_output.removed
+            ffi::wl_resource_set_implementation(
+                self.object,
+                std::ptr::null(),
+                std::ptr::null_mut(),
+                None,
+            );
+            self.layer_shell.make_inert();
+            self.object = std::ptr::null_mut();
+            self.sent_wl_output = false;
+        }
+    }
+
+    pub unsafe fn create(server: *mut Server, wlr_output: *mut ffi::wlr_output) -> Result<(), &'static str> {
+        let title = format!("river - {}\0", std::ffi::CStr::from_ptr(ffi::river_wlr_output_get_name(wlr_output)).to_string_lossy());
+        
+        // Check if output is Wayland/X11 and set application title/app_id
+        if ffi::wlr_output_is_wl(wlr_output) {
+            ffi::wlr_wl_output_set_app_id(wlr_output, "river\0".as_ptr() as *const _);
+            ffi::wlr_wl_output_set_title(wlr_output, title.as_ptr() as *const _);
+        } else if ffi::wlr_output_is_x11(wlr_output) {
+            ffi::wlr_x11_output_set_title(wlr_output, title.as_ptr() as *const _);
+        }
+
+        if !ffi::wlr_output_init_render(wlr_output, (*server).allocator, (*server).renderer) {
+            return Err("Failed to initialize renderer for output");
+        }
+
+        let scene_output = ffi::wlr_scene_output_create((*server).scene.wlr_scene, wlr_output);
+        if scene_output.is_null() {
+            return Err("Failed to create wlr_scene_output");
+        }
+
+        let initial = OutputState {
+            state: OutputStateValue::DisabledHard,
+            x: 0,
+            y: 0,
+            mode: OutputMode::None,
+            scale: 1.0,
+            transform: ffi::wl_output_transform_WL_OUTPUT_TRANSFORM_NORMAL,
+            adaptive_sync: ffi::river_wlr_output_get_adaptive_sync_status(wlr_output) == ffi::wlr_output_adaptive_sync_status_WLR_OUTPUT_ADAPTIVE_SYNC_ENABLED,
+            auto_layout: true,
+        };
+
+        let output = Box::new(Output {
+            server,
+            wlr_output,
+            scene_output,
+            object: std::ptr::null_mut(),
+            layer_shell: LayerShellOutput::default(),
+            lock_render_state: LockRenderState::Blanked,
+            link: std::mem::zeroed(),
+            link_sent: std::mem::zeroed(),
+            scheduled: initial,
+            sent: initial,
+            current: initial,
+            sent_wl_output: false,
+            rendering_requested: RenderingState { tearing: false },
+            rendering_current: RenderingState { tearing: false },
+            destroy: std::mem::zeroed(),
+            request_state: std::mem::zeroed(),
+            frame: std::mem::zeroed(),
+            present: std::mem::zeroed(),
+        });
+
+        let raw = Box::into_raw(output);
+        ffi::river_wlr_output_set_data(wlr_output, raw as *mut std::ffi::c_void);
+
+        let list_head = &mut (*server).om.outputs as *mut ffi::wl_list as *mut WlList;
+        let link_custom = &mut (*raw).link as *mut ffi::wl_list as *mut WlList;
+        wl_list_insert((*list_head).prev, link_custom);
+        
+        ffi::wl_list_init(&mut (*raw).link_sent);
+
+        // Add event listeners
+        let d_listener = &mut (*raw).destroy as *mut ffi::wl_listener as *mut WlListener;
+        (*d_listener).notify = Some(handle_destroy);
+        wl_signal_add(ffi::river_wlr_output_get_destroy_signal(wlr_output), &mut (*raw).destroy);
+
+        let req_listener = &mut (*raw).request_state as *mut ffi::wl_listener as *mut WlListener;
+        (*req_listener).notify = Some(handle_request_state);
+        wl_signal_add(ffi::river_wlr_output_get_request_state_signal(wlr_output), &mut (*raw).request_state);
+
+        let frame_listener = &mut (*raw).frame as *mut ffi::wl_listener as *mut WlListener;
+        (*frame_listener).notify = Some(handle_frame);
+        wl_signal_add(ffi::river_wlr_output_get_frame_signal(wlr_output), &mut (*raw).frame);
+
+        let pres_listener = &mut (*raw).present as *mut ffi::wl_listener as *mut WlListener;
+        (*pres_listener).notify = Some(handle_present);
+        wl_signal_add(ffi::river_wlr_output_get_present_signal(wlr_output), &mut (*raw).present);
+
+        (*raw).scheduled.state = OutputStateValue::Enabled;
+        let preferred = ffi::wlr_output_preferred_mode(wlr_output);
+        if !preferred.is_null() {
+            (*raw).scheduled.mode = OutputMode::Standard(preferred);
+        } else {
+            (*raw).scheduled.mode = OutputMode::Custom { width: 1280, height: 720, refresh: 0 };
+        }
+        // Trigger windowing manager update
+        // We will implement the actual server.wm.dirty_windowing() call in Phase 4, but for now we can stub it
+        // by log output or call a direct FFI / loop callback if needed.
+        log::debug!("Output created, dirty windowing");
+        Ok(())
+    }
+
+    pub unsafe fn render_and_commit(&mut self) -> Result<(), &'static str> {
+        if !ffi::wlr_scene_output_needs_frame(self.scene_output) {
+            return Ok(());
+        }
+
+        let mut state = std::mem::zeroed();
+        ffi::wlr_output_state_init(&mut state);
+        
+        self.current.apply_no_modeset(&mut state);
+
+        if !ffi::wlr_scene_output_build_state(self.scene_output, &mut state, std::ptr::null()) {
+            ffi::wlr_output_state_finish(&mut state);
+            return Err("Failed to build scene state");
+        }
+
+        if self.rendering_current.tearing {
+            state.tearing_page_flip = true;
+            if !ffi::wlr_output_test_state(self.wlr_output, &state) {
+                state.tearing_page_flip = false;
+            }
+        }
+
+        if !ffi::wlr_output_commit_state(self.wlr_output, &state) {
+            ffi::wlr_output_state_finish(&mut state);
+            return Err("Failed to commit state");
+        }
+
+        ffi::wlr_output_state_finish(&mut state);
+
+        match (*self.server).lock_manager.state {
+            LockState::Unlocked => {
+                if self.lock_render_state != LockRenderState::Unlocked {
+                    self.lock_render_state = LockRenderState::PendingUnlock;
+                }
+            }
+            LockState::Locked => {
+                // Assert normal tree disabled, lock surface rendered
+            }
+            LockState::WaitingForBlank => {
+                if self.lock_render_state != LockRenderState::Blanked {
+                    self.lock_render_state = LockRenderState::PendingBlank;
+                }
+            }
+            LockState::WaitingForLockSurfaces => {
+                if let Some(_lock_surf) = (*self.server).lock_manager.lock_surface_from_output(self) {
+                    if self.lock_render_state != LockRenderState::LockSurface {
+                        self.lock_render_state = LockRenderState::PendingLockSurface;
+                    }
+                } else {
+                    if self.lock_render_state != LockRenderState::Unlocked {
+                        self.lock_render_state = LockRenderState::PendingUnlock;
+                    }
+                }
+            }
+        }
+
+        Ok(())
+    }
+}
+
+unsafe extern "C" fn handle_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let output_ptr = crate::container_of!(listener, Output, destroy);
+    let mut output = Box::from_raw(output_ptr);
+
+    log::debug!("Output destroyed");
+
+    // Remove listeners
+    wl_listener_remove(&mut output.destroy);
+    wl_listener_remove(&mut output.request_state);
+    wl_listener_remove(&mut output.frame);
+    wl_listener_remove(&mut output.present);
+
+    // Remove from outputs list
+    let link_custom = &mut output.link as *mut ffi::wl_list as *mut WlList;
+    wl_list_remove(link_custom);
+
+    let link_sent_custom = &mut output.link_sent as *mut ffi::wl_list as *mut WlList;
+    wl_list_remove(link_sent_custom);
+
+    if !output.wlr_output.is_null() {
+        ffi::river_wlr_output_set_data(output.wlr_output, std::ptr::null_mut());
+    }
+}
+
+unsafe extern "C" fn handle_request_state(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let output = &mut *crate::container_of!(listener, Output, request_state);
+    let event = data as *mut ffi::wlr_output_event_request_state;
+
+    let committed: u32 = std::mem::transmute((*(*event).state).committed);
+    // mode field is bit 1 (mode)
+    if committed & 1 != 0 {
+        if !(*(*event).state).mode.is_null() {
+            output.scheduled.mode = OutputMode::Standard((*(*event).state).mode);
+        } else {
+            output.scheduled.mode = OutputMode::Custom {
+                width: (*(*event).state).custom_mode.width,
+                height: (*(*event).state).custom_mode.height,
+                refresh: (*(*event).state).custom_mode.refresh,
+            };
+        }
+    }
+}
+
+unsafe extern "C" fn handle_frame(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let output = &mut *crate::container_of!(listener, Output, frame);
+    if let Err(e) = output.render_and_commit() {
+        log::error!("{}", e);
+    }
+    let now = util::timestamp();
+    let mut ffi_now = ffi::timespec {
+        tv_sec: now.tv_sec,
+        tv_nsec: now.tv_nsec,
+    };
+    ffi::wlr_scene_output_send_frame_done(output.scene_output, &mut ffi_now);
+}
+
+unsafe extern "C" fn handle_present(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let output = &mut *crate::container_of!(listener, Output, present);
+    let event = data as *mut ffi::wlr_output_event_present;
+    if !(*event).presented {
+        return;
+    }
+    match output.lock_render_state {
+        LockRenderState::PendingUnlock => {
+            output.lock_render_state = LockRenderState::Unlocked;
+        }
+        LockRenderState::PendingBlank => {
+            output.lock_render_state = LockRenderState::Blanked;
+            (*output.server).lock_manager.maybe_lock();
+        }
+        LockRenderState::PendingLockSurface => {
+            output.lock_render_state = LockRenderState::LockSurface;
+            (*output.server).lock_manager.maybe_lock();
+        }
+        _ => {}
+    }
+}
+
+// Helpers for raw Wayland FFI protocol events
+pub unsafe fn river_output_send_removed(resource: *mut ffi::wl_resource) {
+    ffi::wl_resource_post_event(resource, 0);
+}
+
+pub unsafe fn river_output_send_wl_output(resource: *mut ffi::wl_resource, name: u32) {
+    ffi::wl_resource_post_event(resource, 1, name);
+}
+
+pub unsafe fn river_output_send_position(resource: *mut ffi::wl_resource, x: i32, y: i32) {
+    ffi::wl_resource_post_event(resource, 2, x, y);
+}
+
+pub unsafe fn river_output_send_dimensions(resource: *mut ffi::wl_resource, width: i32, height: i32) {
+    ffi::wl_resource_post_event(resource, 3, width, height);
+}
diff --git a/src/output_manager.rs b/src/output_manager.rs
new file mode 100644
index 0000000..7db4d3f
--- /dev/null
+++ b/src/output_manager.rs
@@ -0,0 +1,648 @@
+// SPDX-FileCopyrightText: © 2020 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::server::{Server, WlListener, WlList, wl_signal_add, wl_listener_remove, wl_list_remove};
+use crate::output::{Output, OutputStateValue, OutputMode};
+
+#[repr(C)]
+pub struct WlrOutputManagerV1Events {
+    pub apply: ffi::wl_signal,
+    pub test: ffi::wl_signal,
+    pub destroy: ffi::wl_signal,
+}
+
+#[repr(C)]
+pub struct WlrOutputManagerV1 {
+    pub display: *mut ffi::wl_display,
+    pub global: *mut ffi::wl_global,
+    pub resources: ffi::wl_list,
+    pub heads: ffi::wl_list,
+    pub serial: u32,
+    pub current_configuration_dirty: bool,
+    pub events: WlrOutputManagerV1Events,
+}
+
+#[repr(C)]
+pub struct WlrOutputPowerManagerV1Events {
+    pub set_mode: ffi::wl_signal,
+    pub destroy: ffi::wl_signal,
+}
+
+#[repr(C)]
+pub struct WlrOutputPowerManagerV1 {
+    pub global: *mut ffi::wl_global,
+    pub output_powers: ffi::wl_list,
+    pub events: WlrOutputPowerManagerV1Events,
+}
+
+pub struct OutputManager {
+    pub first_modeset: bool,
+    pub new_output: ffi::wl_listener,
+    pub output_layout: *mut ffi::wlr_output_layout,
+    pub presentation: *mut ffi::wlr_presentation,
+    pub xdg_output_manager: *mut ffi::wlr_xdg_output_manager_v1,
+    pub wlr_output_manager: *mut ffi::wlr_output_manager_v1,
+    pub manager_apply: ffi::wl_listener,
+    pub manager_test: ffi::wl_listener,
+    pub power_manager: *mut ffi::wlr_output_power_manager_v1,
+    pub power_manager_set_mode: ffi::wl_listener,
+    pub gamma_control_manager: *mut ffi::wlr_gamma_control_manager_v1,
+    pub outputs: ffi::wl_list,
+}
+
+impl OutputManager {
+    pub unsafe fn init(&mut self, server: *mut Server) -> Result<(), &'static str> {
+        self.first_modeset = true;
+        
+        let output_layout = ffi::wlr_output_layout_create((*server).wl_server);
+        if output_layout.is_null() {
+            return Err("Failed to create wlr_output_layout");
+        }
+        self.output_layout = output_layout;
+
+        let gamma_control_manager = ffi::wlr_gamma_control_manager_v1_create((*server).wl_server);
+        if gamma_control_manager.is_null() {
+            ffi::wlr_output_layout_destroy(output_layout);
+            return Err("Failed to create wlr_gamma_control_manager_v1");
+        }
+        self.gamma_control_manager = gamma_control_manager;
+        ffi::wlr_scene_set_gamma_control_manager_v1((*server).scene.wlr_scene, gamma_control_manager);
+
+        let presentation = ffi::wlr_presentation_create((*server).wl_server, (*server).backend, 2);
+        if presentation.is_null() {
+            return Err("Failed to create wlr_presentation");
+        }
+        self.presentation = presentation;
+
+        let xdg_output_manager = ffi::wlr_xdg_output_manager_v1_create((*server).wl_server, output_layout);
+        if xdg_output_manager.is_null() {
+            return Err("Failed to create wlr_xdg_output_manager_v1");
+        }
+        self.xdg_output_manager = xdg_output_manager;
+
+        let wlr_output_manager = ffi::wlr_output_manager_v1_create((*server).wl_server);
+        if wlr_output_manager.is_null() {
+            return Err("Failed to create wlr_output_manager_v1");
+        }
+        self.wlr_output_manager = wlr_output_manager;
+
+        let power_manager = ffi::wlr_output_power_manager_v1_create((*server).wl_server);
+        if power_manager.is_null() {
+            return Err("Failed to create wlr_output_power_manager_v1");
+        }
+        self.power_manager = power_manager;
+
+        ffi::wl_list_init(&mut self.outputs);
+
+        // Add backend new_output listener
+        let backend_cast = (*server).backend as *mut crate::server::WlrBackend;
+        let new_output_ptr = &mut self.new_output as *mut ffi::wl_listener as *mut WlListener;
+        (*new_output_ptr).notify = Some(handle_new_output);
+        wl_signal_add(&mut (*backend_cast).events.new_output, &mut self.new_output);
+
+        // Add apply/test listeners
+        let manager_cast = self.wlr_output_manager as *mut WlrOutputManagerV1;
+        
+        let apply_ptr = &mut self.manager_apply as *mut ffi::wl_listener as *mut WlListener;
+        (*apply_ptr).notify = Some(handle_manager_apply);
+        wl_signal_add(&mut (*manager_cast).events.apply, &mut self.manager_apply);
+
+        let test_ptr = &mut self.manager_test as *mut ffi::wl_listener as *mut WlListener;
+        (*test_ptr).notify = Some(handle_manager_test);
+        wl_signal_add(&mut (*manager_cast).events.test, &mut self.manager_test);
+
+        // Add power manager set_mode listener
+        let power_cast = self.power_manager as *mut WlrOutputPowerManagerV1;
+        let set_mode_ptr = &mut self.power_manager_set_mode as *mut ffi::wl_listener as *mut WlListener;
+        (*set_mode_ptr).notify = Some(handle_power_manager_set_mode);
+        wl_signal_add(&mut (*power_cast).events.set_mode, &mut self.power_manager_set_mode);
+
+        Ok(())
+    }
+
+    pub unsafe fn deinit(&mut self) {
+        wl_listener_remove(&mut self.manager_apply);
+        wl_listener_remove(&mut self.manager_test);
+        wl_listener_remove(&mut self.power_manager_set_mode);
+        wl_listener_remove(&mut self.new_output);
+
+        if !self.output_layout.is_null() {
+            ffi::wlr_output_layout_destroy(self.output_layout);
+        }
+    }
+
+    pub unsafe fn output_at(&mut self, lx: f64, ly: f64) -> *mut ffi::wlr_output {
+        let mut output_lx: f64 = 0.0;
+        let mut output_ly: f64 = 0.0;
+        ffi::wlr_output_layout_closest_point(
+            self.output_layout,
+            std::ptr::null_mut(),
+            lx,
+            ly,
+            &mut output_lx,
+            &mut output_ly,
+        );
+        ffi::wlr_output_layout_output_at(self.output_layout, output_lx, output_ly)
+    }
+
+    pub unsafe fn auto_layout(&mut self) {
+        let mut rightmost_edge: i32 = 0;
+        let mut row_y: i32 = 0;
+
+        let mut link = self.outputs.next;
+        while link != &mut self.outputs as *mut ffi::wl_list {
+            let output = &mut *crate::container_of!(link, Output, link);
+            if !output.scheduled.auto_layout {
+                let (w, _) = output.scheduled.dimensions();
+                let x = output.scheduled.x + w;
+                if x > rightmost_edge {
+                    rightmost_edge = x;
+                    row_y = output.scheduled.y;
+                }
+            }
+            link = (*link).next;
+        }
+
+        let mut link = self.outputs.next;
+        while link != &mut self.outputs as *mut ffi::wl_list {
+            let output = &mut *crate::container_of!(link, Output, link);
+            if output.scheduled.auto_layout {
+                output.scheduled.x = rightmost_edge;
+                output.scheduled.y = row_y;
+                let (w, _) = output.scheduled.dimensions();
+                rightmost_edge += w;
+            }
+            link = (*link).next;
+        }
+    }
+
+    pub unsafe fn commit_output_state(&mut self, server: *mut Server) {
+        let wm = &mut (*server).wm;
+
+        let mut link = wm.sent.outputs.next;
+        while link != &mut wm.sent.outputs as *mut ffi::wl_list {
+            let output = &mut *crate::container_of!(link, Output, link_sent);
+            assert!(output.sent.state != OutputStateValue::Destroying);
+            output.rendering_current = output.rendering_requested;
+
+            let wlr_output = output.wlr_output;
+            if !wlr_output.is_null() {
+                match output.sent.state {
+                    OutputStateValue::Enabled | OutputStateValue::DisabledSoft => {
+                        ffi::wlr_scene_output_set_position(output.scene_output, output.sent.x, output.sent.y);
+                        ffi::wlr_output_layout_add(self.output_layout, wlr_output, output.sent.x, output.sent.y);
+                        if let Some(lock_surface) = (*server).lock_manager.lock_surface_from_output(output) {
+                            ffi::wlr_scene_node_set_position(
+                                (*lock_surface).tree as *mut ffi::wlr_scene_node,
+                                output.sent.x,
+                                output.sent.y,
+                            );
+                        }
+                    }
+                    OutputStateValue::DisabledHard => {
+                        ffi::wlr_output_layout_remove(self.output_layout, wlr_output);
+                    }
+                    OutputStateValue::Destroying => unreachable!(),
+                }
+            }
+            link = (*link).next;
+        }
+
+        let mut need_modeset = false;
+        let mut link = wm.sent.outputs.next;
+        while link != &mut wm.sent.outputs as *mut ffi::wl_list {
+            let output = &mut *crate::container_of!(link, Output, link_sent);
+            let wlr_output = output.wlr_output;
+            if wlr_output.is_null() {
+                link = (*link).next;
+                continue;
+            }
+
+            let wlr_enabled = ffi::river_wlr_output_get_enabled(wlr_output);
+            match output.sent.state {
+                OutputStateValue::Enabled => {
+                    if !wlr_enabled {
+                        need_modeset = true;
+                        break;
+                    }
+                }
+                OutputStateValue::DisabledSoft | OutputStateValue::DisabledHard => {
+                    if wlr_enabled {
+                        need_modeset = true;
+                        break;
+                    }
+                }
+                OutputStateValue::Destroying => unreachable!(),
+            }
+
+            match output.sent.mode {
+                OutputMode::Standard(mode) => {
+                    if mode != ffi::river_wlr_output_get_current_mode(wlr_output) {
+                        need_modeset = true;
+                        break;
+                    }
+                }
+                OutputMode::Custom { width, height, refresh } => {
+                    if width != ffi::river_wlr_output_get_width(wlr_output) 
+                        || height != ffi::river_wlr_output_get_height(wlr_output) 
+                        || refresh != ffi::river_wlr_output_get_refresh(wlr_output) {
+                        need_modeset = true;
+                        break;
+                    }
+                }
+                OutputMode::None => {
+                    assert!(output.sent.state == OutputStateValue::DisabledHard);
+                }
+            }
+
+            if output.current.mode_none() && output.sent.state == OutputStateValue::Enabled {
+                need_modeset = true;
+                break;
+            }
+
+            let wlr_adaptive = ffi::river_wlr_output_get_adaptive_sync_status(wlr_output)
+                == ffi::wlr_output_adaptive_sync_status_WLR_OUTPUT_ADAPTIVE_SYNC_ENABLED;
+            if output.sent.adaptive_sync != wlr_adaptive {
+                need_modeset = true;
+                break;
+            }
+
+            link = (*link).next;
+        }
+
+        if need_modeset {
+            log::debug!("committing output state requires modeset");
+
+            let mut states_vec = Vec::new();
+            let mut link = wm.sent.outputs.next;
+            while link != &mut wm.sent.outputs as *mut ffi::wl_list {
+                let output = &mut *crate::container_of!(link, Output, link_sent);
+                let wlr_output = output.wlr_output;
+                if wlr_output.is_null() {
+                    link = (*link).next;
+                    continue;
+                }
+
+                let mut state = std::mem::zeroed();
+                ffi::wlr_output_state_init(&mut state);
+                output.sent.apply_modeset(&mut state);
+
+                states_vec.push(ffi::wlr_backend_output_state {
+                    output: wlr_output,
+                    base: state,
+                });
+
+                link = (*link).next;
+            }
+
+            let mut swapchain_manager = std::mem::zeroed();
+            ffi::wlr_output_swapchain_manager_init(&mut swapchain_manager, (*server).backend);
+
+            if !ffi::wlr_output_swapchain_manager_prepare(
+                &mut swapchain_manager,
+                states_vec.as_ptr(),
+                states_vec.len(),
+            ) {
+                log::error!("failed to prepare new output configuration");
+                self.modeset_failed(server);
+                for state in &mut states_vec {
+                    ffi::wlr_output_state_finish(&mut state.base);
+                }
+                ffi::wlr_output_swapchain_manager_finish(&mut swapchain_manager);
+                return;
+            }
+
+            for state in &mut states_vec {
+                let output = &mut *(ffi::river_wlr_output_get_data(state.output) as *mut Output);
+                let sc = ffi::wlr_output_swapchain_manager_get_swapchain(&mut swapchain_manager, state.output);
+                let options = ffi::wlr_scene_output_state_options {
+                    swapchain: sc,
+                    ..std::mem::zeroed()
+                };
+                if !ffi::wlr_scene_output_build_state(output.scene_output, &mut state.base, &options) {
+                    log::error!("failed to render scene for {:?}", std::ffi::CStr::from_ptr(ffi::river_wlr_output_get_name(state.output)));
+                }
+            }
+
+            if !ffi::wlr_backend_commit((*server).backend, states_vec.as_ptr(), states_vec.len()) {
+                log::error!("failed to commit new output configuration");
+                self.modeset_failed(server);
+                for state in &mut states_vec {
+                    ffi::wlr_output_state_finish(&mut state.base);
+                }
+                ffi::wlr_output_swapchain_manager_finish(&mut swapchain_manager);
+                return;
+            }
+
+            self.first_modeset = false;
+            ffi::wlr_output_swapchain_manager_apply(&mut swapchain_manager);
+
+            for mut state in states_vec {
+                ffi::wlr_output_state_finish(&mut state.base);
+            }
+            ffi::wlr_output_swapchain_manager_finish(&mut swapchain_manager);
+        }
+
+        if !wm.sent.output_config.is_null() {
+            ffi::wlr_output_configuration_v1_send_succeeded(wm.sent.output_config);
+            ffi::wlr_output_configuration_v1_destroy(wm.sent.output_config);
+            wm.sent.output_config = std::ptr::null_mut();
+        }
+
+        let mut link = wm.sent.outputs.next;
+        while link != &mut wm.sent.outputs as *mut ffi::wl_list {
+            let next_link = (*link).next;
+            let output = &mut *crate::container_of!(link, Output, link_sent);
+            let wlr_output = output.wlr_output;
+            if wlr_output.is_null() {
+                link = next_link;
+                continue;
+            }
+
+            if !output.sent_wl_output {
+                let global = ffi::river_wlr_output_get_global(wlr_output);
+                if !global.is_null() {
+                    if !output.object.is_null() {
+                        let name = ffi::wl_global_get_name(global, ffi::wl_resource_get_client(output.object));
+                        crate::output::river_output_send_wl_output(output.object, name);
+                        output.sent_wl_output = true;
+                    }
+                }
+            }
+
+            match output.sent.state {
+                OutputStateValue::Enabled => {
+                    assert!(ffi::river_wlr_output_get_enabled(wlr_output));
+                    ffi::wlr_output_schedule_frame(wlr_output);
+                }
+                OutputStateValue::DisabledSoft | OutputStateValue::DisabledHard => {
+                    assert!(!ffi::river_wlr_output_get_enabled(wlr_output));
+                    output.lock_render_state = crate::output::LockRenderState::Blanked;
+                    if output.sent.state == OutputStateValue::DisabledHard {
+                        wl_list_remove(&mut output.link_sent as *mut ffi::wl_list as *mut WlList);
+                        ffi::wl_list_init(&mut output.link_sent);
+                    }
+                }
+                OutputStateValue::Destroying => unreachable!(),
+            }
+
+            output.current = output.sent;
+            link = next_link;
+        }
+
+        let _ = self.send_config(server);
+    }
+
+    pub unsafe fn modeset_failed(&mut self, server: *mut Server) {
+        let wm = &mut (*server).wm;
+
+        if self.first_modeset {
+            log::error!("initial modeset failed, exiting river");
+            ffi::wl_display_terminate((*server).wl_server);
+            return;
+        }
+
+        if !wm.sent.output_config.is_null() {
+            ffi::wlr_output_configuration_v1_send_failed(wm.sent.output_config);
+            ffi::wlr_output_configuration_v1_destroy(wm.sent.output_config);
+            wm.sent.output_config = std::ptr::null_mut();
+        }
+
+        let mut link = wm.sent.outputs.next;
+        while link != &mut wm.sent.outputs as *mut ffi::wl_list {
+            let output = &mut *crate::container_of!(link, Output, link_sent);
+            output.scheduled = output.current;
+            output.sent = output.current;
+            link = (*link).next;
+        }
+        wm.dirty_windowing();
+    }
+
+    pub unsafe fn send_config(&mut self, server: *mut Server) -> Result<(), &'static str> {
+        let config = ffi::wlr_output_configuration_v1_create();
+        if config.is_null() {
+            return Err("Failed to create configuration v1");
+        }
+
+        let mut link = self.outputs.next;
+        while link != &mut self.outputs as *mut ffi::wl_list {
+            let output = &mut *crate::container_of!(link, Output, link);
+            let wlr_output = output.wlr_output;
+            if wlr_output.is_null() {
+                link = (*link).next;
+                continue;
+            }
+
+            let head = ffi::wlr_output_configuration_head_v1_create(config, wlr_output);
+            if head.is_null() {
+                ffi::wlr_output_configuration_v1_destroy(config);
+                return Err("Failed to create configuration head");
+            }
+
+            (*head).state.enabled = match output.current.state {
+                OutputStateValue::Enabled | OutputStateValue::DisabledSoft => true,
+                OutputStateValue::DisabledHard => false,
+                OutputStateValue::Destroying => unreachable!(),
+            };
+            (*head).state.scale = output.current.scale;
+            (*head).state.transform = output.current.transform;
+            (*head).state.x = output.current.x;
+            (*head).state.y = output.current.y;
+
+            link = (*link).next;
+        }
+
+        ffi::wlr_output_manager_v1_set_configuration(self.wlr_output_manager, config);
+        Ok(())
+    }
+
+    pub unsafe fn max_overlap_output(&mut self, box_: *const ffi::wlr_box) -> *mut ffi::wlr_output {
+        let mut max_overlap_area = 0;
+        let mut max_overlap_output = std::ptr::null_mut();
+
+        let mut link = self.outputs.next;
+        while link != &mut self.outputs as *mut ffi::wl_list {
+            let output = &mut *crate::container_of!(link, Output, link);
+            let wlr_output = output.wlr_output;
+            if wlr_output.is_null() {
+                link = (*link).next;
+                continue;
+            }
+
+            let mut overlap: ffi::wlr_box = std::mem::zeroed();
+            ffi::wlr_output_layout_get_box(self.output_layout, wlr_output, &mut overlap);
+            
+            if overlap.width <= 0 || overlap.height <= 0 {
+                link = (*link).next;
+                continue;
+            }
+
+            let mut dest: ffi::wlr_box = std::mem::zeroed();
+            if ffi::wlr_box_intersection(&mut dest, &overlap, box_) {
+                let overlap_area = dest.width * dest.height;
+                if overlap_area > max_overlap_area {
+                    max_overlap_area = overlap_area;
+                    max_overlap_output = wlr_output;
+                }
+            }
+
+            link = (*link).next;
+        }
+
+        max_overlap_output
+    }
+}
+
+fn validate_config_coordinates(server: *mut Server, config: *mut ffi::wlr_output_configuration_v1) -> bool {
+    unsafe {
+        let mut head_link = (*config).heads.next;
+        while head_link != &mut (*config).heads {
+            let head = crate::container_of!(head_link, ffi::wlr_output_configuration_head_v1, link);
+            let output_global = ffi::river_wlr_output_get_global((*head).state.output);
+            
+            if (*head).state.enabled {
+                let proposed = crate::output::OutputState::from_head_state(&((*head).state));
+                if !(*server).xwayland.is_null() {
+                    if proposed.x < 0 || proposed.y < 0 {
+                        log::error!(
+                            "Attempted to set negative coordinates for output. Negative output coordinates are disallowed if Xwayland is active."
+                        );
+                        return false;
+                    }
+                    let (width, height) = proposed.dimensions();
+                    if proposed.x + width > i16::MAX as i32 || proposed.y + height > i16::MAX as i32 {
+                        log::error!(
+                            "Attempted to set too-large coordinates for output. Coordinates greater than {} are disallowed if Xwayland is active.",
+                            i16::MAX
+                        );
+                        return false;
+                    }
+                }
+            }
+            head_link = (*head_link).next;
+        }
+    }
+    true
+}
+
+unsafe extern "C" fn handle_new_output(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let om = &mut *crate::container_of!(listener, OutputManager, new_output);
+    let server = crate::container_of!(om as *mut OutputManager, Server, om);
+    let wlr_output = data as *mut ffi::wlr_output;
+    log::debug!("new output {:?}", std::ffi::CStr::from_ptr(ffi::river_wlr_output_get_name(wlr_output)));
+    if let Err(e) = Output::create(server, wlr_output) {
+        log::error!("failed to create output: {}", e);
+        ffi::wlr_output_destroy(wlr_output);
+    }
+}
+
+unsafe extern "C" fn handle_manager_test(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let om = &mut *crate::container_of!(listener, OutputManager, manager_test);
+    let server = crate::container_of!(om as *mut OutputManager, Server, om);
+    let config = data as *mut ffi::wlr_output_configuration_v1;
+
+    if !validate_config_coordinates(server, config) {
+        ffi::wlr_output_configuration_v1_send_failed(config);
+        ffi::wlr_output_configuration_v1_destroy(config);
+        return;
+    }
+
+    let mut states_len: usize = 0;
+    let states = ffi::wlr_output_configuration_v1_build_state(config, &mut states_len);
+    if states.is_null() {
+        log::error!("out of memory");
+        ffi::wlr_output_configuration_v1_send_failed(config);
+        ffi::wlr_output_configuration_v1_destroy(config);
+        return;
+    }
+
+    let mut swapchain_manager = std::mem::zeroed();
+    ffi::wlr_output_swapchain_manager_init(&mut swapchain_manager, (*server).backend);
+
+    if ffi::wlr_output_swapchain_manager_prepare(&mut swapchain_manager, states, states_len) {
+        ffi::wlr_output_configuration_v1_send_succeeded(config);
+    } else {
+        ffi::wlr_output_configuration_v1_send_failed(config);
+    }
+
+    ffi::wlr_output_swapchain_manager_finish(&mut swapchain_manager);
+    libc::free(states as *mut std::ffi::c_void);
+    ffi::wlr_output_configuration_v1_destroy(config);
+}
+
+unsafe extern "C" fn handle_manager_apply(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let om = &mut *crate::container_of!(listener, OutputManager, manager_apply);
+    let server = crate::container_of!(om as *mut OutputManager, Server, om);
+    let config = data as *mut ffi::wlr_output_configuration_v1;
+
+    log::info!("applying output configuration");
+
+    if !validate_config_coordinates(server, config) {
+        ffi::wlr_output_configuration_v1_send_failed(config);
+        ffi::wlr_output_configuration_v1_destroy(config);
+        return;
+    }
+
+    let mut head_link = (*config).heads.next;
+    while head_link != &mut (*config).heads {
+        let head = crate::container_of!(head_link, ffi::wlr_output_configuration_head_v1, link);
+        let output = &mut *(ffi::river_wlr_output_get_data((*head).state.output) as *mut Output);
+        
+        if (*head).state.enabled {
+            let previous = output.scheduled.state;
+            output.scheduled = crate::output::OutputState::from_head_state(&((*head).state));
+            if previous == crate::output::OutputStateValue::DisabledSoft {
+                output.scheduled.state = crate::output::OutputStateValue::DisabledSoft;
+            } else {
+                assert!(output.scheduled.state == crate::output::OutputStateValue::Enabled);
+            }
+        } else {
+            output.scheduled.state = crate::output::OutputStateValue::DisabledHard;
+        }
+
+        head_link = (*head_link).next;
+    }
+
+    if !(*server).wm.scheduled.output_config.is_null() {
+        ffi::wlr_output_configuration_v1_send_failed((*server).wm.scheduled.output_config);
+        ffi::wlr_output_configuration_v1_destroy((*server).wm.scheduled.output_config);
+    }
+    (*server).wm.scheduled.output_config = config;
+
+    (*server).wm.dirty_windowing();
+}
+
+unsafe extern "C" fn handle_power_manager_set_mode(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let om = &mut *crate::container_of!(listener, OutputManager, power_manager_set_mode);
+    let server = crate::container_of!(om as *mut OutputManager, Server, om);
+    let event = data as *mut ffi::wlr_output_power_v1_set_mode_event;
+
+    let output_data = ffi::river_wlr_output_get_data((*event).output);
+    if output_data.is_null() {
+        return;
+    }
+    let output = &mut *(output_data as *mut Output);
+
+    log::debug!("client requested dpms mode {:?} for output {:?}", (*event).mode, std::ffi::CStr::from_ptr(ffi::river_wlr_output_get_name((*event).output)));
+
+    match output.scheduled.state {
+        OutputStateValue::Enabled => {
+            if (*event).mode == ffi::zwlr_output_power_v1_mode_ZWLR_OUTPUT_POWER_V1_MODE_OFF {
+                output.scheduled.state = OutputStateValue::DisabledSoft;
+            } else {
+                return;
+            }
+        }
+        OutputStateValue::DisabledSoft => {
+            if (*event).mode == ffi::zwlr_output_power_v1_mode_ZWLR_OUTPUT_POWER_V1_MODE_ON {
+                output.scheduled.state = OutputStateValue::Enabled;
+            } else {
+                return;
+            }
+        }
+        OutputStateValue::DisabledHard | OutputStateValue::Destroying => return,
+    }
+
+    (*server).wm.dirty_windowing();
+}
diff --git a/src/pointer_binding.rs b/src/pointer_binding.rs
new file mode 100644
index 0000000..cfc2115
--- /dev/null
+++ b/src/pointer_binding.rs
@@ -0,0 +1,172 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::seat::Seat;
+
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub enum PointerBindingStateChange {
+    None,
+    Pressed,
+    Released,
+}
+
+pub struct PointerBindingScheduled {
+    pub state_change: PointerBindingStateChange,
+}
+
+pub struct PointerBindingRequested {
+    pub enabled: bool,
+}
+
+pub struct PointerBinding {
+    pub seat: *mut Seat,
+    pub object: *mut ffi::wl_resource,
+    pub button: u32,
+    pub modifiers: u32,
+    pub wm_scheduled: PointerBindingScheduled,
+    pub wm_requested: PointerBindingRequested,
+    pub sent_pressed: bool,
+    pub link: ffi::wl_list,
+}
+
+impl PointerBinding {
+    pub unsafe fn create(
+        seat: *mut Seat,
+        client: *mut ffi::wl_client,
+        version: u32,
+        id: u32,
+        button: u32,
+        modifiers: u32,
+    ) -> Result<(), &'static str> {
+        let binding_ptr = Box::into_raw(Box::new(Self {
+            seat,
+            object: std::ptr::null_mut(),
+            button,
+            modifiers,
+            wm_scheduled: PointerBindingScheduled {
+                state_change: PointerBindingStateChange::None,
+            },
+            wm_requested: PointerBindingRequested {
+                enabled: false,
+            },
+            sent_pressed: false,
+            link: std::mem::zeroed(),
+        }));
+
+        let resource = ffi::wl_resource_create(client, &ffi::river_pointer_binding_v1_interface, version as i32, id);
+        if resource.is_null() {
+            let _ = Box::from_raw(binding_ptr);
+            return Err("wl_resource_create failed");
+        }
+
+        (*binding_ptr).object = resource;
+        ffi::wl_resource_set_implementation(
+            resource,
+            &POINTER_BINDING_INTERFACE as *const _ as *const _,
+            binding_ptr as *mut _,
+            Some(handle_binding_resource_destroy),
+        );
+
+        let pointer_bindings_list = &mut (*seat).pointer_bindings as *mut ffi::wl_list as *mut crate::server::WlList;
+        crate::server::wl_list_insert((*pointer_bindings_list).prev, &mut (*binding_ptr).link as *mut ffi::wl_list as *mut crate::server::WlList);
+
+        log::debug!(
+            "new river_pointer_binding_v1: button: {} modifiers: {}",
+            button,
+            modifiers
+        );
+
+        Ok(())
+    }
+
+    pub unsafe fn destroy(binding: *mut Self) {
+        ffi::wl_resource_set_implementation(
+            (*binding).object,
+            std::ptr::null(),
+            std::ptr::null_mut(),
+            None,
+        );
+        handle_binding_resource_destroy((*binding).object);
+    }
+
+    pub unsafe fn pressed(&mut self) {
+        assert!(!self.sent_pressed);
+        assert!(matches!(self.wm_scheduled.state_change, PointerBindingStateChange::None));
+        self.wm_scheduled.state_change = PointerBindingStateChange::Pressed;
+        (*(*self.seat).server).wm.dirty_windowing();
+    }
+
+    pub unsafe fn released(&mut self) {
+        assert!(self.sent_pressed);
+        assert!(matches!(self.wm_scheduled.state_change, PointerBindingStateChange::None));
+        self.wm_scheduled.state_change = PointerBindingStateChange::Released;
+        (*(*self.seat).server).wm.dirty_windowing();
+    }
+
+    pub fn match_binding(&self, button: u32, modifiers: u32) -> bool {
+        if !self.wm_requested.enabled {
+            return false;
+        }
+        button == self.button && modifiers == self.modifiers
+    }
+}
+
+unsafe extern "C" fn handle_binding_resource_destroy(resource: *mut ffi::wl_resource) {
+    let binding = ffi::wl_resource_get_user_data(resource) as *mut PointerBinding;
+    if !binding.is_null() {
+        // It is possible for the window manager to create duplicate pointer bindings.
+        let cursor = &mut (*(*binding).seat).cursor;
+        if let Some(val) = cursor.pressed.get_mut(&(*binding).button) {
+            if *val == Some(binding) {
+                *val = None;
+            }
+        }
+
+        crate::server::wl_list_remove(&mut (*binding).link as *mut ffi::wl_list as *mut crate::server::WlList);
+        let _ = Box::from_raw(binding);
+    }
+}
+
+static POINTER_BINDING_INTERFACE: ffi::river_pointer_binding_v1_interface = ffi::river_pointer_binding_v1_interface {
+    destroy: Some(pointer_binding_destroy),
+    enable: Some(pointer_binding_enable),
+    disable: Some(pointer_binding_disable),
+};
+
+unsafe extern "C" fn pointer_binding_destroy(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn pointer_binding_enable(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let binding = ffi::wl_resource_get_user_data(resource) as *mut PointerBinding;
+    if binding.is_null() {
+        return;
+    }
+    let server = (*(*binding).seat).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    (*binding).wm_requested.enabled = true;
+}
+
+unsafe extern "C" fn pointer_binding_disable(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let binding = ffi::wl_resource_get_user_data(resource) as *mut PointerBinding;
+    if binding.is_null() {
+        return;
+    }
+    let server = (*(*binding).seat).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    (*binding).wm_requested.enabled = false;
+}
diff --git a/src/pointer_constraint.rs b/src/pointer_constraint.rs
new file mode 100644
index 0000000..4b7629d
--- /dev/null
+++ b/src/pointer_constraint.rs
@@ -0,0 +1,259 @@
+use crate::ffi;
+use crate::seat::{Seat, Focus};
+use crate::server::{WlListener, wl_listener_remove, wl_signal_add};
+
+#[derive(Clone, Copy)]
+pub enum PointerConstraintState {
+    Inactive,
+    Active {
+        node: *mut ffi::wlr_scene_node,
+        sx: f64,
+        sy: f64,
+    },
+}
+
+pub struct PointerConstraint {
+    pub wlr_constraint: *mut ffi::wlr_pointer_constraint_v1,
+    pub state: PointerConstraintState,
+    pub destroy_listener: ffi::wl_listener,
+    pub commit_listener: ffi::wl_listener,
+    pub node_destroy_listener: ffi::wl_listener,
+}
+
+impl PointerConstraint {
+    pub unsafe fn create(wlr_constraint: *mut ffi::wlr_pointer_constraint_v1) -> *mut Self {
+        let constraint = Box::into_raw(Box::new(Self {
+            wlr_constraint,
+            state: PointerConstraintState::Inactive,
+            destroy_listener: std::mem::zeroed(),
+            commit_listener: std::mem::zeroed(),
+            node_destroy_listener: std::mem::zeroed(),
+        }));
+
+        (*wlr_constraint).data = constraint as *mut _;
+
+        let destroy_listener_ptr = &mut (*constraint).destroy_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*destroy_listener_ptr).notify = Some(handle_destroy);
+        
+        let commit_listener_ptr = &mut (*constraint).commit_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*commit_listener_ptr).notify = Some(handle_commit);
+
+        let node_destroy_listener_ptr = &mut (*constraint).node_destroy_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*node_destroy_listener_ptr).notify = Some(handle_node_destroy);
+
+        wl_signal_add(&mut (*wlr_constraint).events.destroy, &mut (*constraint).destroy_listener);
+        let commit_signal = ffi::river_wlr_surface_get_commit_signal((*wlr_constraint).surface);
+        wl_signal_add(commit_signal, &mut (*constraint).commit_listener);
+
+        let seat = river_wlr_seat_get_data_safe((*wlr_constraint).seat);
+        if !seat.is_null() {
+            if let Focus::LayerSurface(surface) = (*seat).focused {
+                if surface == (*wlr_constraint).surface {
+                    (*seat).cursor.constraint = constraint;
+                    (*constraint).maybe_activate();
+                }
+            }
+        }
+
+        constraint
+    }
+
+    pub unsafe fn maybe_activate(&mut self) {
+        let seat = river_wlr_seat_get_data_safe((*self.wlr_constraint).seat);
+        if seat.is_null() {
+            return;
+        }
+
+        if (*seat).cursor.constraint != self {
+            return;
+        }
+
+        if matches!(self.state, PointerConstraintState::Active { .. }) {
+            return;
+        }
+
+        // Retrieve cursor coordinates
+        let cx = (*seat).cursor.x();
+        let cy = (*seat).cursor.y();
+
+        let server = (*seat).server;
+        if let Some(result) = (*server).scene.at(cx, cy) {
+            if result.surface != (*self.wlr_constraint).surface {
+                return;
+            }
+
+            let sx = result.sx;
+            let sy = result.sy;
+
+            // Check if within the pixman region
+            if ffi::pixman_region32_contains_point(&mut (*self.wlr_constraint).region, sx as i32, sy as i32, std::ptr::null_mut()) == 0 {
+                return;
+            }
+
+            self.state = PointerConstraintState::Active {
+                node: result.node,
+                sx,
+                sy,
+            };
+
+            let destroy_signal = ffi::river_scene_node_get_destroy_signal(result.node);
+            wl_signal_add(destroy_signal, &mut self.node_destroy_listener);
+
+            log::info!("activating pointer constraint");
+            ffi::wlr_pointer_constraint_v1_send_activated(self.wlr_constraint);
+        }
+    }
+
+    pub unsafe fn deactivate(&mut self) {
+        if !matches!(self.state, PointerConstraintState::Active { .. }) {
+            return;
+        }
+
+        self.warp_to_hint_if_set();
+
+        self.state = PointerConstraintState::Inactive;
+        wl_listener_remove(&mut self.node_destroy_listener);
+        ffi::wlr_pointer_constraint_v1_send_deactivated(self.wlr_constraint);
+    }
+
+    pub unsafe fn update_state(&mut self) {
+        self.maybe_activate();
+
+        if let PointerConstraintState::Active { node, sx, sy } = self.state {
+            let seat = river_wlr_seat_get_data_safe((*self.wlr_constraint).seat);
+            if seat.is_null() {
+                return;
+            }
+
+            let mut lx = 0;
+            let mut ly = 0;
+            // Get coordinates of scene node relative to layout
+            // In wlroots, scene node coords are retrieved
+            if !wlr_scene_node_coords_safe(node, &mut lx, &mut ly) {
+                log::info!("deactivating pointer constraint, scene node disabled");
+                self.deactivate();
+                return;
+            }
+
+            let warp_lx = lx as f64 + sx;
+            let warp_ly = ly as f64 + sy;
+
+            if !ffi::wlr_cursor_warp((*seat).cursor.wlr_cursor, std::ptr::null_mut(), warp_lx, warp_ly) {
+                log::info!("deactivating pointer constraint, could not warp cursor");
+                self.deactivate();
+                return;
+            }
+
+            if ffi::pixman_region32_contains_point(&mut (*self.wlr_constraint).region, sx as i32, sy as i32, std::ptr::null_mut()) == 0 {
+                log::info!("deactivating pointer constraint, cursor outside region despite warp");
+                self.deactivate();
+            }
+        }
+    }
+
+    pub unsafe fn confine(&mut self, dx: &mut f64, dy: &mut f64) {
+        if let PointerConstraintState::Active { sx, sy, node } = self.state {
+            let mut new_sx = 0.0;
+            let mut new_sy = 0.0;
+
+            // Call wlroots / pixman confine helper
+            // river does: wlr.region.confine(region, sx, sy, sx + dx, sy + dy, &new_sx, &new_sy)
+            // If the region contains the line segment or we can confine it.
+            // Let's implement confinement using wlroots wlr_region_confine (or similar, or custom simple confinement).
+            // Actually, wlroots has: wlr_region_confine(struct pixman_region32 *region, double sx1, double sy1, double sx2, double sy2, double *ak_sx, double *ak_sy)
+            // Let's call it:
+            if ffi::wlr_region_confine(&mut (*self.wlr_constraint).region, sx, sy, sx + *dx, sy + *dy, &mut new_sx, &mut new_sy) {
+                *dx = new_sx - sx;
+                *dy = new_sy - sy;
+
+                self.state = PointerConstraintState::Active {
+                    node,
+                    sx: new_sx,
+                    sy: new_sy,
+                };
+            }
+        }
+    }
+
+    unsafe fn warp_to_hint_if_set(&mut self) {
+        let seat = river_wlr_seat_get_data_safe((*self.wlr_constraint).seat);
+        if seat.is_null() {
+            return;
+        }
+
+        if (*self.wlr_constraint).current.cursor_hint.enabled {
+            if let PointerConstraintState::Active { node, .. } = self.state {
+                let mut lx = 0;
+                let mut ly = 0;
+                wlr_scene_node_coords_safe(node, &mut lx, &mut ly);
+
+                let sx = (*self.wlr_constraint).current.cursor_hint.x;
+                let sy = (*self.wlr_constraint).current.cursor_hint.y;
+                ffi::wlr_cursor_warp((*seat).cursor.wlr_cursor, std::ptr::null_mut(), lx as f64 + sx, ly as f64 + sy);
+                ffi::wlr_seat_pointer_warp((*seat).wlr_seat, sx, sy);
+            }
+        }
+    }
+}
+
+unsafe fn river_wlr_seat_get_data_safe(wlr_seat: *mut ffi::wlr_seat) -> *mut Seat {
+    if wlr_seat.is_null() {
+        std::ptr::null_mut()
+    } else {
+        ffi::river_wlr_seat_get_data(wlr_seat) as *mut Seat
+    }
+}
+
+unsafe fn wlr_scene_node_coords_safe(node: *mut ffi::wlr_scene_node, x: *mut i32, y: *mut i32) -> bool {
+    if node.is_null() {
+        false
+    } else {
+        ffi::wlr_scene_node_coords(node, x, y)
+    }
+}
+
+unsafe extern "C" fn handle_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let constraint_ptr = crate::container_of!(listener, PointerConstraint, destroy_listener) as *mut PointerConstraint;
+    let constraint = &mut *constraint_ptr;
+    let seat = river_wlr_seat_get_data_safe((*constraint.wlr_constraint).seat);
+
+    if matches!(constraint.state, PointerConstraintState::Active { .. }) {
+        constraint.warp_to_hint_if_set();
+        wl_listener_remove(&mut constraint.node_destroy_listener);
+    }
+
+    wl_listener_remove(&mut constraint.destroy_listener);
+    wl_listener_remove(&mut constraint.commit_listener);
+
+    if !seat.is_null() && (*seat).cursor.constraint == constraint_ptr {
+        (*seat).cursor.constraint = std::ptr::null_mut();
+    }
+
+    let _boxed = Box::from_raw(constraint_ptr);
+}
+
+unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let constraint_ptr = crate::container_of!(listener, PointerConstraint, commit_listener) as *mut PointerConstraint;
+    let constraint = &mut *constraint_ptr;
+    let seat = river_wlr_seat_get_data_safe((*constraint.wlr_constraint).seat);
+
+    match constraint.state {
+        PointerConstraintState::Active { sx, sy, .. } => {
+            if ffi::pixman_region32_contains_point(&mut (*constraint.wlr_constraint).region, sx as i32, sy as i32, std::ptr::null_mut()) == 0 {
+                log::info!("deactivating pointer constraint, input region change left pointer outside");
+                constraint.deactivate();
+            }
+        }
+        PointerConstraintState::Inactive => {
+            if !seat.is_null() && (*seat).cursor.constraint == constraint_ptr {
+                constraint.maybe_activate();
+            }
+        }
+    }
+}
+
+unsafe extern "C" fn handle_node_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let constraint_ptr = crate::container_of!(listener, PointerConstraint, node_destroy_listener) as *mut PointerConstraint;
+    log::info!("deactivating pointer constraint, scene node destroyed");
+    (*constraint_ptr).deactivate();
+}
diff --git a/src/process.rs b/src/process.rs
new file mode 100644
index 0000000..def8814
--- /dev/null
+++ b/src/process.rs
@@ -0,0 +1,55 @@
+// SPDX-FileCopyrightText: © 2022 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::sync::Mutex;
+use nix::sys::resource::{getrlimit, setrlimit, Resource};
+
+static ORIGINAL_RLIMIT: Mutex<Option<libc::rlimit>> = Mutex::new(None);
+
+pub fn setup() {
+    // Ignore SIGPIPE so we don't get killed when writing to a socket that
+    // has had its read end closed by another process.
+    unsafe {
+        libc::signal(libc::SIGPIPE, libc::SIG_IGN);
+    }
+
+    // Most unix systems have a default limit of 1024 file descriptors.
+    // Raise it to avoid issues with many wayland clients.
+    if let Ok((cur, max)) = getrlimit(Resource::RLIMIT_NOFILE) {
+        let mut orig = ORIGINAL_RLIMIT.lock().unwrap();
+        *orig = Some(libc::rlimit {
+            rlim_cur: cur,
+            rlim_max: max,
+        });
+
+        let new_cur = std::cmp::min(4096, max);
+        if let Err(e) = setrlimit(Resource::RLIMIT_NOFILE, new_cur, max) {
+            log::error!("setrlimit failed: {}, using system default limit of {}", e, cur);
+        } else {
+            log::info!("raised file descriptor limit of the river process to {}", new_cur);
+        }
+    } else {
+        log::error!("getrlimit failed, using system default file descriptor limit");
+    }
+}
+
+pub fn cleanup_child() {
+    unsafe {
+        if libc::setsid() < 0 {
+            // setsid failed
+        }
+
+        let mut empty_mask: libc::sigset_t = std::mem::zeroed();
+        libc::sigemptyset(&mut empty_mask);
+        libc::sigprocmask(libc::SIG_SETMASK, &empty_mask, std::ptr::null_mut());
+
+        libc::signal(libc::SIGPIPE, libc::SIG_DFL);
+    }
+
+    let orig = ORIGINAL_RLIMIT.lock().unwrap();
+    if let Some(original) = *orig {
+        unsafe {
+            libc::setrlimit(libc::RLIMIT_NOFILE, &original);
+        }
+    }
+}
diff --git a/src/scene.rs b/src/scene.rs
new file mode 100644
index 0000000..c7239cb
--- /dev/null
+++ b/src/scene.rs
@@ -0,0 +1,213 @@
+// SPDX-FileCopyrightText: © 2024 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::scene_node_data::{SceneNodeData, SceneNodeDataVal};
+
+pub struct SceneLayers {
+    pub background: *mut ffi::wlr_scene_tree,
+    pub bottom: *mut ffi::wlr_scene_tree,
+    pub wm: *mut ffi::wlr_scene_tree,
+    pub top: *mut ffi::wlr_scene_tree,
+    pub fullscreen: *mut ffi::wlr_scene_tree,
+    pub overlay: *mut ffi::wlr_scene_tree,
+    pub popups: *mut ffi::wlr_scene_tree,
+    pub override_redirect: *mut ffi::wlr_scene_tree,
+}
+
+pub struct Scene {
+    pub wlr_scene: *mut ffi::wlr_scene,
+    pub interactive_tree: *mut ffi::wlr_scene_tree,
+    pub drag_icons: *mut ffi::wlr_scene_tree,
+    pub hidden_tree: *mut ffi::wlr_scene_tree,
+    pub normal_tree: *mut ffi::wlr_scene_tree,
+    pub locked_tree: *mut ffi::wlr_scene_tree,
+    pub layers: SceneLayers,
+}
+
+impl Scene {
+    pub fn new() -> Self {
+        Self {
+            wlr_scene: std::ptr::null_mut(),
+            interactive_tree: std::ptr::null_mut(),
+            drag_icons: std::ptr::null_mut(),
+            hidden_tree: std::ptr::null_mut(),
+            normal_tree: std::ptr::null_mut(),
+            locked_tree: std::ptr::null_mut(),
+            layers: SceneLayers {
+                background: std::ptr::null_mut(),
+                bottom: std::ptr::null_mut(),
+                wm: std::ptr::null_mut(),
+                top: std::ptr::null_mut(),
+                fullscreen: std::ptr::null_mut(),
+                overlay: std::ptr::null_mut(),
+                popups: std::ptr::null_mut(),
+                override_redirect: std::ptr::null_mut(),
+            },
+        }
+    }
+
+    pub unsafe fn init(
+        &mut self,
+        linux_dmabuf: *mut ffi::wlr_linux_dmabuf_v1,
+        color_manager: *mut ffi::wlr_color_manager_v1,
+    ) -> Result<(), &'static str> {
+        let wlr_scene = ffi::wlr_scene_create();
+        if wlr_scene.is_null() {
+            return Err("Failed to create wlr_scene");
+        }
+        self.wlr_scene = wlr_scene;
+
+        if !linux_dmabuf.is_null() {
+            ffi::wlr_scene_set_linux_dmabuf_v1(wlr_scene, linux_dmabuf);
+        }
+        if !color_manager.is_null() {
+            ffi::wlr_scene_set_color_manager_v1(wlr_scene, color_manager);
+        }
+
+        let interactive_tree = ffi::wlr_scene_tree_create(&mut (*wlr_scene).tree);
+        let drag_icons = ffi::wlr_scene_tree_create(&mut (*wlr_scene).tree);
+        let hidden_tree = ffi::wlr_scene_tree_create(&mut (*wlr_scene).tree);
+        if interactive_tree.is_null() || drag_icons.is_null() || hidden_tree.is_null() {
+            return Err("Failed to create root scene trees");
+        }
+        self.interactive_tree = interactive_tree;
+        self.drag_icons = drag_icons;
+        self.hidden_tree = hidden_tree;
+
+        ffi::wlr_scene_node_set_enabled(hidden_tree as *mut ffi::wlr_scene_node, false);
+
+        let normal_tree = ffi::wlr_scene_tree_create(interactive_tree);
+        let locked_tree = ffi::wlr_scene_tree_create(interactive_tree);
+        if normal_tree.is_null() || locked_tree.is_null() {
+            return Err("Failed to create normal/locked scene trees");
+        }
+        self.normal_tree = normal_tree;
+        self.locked_tree = locked_tree;
+
+        ffi::wlr_scene_node_set_enabled(locked_tree as *mut ffi::wlr_scene_node, false);
+
+        self.layers.background = ffi::wlr_scene_tree_create(normal_tree);
+        self.layers.bottom = ffi::wlr_scene_tree_create(normal_tree);
+        self.layers.wm = ffi::wlr_scene_tree_create(normal_tree);
+        self.layers.top = ffi::wlr_scene_tree_create(normal_tree);
+        self.layers.fullscreen = ffi::wlr_scene_tree_create(normal_tree);
+        self.layers.overlay = ffi::wlr_scene_tree_create(normal_tree);
+        self.layers.popups = ffi::wlr_scene_tree_create(normal_tree);
+        self.layers.override_redirect = ffi::wlr_scene_tree_create(normal_tree);
+
+        if self.layers.background.is_null()
+            || self.layers.bottom.is_null()
+            || self.layers.wm.is_null()
+            || self.layers.top.is_null()
+            || self.layers.fullscreen.is_null()
+            || self.layers.overlay.is_null()
+            || self.layers.popups.is_null()
+            || self.layers.override_redirect.is_null()
+        {
+            return Err("Failed to create scene layer trees");
+        }
+
+        Ok(())
+    }
+
+    pub unsafe fn deinit(&mut self) {}
+
+    pub unsafe fn at(&self, lx: f64, ly: f64) -> Option<AtResult> {
+        let mut sx: f64 = 0.0;
+        let mut sy: f64 = 0.0;
+        let node = ffi::wlr_scene_node_at(self.interactive_tree as *mut ffi::wlr_scene_node, lx, ly, &mut sx, &mut sy);
+        if node.is_null() {
+            return None;
+        }
+
+        let surface = ffi::river_scene_node_get_surface(node);
+
+        if let Some(scene_node_data) = SceneNodeData::from_node(node) {
+            Some(AtResult {
+                node,
+                surface,
+                sx,
+                sy,
+                data: scene_node_data.data,
+            })
+        } else {
+            None
+        }
+    }
+
+    pub unsafe fn layer_surface_tree(&self, layer: u32) -> *mut ffi::wlr_scene_tree {
+        // layer is zwlr_layer_shell_v1_layer enum values
+        match layer {
+            ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND => self.layers.background,
+            ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM => self.layers.bottom,
+            ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_TOP => self.layers.top,
+            ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY => self.layers.overlay,
+            _ => std::ptr::null_mut(),
+        }
+    }
+}
+
+pub struct AtResult {
+    pub node: *mut ffi::wlr_scene_node,
+    pub surface: *mut ffi::wlr_surface,
+    pub sx: f64,
+    pub sy: f64,
+    pub data: SceneNodeDataVal,
+}
+
+pub struct SaveableSurfaces {
+    pub enabled: bool,
+    pub saved: bool,
+    pub tree: *mut ffi::wlr_scene_tree,
+    pub saved_tree: *mut ffi::wlr_scene_tree,
+}
+
+impl SaveableSurfaces {
+    pub unsafe fn init(parent: *mut ffi::wlr_scene_tree) -> Result<Self, &'static str> {
+        let tree = ffi::wlr_scene_tree_create(parent);
+        let saved_tree = ffi::wlr_scene_tree_create(parent);
+        if tree.is_null() || saved_tree.is_null() {
+            return Err("Failed to create saveable surfaces trees");
+        }
+        let surfaces = SaveableSurfaces {
+            enabled: true,
+            saved: false,
+            tree,
+            saved_tree,
+        };
+        surfaces.sync_enabled();
+        Ok(surfaces)
+    }
+
+    pub unsafe fn sync_enabled(&self) {
+        ffi::wlr_scene_node_set_enabled(self.tree as *mut ffi::wlr_scene_node, self.enabled && !self.saved);
+        ffi::wlr_scene_node_set_enabled(self.saved_tree as *mut ffi::wlr_scene_node, self.enabled && self.saved);
+    }
+
+    pub unsafe fn set_enabled(&mut self, enabled: bool) {
+        if self.enabled == enabled {
+            return;
+        }
+        self.enabled = enabled;
+        self.sync_enabled();
+    }
+
+    pub unsafe fn save(&mut self) {
+        if self.saved {
+            return;
+        }
+        ffi::river_scene_tree_save_buffers(self.tree, self.saved_tree);
+        self.saved = true;
+        self.sync_enabled();
+    }
+
+    pub unsafe fn drop_saved(&mut self) {
+        if !self.saved {
+            return;
+        }
+        ffi::river_scene_tree_clear_children(self.saved_tree);
+        self.saved = false;
+        self.sync_enabled();
+    }
+}
diff --git a/src/scene_node_data.rs b/src/scene_node_data.rs
new file mode 100644
index 0000000..8aecb78
--- /dev/null
+++ b/src/scene_node_data.rs
@@ -0,0 +1,83 @@
+// SPDX-FileCopyrightText: © 2023 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::server::{WlListener, wl_signal_add, wl_listener_remove};
+use crate::window_manager::{Window, ShellSurface, XwaylandOverrideRedirect};
+use crate::layer_shell::LayerSurface;
+use crate::lock_manager::LockSurface;
+
+#[derive(Clone, Copy)]
+pub enum SceneNodeDataVal {
+    Window(*mut Window),
+    ShellSurface(*mut ShellSurface),
+    LockSurface(*mut LockSurface),
+    LayerSurface(*mut LayerSurface),
+    OverrideRedirect(*mut XwaylandOverrideRedirect),
+}
+
+#[repr(C)]
+pub struct SceneNodeData {
+    pub node: *mut ffi::wlr_scene_node,
+    pub data: SceneNodeDataVal,
+    pub destroy: ffi::wl_listener,
+}
+
+impl SceneNodeData {
+    pub unsafe fn attach(node: *mut ffi::wlr_scene_node, data: SceneNodeDataVal) {
+        let scene_node_data = Box::new(SceneNodeData {
+            node,
+            data,
+            destroy: std::mem::zeroed(),
+        });
+
+        let raw = Box::into_raw(scene_node_data);
+        ffi::river_scene_node_set_data(node, raw as *mut std::ffi::c_void);
+
+        let destroy_listener = &mut (*raw).destroy as *mut ffi::wl_listener as *mut WlListener;
+        (*destroy_listener).notify = Some(handle_destroy);
+        wl_signal_add(ffi::river_scene_node_get_destroy_signal(node), &mut (*raw).destroy);
+    }
+
+    pub unsafe fn from_node(node: *mut ffi::wlr_scene_node) -> Option<&'static SceneNodeData> {
+        let mut n = node;
+        while !n.is_null() {
+            let data_ptr = ffi::river_scene_node_get_data(n) as *mut SceneNodeData;
+            if !data_ptr.is_null() {
+                return Some(&*data_ptr);
+            }
+            let parent_tree = ffi::river_scene_node_get_parent(n);
+            if !parent_tree.is_null() {
+                n = parent_tree as *mut ffi::wlr_scene_node;
+            } else {
+                break;
+            }
+        }
+        None
+    }
+
+    pub unsafe fn from_surface(surface: *mut ffi::wlr_surface) -> Option<&'static SceneNodeData> {
+        if surface.is_null() {
+            return None;
+        }
+        let root_surface = ffi::wlr_surface_get_root_surface(surface);
+        if root_surface.is_null() {
+            return None;
+        }
+        let node_ptr = ffi::river_wlr_surface_get_data(root_surface) as *mut ffi::wlr_scene_node;
+        if !node_ptr.is_null() {
+            Self::from_node(node_ptr)
+        } else {
+            None
+        }
+    }
+}
+
+unsafe extern "C" fn handle_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let scene_node_data_ptr = crate::container_of!(listener, SceneNodeData, destroy);
+    let mut scene_node_data = Box::from_raw(scene_node_data_ptr);
+    wl_listener_remove(&mut scene_node_data.destroy);
+    if !scene_node_data.node.is_null() {
+        ffi::river_scene_node_set_data(scene_node_data.node, std::ptr::null_mut());
+    }
+}
diff --git a/src/seat.rs b/src/seat.rs
new file mode 100644
index 0000000..8614c8c
--- /dev/null
+++ b/src/seat.rs
@@ -0,0 +1,1036 @@
+use crate::ffi;
+use crate::server::{Server, WlListener, wl_listener_remove, wl_signal_add};
+use crate::cursor::Cursor;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum SeatOpInput {
+    Pointer,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct SeatOp {
+    pub sent_release: bool,
+    pub input: SeatOpInput,
+    pub start_x: i32,
+    pub start_y: i32,
+    pub x: i32,
+    pub y: i32,
+}
+
+#[derive(Clone, Copy, PartialEq)]
+pub enum Focus {
+    None,
+    LayerSurface(*mut ffi::wlr_surface),
+    Window(*mut crate::window::Window),
+    LockSurface(*mut crate::lock_manager::LockSurface),
+    OverrideRedirect(*mut crate::xwayland_override_redirect::XwaylandOverrideRedirect),
+    ShellSurface(*mut crate::shell_surface::ShellSurface),
+}
+
+impl Focus {
+    pub unsafe fn surface(&self) -> *mut ffi::wlr_surface {
+        match *self {
+            Focus::None => std::ptr::null_mut(),
+            Focus::LayerSurface(surface) => surface,
+            Focus::Window(window) => if window.is_null() { std::ptr::null_mut() } else { (*window).root_surface() },
+            Focus::LockSurface(lock_surface) => if lock_surface.is_null() { std::ptr::null_mut() } else { (*(*lock_surface).wlr_lock_surface).surface },
+            Focus::OverrideRedirect(or) => if or.is_null() { std::ptr::null_mut() } else { (*(*or).xsurface).surface },
+            Focus::ShellSurface(shell_surface) => if shell_surface.is_null() { std::ptr::null_mut() } else { (*shell_surface).surface },
+        }
+    }
+}
+
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum DragState {
+    None,
+    Pointer,
+    Touch,
+}
+
+pub struct Seat {
+    pub server: *mut Server,
+    pub wlr_seat: *mut ffi::wlr_seat,
+    pub cursor: Cursor,
+    pub focused: Focus,
+    pub relay: crate::input_relay::InputRelay,
+    pub layer_shell: crate::layer_shell::LayerShellSeat,
+    pub xkb_bindings_seat: crate::xkb_bindings::XkbBindingsSeat,
+    pub xkb_bindings: ffi::wl_list,
+    pub pointer_bindings: ffi::wl_list,
+    pub keyboard_groups: ffi::wl_list,
+    pub modifiers_old: u32,
+    pub op: Option<SeatOp>,
+    pub op_release: bool,
+    pub wm_sent_x: i32,
+    pub wm_sent_y: i32,
+
+    pub request_set_cursor: ffi::wl_listener,
+    pub request_set_selection: ffi::wl_listener,
+    pub request_start_drag: ffi::wl_listener,
+    pub start_drag: ffi::wl_listener,
+    pub request_set_primary_selection: ffi::wl_listener,
+
+    pub drag: DragState,
+    pub drag_destroy: ffi::wl_listener,
+
+    pub link: ffi::wl_list,
+    pub link_sent: ffi::wl_list,
+    pub object: *mut ffi::wl_resource,
+    pub destroying: bool,
+}
+
+impl Seat {
+    pub unsafe fn create(server: *mut Server, name: &str) -> Result<*mut Self, &'static str> {
+        let name_c = std::ffi::CString::new(name).unwrap();
+        let wlr_seat = ffi::wlr_seat_create((*server).wl_server, name_c.as_ptr());
+        if wlr_seat.is_null() {
+            return Err("Failed to create wlr_seat");
+        }
+
+        let seat = Box::into_raw(Box::new(Self {
+            server,
+            wlr_seat,
+            cursor: Cursor::default(),
+            focused: Focus::None,
+            relay: std::mem::zeroed(),
+            layer_shell: crate::layer_shell::LayerShellSeat::default(),
+            xkb_bindings_seat: crate::xkb_bindings::XkbBindingsSeat::default(),
+            xkb_bindings: std::mem::zeroed(),
+            pointer_bindings: std::mem::zeroed(),
+            keyboard_groups: std::mem::zeroed(),
+            modifiers_old: 0,
+            op: None,
+            op_release: false,
+            wm_sent_x: 0,
+            wm_sent_y: 0,
+            request_set_cursor: std::mem::zeroed(),
+            request_set_selection: std::mem::zeroed(),
+            request_start_drag: std::mem::zeroed(),
+            start_drag: std::mem::zeroed(),
+            request_set_primary_selection: std::mem::zeroed(),
+            drag: DragState::None,
+            drag_destroy: std::mem::zeroed(),
+            link: std::mem::zeroed(),
+            link_sent: std::mem::zeroed(),
+            object: std::ptr::null_mut(),
+            destroying: false,
+        }));
+
+        ffi::wl_list_init(&mut (*seat).link);
+        ffi::wl_list_init(&mut (*seat).link_sent);
+        ffi::wl_list_init(&mut (*seat).xkb_bindings);
+        ffi::wl_list_init(&mut (*seat).pointer_bindings);
+        ffi::wl_list_init(&mut (*seat).keyboard_groups);
+
+        // Add to input_manager seats
+        let seats_list = &mut (*server).input_manager.seats as *mut ffi::wl_list as *mut crate::server::WlList;
+        crate::server::wl_list_insert((*seats_list).prev, &mut (*seat).link as *mut ffi::wl_list as *mut crate::server::WlList);
+
+        ffi::river_wlr_seat_set_data(wlr_seat, seat as *mut _);
+        (*seat).relay.init(seat);
+
+        // Initialize Cursor
+        let output_layout = (*server).om.output_layout;
+        (*seat).cursor.init(seat, output_layout)?;
+
+        // Setup listeners
+        let set_cursor_ptr = &mut (*seat).request_set_cursor as *mut ffi::wl_listener as *mut WlListener;
+        (*set_cursor_ptr).notify = Some(handle_request_set_cursor);
+        wl_signal_add(
+            ffi::river_wlr_seat_get_request_set_cursor_signal(wlr_seat),
+            &mut (*seat).request_set_cursor,
+        );
+
+        let set_sel_ptr = &mut (*seat).request_set_selection as *mut ffi::wl_listener as *mut WlListener;
+        (*set_sel_ptr).notify = Some(handle_request_set_selection);
+        wl_signal_add(
+            ffi::river_wlr_seat_get_request_set_selection_signal(wlr_seat),
+            &mut (*seat).request_set_selection,
+        );
+
+        let start_drag_req_ptr = &mut (*seat).request_start_drag as *mut ffi::wl_listener as *mut WlListener;
+        (*start_drag_req_ptr).notify = Some(handle_request_start_drag);
+        wl_signal_add(
+            ffi::river_wlr_seat_get_request_start_drag_signal(wlr_seat),
+            &mut (*seat).request_start_drag,
+        );
+
+        let start_drag_ptr = &mut (*seat).start_drag as *mut ffi::wl_listener as *mut WlListener;
+        (*start_drag_ptr).notify = Some(handle_start_drag);
+        wl_signal_add(
+            ffi::river_wlr_seat_get_start_drag_signal(wlr_seat),
+            &mut (*seat).start_drag,
+        );
+
+        let set_prim_ptr = &mut (*seat).request_set_primary_selection as *mut ffi::wl_listener as *mut WlListener;
+        (*set_prim_ptr).notify = Some(handle_request_set_primary_selection);
+        wl_signal_add(
+            ffi::river_wlr_seat_get_request_set_primary_selection_signal(wlr_seat),
+            &mut (*seat).request_set_primary_selection,
+        );
+
+        (*seat).update_capabilities();
+
+        Ok(seat)
+    }
+
+    pub unsafe fn destroy(seat: *mut Self) {
+        (*seat).layer_shell.make_inert();
+        (*seat).xkb_bindings_seat.make_inert();
+
+        let bindings_head = &mut (*seat).xkb_bindings as *mut ffi::wl_list as *mut crate::server::WlList;
+        let mut curr = (*bindings_head).next;
+        while curr != bindings_head {
+            let next = (*curr).next;
+            let binding = crate::container_of!(curr, crate::xkb_bindings::XkbBinding, link);
+            crate::xkb_bindings::XkbBinding::destroy(binding);
+            curr = next;
+        }
+
+        let ptr_bindings_head = &mut (*seat).pointer_bindings as *mut ffi::wl_list as *mut crate::server::WlList;
+        let mut curr_ptr = (*ptr_bindings_head).next;
+        while curr_ptr != ptr_bindings_head {
+            let next = (*curr_ptr).next;
+            let binding = crate::container_of!(curr_ptr, crate::pointer_binding::PointerBinding, link);
+            crate::pointer_binding::PointerBinding::destroy(binding);
+            curr_ptr = next;
+        }
+
+        (*seat).cursor.deinit();
+
+        // Verify keyboard_groups is empty
+        let groups_head = &mut (*seat).keyboard_groups as *mut ffi::wl_list as *mut crate::server::WlList;
+        assert_eq!((*groups_head).next, groups_head);
+
+        crate::server::wl_list_remove(&mut (*seat).link as *mut ffi::wl_list as *mut crate::server::WlList);
+        crate::server::wl_list_remove(&mut (*seat).link_sent as *mut ffi::wl_list as *mut crate::server::WlList);
+
+        wl_listener_remove(&mut (*seat).request_set_cursor);
+        wl_listener_remove(&mut (*seat).request_set_selection);
+        wl_listener_remove(&mut (*seat).request_start_drag);
+        wl_listener_remove(&mut (*seat).start_drag);
+        wl_listener_remove(&mut (*seat).request_set_primary_selection);
+
+        if (*seat).drag != DragState::None {
+            wl_listener_remove(&mut (*seat).drag_destroy);
+        }
+
+        ffi::wlr_seat_destroy((*seat).wlr_seat);
+        let _boxed = Box::from_raw(seat);
+    }
+
+    pub unsafe fn attach_device(&mut self, device: *mut crate::input_device::InputDevice) {
+        (*device).seat = self;
+        let dev_type = ffi::river_wlr_input_device_get_type((*device).wlr_device);
+        match dev_type {
+            ffi::wlr_input_device_type_WLR_INPUT_DEVICE_KEYBOARD => {
+                let keyboard = ffi::river_wlr_input_device_get_data((*device).wlr_device) as *mut crate::keyboard::Keyboard;
+                if !keyboard.is_null() {
+                    (*keyboard).set_group();
+                    if !(*keyboard).group.is_null() {
+                        ffi::wlr_seat_set_keyboard(self.wlr_seat, &mut (*(*keyboard).group).wlr_keyboard);
+                        let focused_surface = ffi::river_wlr_seat_get_keyboard_focused_surface(self.wlr_seat);
+                        if !focused_surface.is_null() {
+                            self.keyboard_notify_enter(focused_surface);
+                        }
+                    }
+                }
+            }
+            ffi::wlr_input_device_type_WLR_INPUT_DEVICE_POINTER => {
+                ffi::wlr_cursor_attach_input_device(self.cursor.wlr_cursor, (*device).wlr_device);
+            }
+            ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TOUCH | ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TABLET => {
+                ffi::wlr_cursor_attach_input_device(self.cursor.wlr_cursor, (*device).wlr_device);
+                if !(*device).config.map_to_output.is_null() {
+                    ffi::wlr_cursor_map_input_to_output(self.cursor.wlr_cursor, (*device).wlr_device, (*device).config.map_to_output);
+                }
+                ffi::wlr_cursor_map_input_to_region(self.cursor.wlr_cursor, (*device).wlr_device, &mut (*device).config.map_to_rectangle);
+            }
+            _ => {}
+        }
+        self.update_capabilities();
+    }
+
+    pub unsafe fn detach_device(&mut self, device: *mut crate::input_device::InputDevice) {
+        ffi::wlr_cursor_detach_input_device(self.cursor.wlr_cursor, (*device).wlr_device);
+
+        let dev_type = ffi::river_wlr_input_device_get_type((*device).wlr_device);
+        if dev_type == ffi::wlr_input_device_type_WLR_INPUT_DEVICE_KEYBOARD {
+            let keyboard = ffi::river_wlr_input_device_get_data((*device).wlr_device) as *mut crate::keyboard::Keyboard;
+            if !keyboard.is_null() {
+                if !(*keyboard).group.is_null() {
+                    let keys: Vec<u32> = (*keyboard).pressed.iter().cloned().collect();
+                    crate::server::wl_list_remove(&mut (*keyboard).group_link as *mut ffi::wl_list as *mut crate::server::WlList);
+                    (*(*keyboard).group).unref(&keys);
+                    (*keyboard).group = std::ptr::null_mut();
+                }
+            }
+        }
+        self.update_capabilities();
+    }
+
+    pub unsafe fn handle_activity(&mut self) {
+        let notifier = (*self.server).input_manager.idle_notifier;
+        if !notifier.is_null() {
+            ffi::wlr_idle_notifier_v1_notify_activity(notifier, self.wlr_seat);
+        }
+    }
+
+    pub unsafe fn update_capabilities(&mut self) {
+        let caps = ffi::wl_seat_capability_WL_SEAT_CAPABILITY_POINTER
+            | ffi::wl_seat_capability_WL_SEAT_CAPABILITY_KEYBOARD;
+        ffi::wlr_seat_set_capabilities(self.wlr_seat, caps);
+    }
+
+    pub unsafe fn focus(&mut self, new_focus: Focus) {
+        if self.focused == new_focus {
+            return;
+        }
+
+        match self.focused {
+            Focus::None => {}
+            Focus::LayerSurface(_) | Focus::Window(_) | Focus::LockSurface(_) | Focus::OverrideRedirect(_) | Focus::ShellSurface(_) => {
+                ffi::wlr_seat_keyboard_notify_clear_focus(self.wlr_seat);
+                let focused_client = ffi::river_wlr_seat_get_pointer_focused_client(self.wlr_seat);
+                if !focused_client.is_null() {
+                    ffi::wlr_seat_pointer_notify_clear_focus(self.wlr_seat);
+                }
+            }
+        }
+
+        self.focused = new_focus;
+
+        match new_focus {
+            Focus::None => {}
+            Focus::LayerSurface(surface) => {
+                let kbd = ffi::river_wlr_seat_get_keyboard(self.wlr_seat);
+                if !kbd.is_null() {
+                    let modifiers = ffi::river_wlr_keyboard_get_modifiers(kbd);
+                    ffi::wlr_seat_keyboard_notify_enter(
+                        self.wlr_seat,
+                        surface,
+                        std::ptr::null_mut(),
+                        0,
+                        modifiers,
+                    );
+                } else {
+                    ffi::wlr_seat_keyboard_notify_enter(
+                        self.wlr_seat,
+                        surface,
+                        std::ptr::null_mut(),
+                        0,
+                        std::ptr::null_mut(),
+                    );
+                }
+
+                let lx = self.cursor.x();
+                let ly = self.cursor.y();
+                let server = self.server;
+                if let Some(result) = (*server).scene.at(lx, ly) {
+                    if result.surface == surface {
+                        ffi::wlr_seat_pointer_notify_enter(self.wlr_seat, surface, result.sx, result.sy);
+                    }
+                }
+            }
+            Focus::Window(window) => {
+                // Focus root surface of window
+                let surface = (*window).root_surface();
+                if !surface.is_null() {
+                    let kbd = ffi::river_wlr_seat_get_keyboard(self.wlr_seat);
+                    if !kbd.is_null() {
+                        let modifiers = ffi::river_wlr_keyboard_get_modifiers(kbd);
+                        ffi::wlr_seat_keyboard_notify_enter(
+                            self.wlr_seat,
+                            surface,
+                            std::ptr::null_mut(),
+                            0,
+                            modifiers,
+                        );
+                    } else {
+                        ffi::wlr_seat_keyboard_notify_enter(
+                            self.wlr_seat,
+                            surface,
+                            std::ptr::null_mut(),
+                            0,
+                            std::ptr::null_mut(),
+                        );
+                    }
+
+                    let lx = self.cursor.x();
+                    let ly = self.cursor.y();
+                    let server = self.server;
+                    if let Some(result) = (*server).scene.at(lx, ly) {
+                        if result.surface == surface {
+                            ffi::wlr_seat_pointer_notify_enter(self.wlr_seat, surface, result.sx, result.sy);
+                        }
+                    }
+                }
+            }
+            Focus::LockSurface(lock_surface) => {
+                let surface = (*(*lock_surface).wlr_lock_surface).surface;
+                if !surface.is_null() {
+                    let kbd = ffi::river_wlr_seat_get_keyboard(self.wlr_seat);
+                    if !kbd.is_null() {
+                        let modifiers = ffi::river_wlr_keyboard_get_modifiers(kbd);
+                        ffi::wlr_seat_keyboard_notify_enter(
+                            self.wlr_seat,
+                            surface,
+                            std::ptr::null_mut(),
+                            0,
+                            modifiers,
+                        );
+                    } else {
+                        ffi::wlr_seat_keyboard_notify_enter(
+                            self.wlr_seat,
+                            surface,
+                            std::ptr::null_mut(),
+                            0,
+                            std::ptr::null_mut(),
+                        );
+                    }
+
+                    let lx = self.cursor.x();
+                    let ly = self.cursor.y();
+                    let server = self.server;
+                    if let Some(result) = (*server).scene.at(lx, ly) {
+                        if result.surface == surface {
+                            ffi::wlr_seat_pointer_notify_enter(self.wlr_seat, surface, result.sx, result.sy);
+                        }
+                    }
+                }
+            }
+            Focus::OverrideRedirect(or) => {
+                let surface = (*(*or).xsurface).surface;
+                if !surface.is_null() {
+                    let kbd = ffi::river_wlr_seat_get_keyboard(self.wlr_seat);
+                    if !kbd.is_null() {
+                        let modifiers = ffi::river_wlr_keyboard_get_modifiers(kbd);
+                        ffi::wlr_seat_keyboard_notify_enter(
+                            self.wlr_seat,
+                            surface,
+                            std::ptr::null_mut(),
+                            0,
+                            modifiers,
+                        );
+                    } else {
+                        ffi::wlr_seat_keyboard_notify_enter(
+                            self.wlr_seat,
+                            surface,
+                            std::ptr::null_mut(),
+                            0,
+                            std::ptr::null_mut(),
+                        );
+                    }
+                }
+            }
+            Focus::ShellSurface(shell_surface) => {
+                let surface = (*shell_surface).surface;
+                if !surface.is_null() {
+                    let kbd = ffi::river_wlr_seat_get_keyboard(self.wlr_seat);
+                    if !kbd.is_null() {
+                        let modifiers = ffi::river_wlr_keyboard_get_modifiers(kbd);
+                        ffi::wlr_seat_keyboard_notify_enter(
+                            self.wlr_seat,
+                            surface,
+                            std::ptr::null_mut(),
+                            0,
+                            modifiers,
+                        );
+                    } else {
+                        ffi::wlr_seat_keyboard_notify_enter(
+                            self.wlr_seat,
+                            surface,
+                            std::ptr::null_mut(),
+                            0,
+                            std::ptr::null_mut(),
+                        );
+                    }
+                }
+            }
+        }
+        let target_surface = new_focus.surface();
+        self.relay.focus(target_surface);
+    }
+
+    pub unsafe fn keyboard_notify_enter(&mut self, wlr_surface: *mut ffi::wlr_surface) {
+        let kbd = ffi::river_wlr_seat_get_keyboard(self.wlr_seat);
+        if !kbd.is_null() {
+            let group_ptr = ffi::river_wlr_keyboard_get_data(kbd) as *mut crate::keyboard_group::KeyboardGroup;
+            if !group_ptr.is_null() {
+                let mut buffer = [0u32; 32];
+                let mut count = 0;
+                for &keycode in (*group_ptr).pressed.keys() {
+                    if count >= 32 {
+                        break;
+                    }
+                    buffer[count] = keycode + 8;
+                    count += 1;
+                }
+                let modifiers = ffi::river_wlr_keyboard_get_modifiers(kbd);
+                ffi::wlr_seat_keyboard_notify_enter(
+                    self.wlr_seat,
+                    wlr_surface,
+                    buffer.as_mut_ptr(),
+                    count,
+                    modifiers,
+                );
+                return;
+            }
+        }
+        ffi::wlr_seat_keyboard_notify_enter(
+            self.wlr_seat,
+            wlr_surface,
+            std::ptr::null_mut(),
+            0,
+            std::ptr::null_mut(),
+        );
+    }
+
+    pub unsafe fn keyboard_enter_or_leave(&mut self, target_surface: *mut ffi::wlr_surface) {
+        if !target_surface.is_null() {
+            self.keyboard_notify_enter(target_surface);
+        } else {
+            ffi::wlr_seat_keyboard_notify_clear_focus(self.wlr_seat);
+        }
+        self.relay.focus(target_surface);
+    }
+
+    pub unsafe fn manage_start(&mut self) {
+        if self.destroying {
+            Self::destroy(self);
+            return;
+        }
+
+        self.layer_shell.manage_start();
+
+        let wm_v1 = (*self.server).wm.object;
+        if !wm_v1.is_null() {
+            let new = self.object.is_null();
+            if new {
+                let client = ffi::wl_resource_get_client(wm_v1);
+                let version = ffi::wl_resource_get_version(wm_v1);
+                let seat_v1 = ffi::wl_resource_create(client, &ffi::river_seat_v1_interface, version, 0);
+                if seat_v1.is_null() {
+                    log::error!("out of memory creating river_seat_v1");
+                    return;
+                }
+                self.object = seat_v1;
+                
+                ffi::wl_resource_set_implementation(
+                    seat_v1,
+                    &SEAT_INTERFACE as *const _ as *const _,
+                    self as *mut Seat as *mut _,
+                    Some(handle_destroy_resource),
+                );
+                
+                ffi::wl_resource_post_event(wm_v1, 8, seat_v1); // river_window_manager_v1.seat
+
+                crate::server::wl_list_remove(&mut self.link_sent as *mut ffi::wl_list as *mut crate::server::WlList);
+                let sent_seats = &mut (*self.server).wm.sent.seats as *mut ffi::wl_list as *mut crate::server::WlList;
+                crate::server::wl_list_insert((*sent_seats).prev, &mut self.link_sent as *mut ffi::wl_list as *mut crate::server::WlList);
+            }
+
+            if new {
+                let seat_v1 = self.object;
+                let client = ffi::wl_resource_get_client(seat_v1);
+                let wl_seat_name = ffi::wl_global_get_name(ffi::river_wlr_seat_get_global(self.wlr_seat), client);
+                ffi::wl_resource_post_event(seat_v1, 1, wl_seat_name); // river_seat_v1.wl_seat
+            }
+
+            self.xkb_bindings_seat.manage_start();
+
+            // Dispatch xkb binding events
+            let bindings_head = &mut self.xkb_bindings as *mut ffi::wl_list as *mut crate::server::WlList;
+            let mut curr = (*bindings_head).next;
+            while curr != bindings_head {
+                let next = (*curr).next;
+                let binding = crate::container_of!(curr, crate::xkb_bindings::XkbBinding, link);
+                match (*binding).wm_scheduled.state_change {
+                    crate::xkb_bindings::XkbBindingStateChange::None => {},
+                    crate::xkb_bindings::XkbBindingStateChange::Pressed => {
+                        assert!(!(*binding).sent_pressed);
+                        (*binding).sent_pressed = true;
+                        ffi::wl_resource_post_event((*binding).object, 0);
+                    },
+                    crate::xkb_bindings::XkbBindingStateChange::StopRepeat => {
+                        assert!((*binding).sent_pressed);
+                        if ffi::wl_resource_get_version((*binding).object) >= 2 {
+                            ffi::wl_resource_post_event((*binding).object, 2);
+                        }
+                    },
+                    crate::xkb_bindings::XkbBindingStateChange::Released => {
+                        assert!((*binding).sent_pressed);
+                        (*binding).sent_pressed = false;
+                        ffi::wl_resource_post_event((*binding).object, 1);
+                    },
+                }
+                (*binding).wm_scheduled.state_change = crate::xkb_bindings::XkbBindingStateChange::None;
+                curr = next;
+            }
+
+            // Dispatch pointer binding events
+            let ptr_bindings_head = &mut self.pointer_bindings as *mut ffi::wl_list as *mut crate::server::WlList;
+            let mut curr_ptr = (*ptr_bindings_head).next;
+            while curr_ptr != ptr_bindings_head {
+                let next = (*curr_ptr).next;
+                let binding = crate::container_of!(curr_ptr, crate::pointer_binding::PointerBinding, link);
+                match (*binding).wm_scheduled.state_change {
+                    crate::pointer_binding::PointerBindingStateChange::None => {}
+                    crate::pointer_binding::PointerBindingStateChange::Pressed => {
+                        assert!(!(*binding).sent_pressed);
+                        (*binding).sent_pressed = true;
+                        ffi::wl_resource_post_event((*binding).object, 0); // pressed
+                    }
+                    crate::pointer_binding::PointerBindingStateChange::Released => {
+                        assert!((*binding).sent_pressed);
+                        (*binding).sent_pressed = false;
+                        ffi::wl_resource_post_event((*binding).object, 1); // released
+                    }
+                }
+                (*binding).wm_scheduled.state_change = crate::pointer_binding::PointerBindingStateChange::None;
+                curr_ptr = next;
+            }
+
+            // Dispatch pointer operation events
+            if let Some(ref mut op) = self.op {
+                let dx = op.x - op.start_x;
+                let dy = op.y - op.start_y;
+                ffi::wl_resource_post_event(self.object, 6, dx, dy); // op_delta
+
+                if self.op_release && !op.sent_release {
+                    ffi::wl_resource_post_event(self.object, 7); // op_release
+                    self.op_release = false;
+                    op.sent_release = true;
+                }
+            }
+
+            // Dispatch pointer position event
+            if ffi::wl_resource_get_version(self.object) >= 2 {
+                let x = (*self.cursor.wlr_cursor).x as i32;
+                let y = (*self.cursor.wlr_cursor).y as i32;
+                if x != self.wm_sent_x || y != self.wm_sent_y {
+                    ffi::wl_resource_post_event(self.object, 8, x, y); // pointer_position
+                    self.wm_sent_x = x;
+                    self.wm_sent_y = y;
+                }
+            }
+        }
+    }
+
+    pub unsafe fn manage_finish(&mut self) {
+        self.xkb_bindings_seat.manage_finish();
+    }
+
+    pub unsafe fn make_inert(&mut self) {
+        if !self.object.is_null() {
+            ffi::wl_resource_post_event(self.object, 0); // river_seat_v1.removed
+            ffi::wl_resource_set_implementation(
+                self.object,
+                std::ptr::null(),
+                std::ptr::null_mut(),
+                None,
+            );
+            self.object = std::ptr::null_mut();
+            (*self.server).wm.dirty_windowing();
+        }
+        self.layer_shell.make_inert();
+        self.xkb_bindings_seat.make_inert();
+    }
+
+    pub unsafe fn match_xkb_binding(
+        &self,
+        keycode: u32,
+        wlr_keyboard: *mut ffi::wlr_keyboard,
+    ) -> Option<*mut crate::xkb_bindings::XkbBinding> {
+        let xkb_state = (*wlr_keyboard).xkb_state;
+        if xkb_state.is_null() {
+            return None;
+        }
+        
+        let modifiers = ffi::wlr_keyboard_get_modifiers(wlr_keyboard);
+        
+        let bindings_head = &self.xkb_bindings as *const ffi::wl_list as *mut crate::server::WlList;
+        let mut curr = (*bindings_head).next;
+        let mut found: Option<*mut crate::xkb_bindings::XkbBinding> = None;
+        
+        while curr != bindings_head {
+            let next = (*curr).next;
+            let binding = crate::container_of!(curr, crate::xkb_bindings::XkbBinding, link);
+            if (*binding).match_keycode(keycode, modifiers, xkb_state, false) {
+                if found.is_none() {
+                    found = Some(binding);
+                } else {
+                    log::debug!("already found a matching xkb_binding, ignoring additional match");
+                }
+            }
+            curr = next;
+        }
+        
+        if found.is_some() {
+            return found;
+        }
+        
+        curr = (*bindings_head).next;
+        while curr != bindings_head {
+            let next = (*curr).next;
+            let binding = crate::container_of!(curr, crate::xkb_bindings::XkbBinding, link);
+            if (*binding).match_keycode(keycode, modifiers, xkb_state, true) {
+                if found.is_none() {
+                    found = Some(binding);
+                } else {
+                    log::debug!("already found a matching xkb_binding, ignoring additional match");
+                }
+            }
+            curr = next;
+        }
+        
+        found
+    }
+
+    pub unsafe fn match_pointer_binding(
+        &self,
+        button: u32,
+    ) -> Option<*mut crate::pointer_binding::PointerBinding> {
+        let wlr_keyboard = ffi::river_wlr_seat_get_keyboard(self.wlr_seat);
+        if wlr_keyboard.is_null() {
+            return None;
+        }
+        let modifiers = ffi::wlr_keyboard_get_modifiers(wlr_keyboard);
+
+        let bindings_head = &self.pointer_bindings as *const ffi::wl_list as *mut crate::server::WlList;
+        let mut curr = (*bindings_head).next;
+        let mut found: Option<*mut crate::pointer_binding::PointerBinding> = None;
+
+        while curr != bindings_head {
+            let next = (*curr).next;
+            let binding = crate::container_of!(curr, crate::pointer_binding::PointerBinding, link);
+            if (*binding).match_binding(button, modifiers) {
+                if found.is_none() {
+                    found = Some(binding);
+                } else {
+                    log::debug!("already found a matching pointer binding, ignoring additional match");
+                }
+            }
+            curr = next;
+        }
+        found
+    }
+
+    pub unsafe fn op_update(&mut self, x: i32, y: i32) {
+        if let Some(ref mut op) = self.op {
+            op.x = x;
+            op.y = y;
+            (*self.server).wm.dirty_windowing();
+        }
+    }
+
+    pub unsafe fn op_end(&mut self) {
+        if let Some(op) = self.op.take() {
+            log::debug!("end seat op");
+            match op.input {
+                SeatOpInput::Pointer => {
+                    self.cursor.op_end_pointer();
+                }
+            }
+        }
+    }
+}
+
+unsafe extern "C" fn handle_request_set_cursor(
+    listener: *mut ffi::wl_listener,
+    data: *mut std::ffi::c_void,
+) {
+    let seat = &mut *crate::container_of!(listener, Seat, request_set_cursor);
+    let event = data as *mut ffi::wlr_seat_pointer_request_set_cursor_event;
+    
+    let focused_client = ffi::river_wlr_seat_get_pointer_focused_client(seat.wlr_seat);
+    if focused_client == (*event).seat_client {
+        ffi::wlr_cursor_set_surface(
+            seat.cursor.wlr_cursor,
+            (*event).surface,
+            (*event).hotspot_x,
+            (*event).hotspot_y,
+        );
+    }
+}
+
+unsafe extern "C" fn handle_request_set_selection(
+    listener: *mut ffi::wl_listener,
+    data: *mut std::ffi::c_void,
+) {
+    let seat = &mut *crate::container_of!(listener, Seat, request_set_selection);
+    let event = data as *mut ffi::wlr_seat_request_set_selection_event;
+    ffi::wlr_seat_set_selection(seat.wlr_seat, (*event).source, (*event).serial);
+}
+
+unsafe extern "C" fn handle_request_start_drag(
+    listener: *mut ffi::wl_listener,
+    data: *mut std::ffi::c_void,
+) {
+    let seat = &mut *crate::container_of!(listener, Seat, request_start_drag);
+    let event = data as *mut ffi::wlr_seat_request_start_drag_event;
+
+    assert!(seat.drag == DragState::None);
+
+    if ffi::wlr_seat_validate_pointer_grab_serial(seat.wlr_seat, (*event).origin, (*event).serial) {
+        ffi::wlr_seat_start_pointer_drag(seat.wlr_seat, (*event).drag, (*event).serial);
+        return;
+    }
+
+    let mut point: *mut ffi::wlr_touch_point = std::ptr::null_mut();
+    if ffi::wlr_seat_validate_touch_grab_serial(seat.wlr_seat, (*event).origin, (*event).serial, &mut point) {
+        ffi::wlr_seat_start_touch_drag(seat.wlr_seat, (*event).drag, (*event).serial, point);
+        return;
+    }
+
+    let source = ffi::river_wlr_drag_get_source((*event).drag);
+    if !source.is_null() {
+        ffi::wlr_data_source_destroy(source);
+    }
+}
+
+unsafe extern "C" fn handle_start_drag(
+    listener: *mut ffi::wl_listener,
+    data: *mut std::ffi::c_void,
+) {
+    let seat = &mut *crate::container_of!(listener, Seat, start_drag);
+    let wlr_drag = data as *mut ffi::wlr_drag;
+
+    assert!(seat.drag == DragState::None);
+    let grab_type = ffi::river_wlr_drag_get_grab_type(wlr_drag);
+    match grab_type {
+        ffi::wlr_drag_grab_type_WLR_DRAG_GRAB_KEYBOARD_POINTER => {
+            seat.drag = DragState::Pointer;
+        }
+        ffi::wlr_drag_grab_type_WLR_DRAG_GRAB_KEYBOARD_TOUCH => {
+            seat.drag = DragState::Touch;
+        }
+        _ => {}
+    }
+
+    let drag_destroy_ptr = &mut seat.drag_destroy as *mut ffi::wl_listener as *mut WlListener;
+    (*drag_destroy_ptr).notify = Some(handle_drag_destroy);
+    wl_signal_add(
+        ffi::river_wlr_drag_get_destroy_signal(wlr_drag),
+        &mut seat.drag_destroy,
+    );
+
+    let wlr_drag_icon = ffi::river_wlr_drag_get_icon(wlr_drag);
+    if !wlr_drag_icon.is_null() {
+        if let Err(err) = crate::drag_icon::DragIcon::create(wlr_drag_icon, &mut seat.cursor) {
+            log::error!("Failed to create drag icon: {}", err);
+            let seat_client = ffi::river_wlr_drag_get_seat_client(wlr_drag);
+            if !seat_client.is_null() {
+                let client = ffi::river_wlr_seat_client_get_client(seat_client);
+                if !client.is_null() {
+                    ffi::wl_client_post_no_memory(client);
+                }
+            }
+        }
+    }
+}
+
+unsafe extern "C" fn handle_drag_destroy(
+    listener: *mut ffi::wl_listener,
+    _data: *mut std::ffi::c_void,
+) {
+    let seat = &mut *crate::container_of!(listener, Seat, drag_destroy);
+    wl_listener_remove(&mut seat.drag_destroy);
+
+    match seat.drag {
+        DragState::None => unreachable!(),
+        DragState::Pointer => {
+            seat.cursor.update_state();
+        }
+        DragState::Touch => {}
+    }
+    seat.drag = DragState::None;
+}
+
+unsafe extern "C" fn handle_request_set_primary_selection(
+    listener: *mut ffi::wl_listener,
+    data: *mut std::ffi::c_void,
+) {
+    let seat = &mut *crate::container_of!(listener, Seat, request_set_primary_selection);
+    let event = data as *mut ffi::wlr_seat_request_set_primary_selection_event;
+    ffi::wlr_seat_set_primary_selection(seat.wlr_seat, (*event).source, (*event).serial);
+}
+
+unsafe extern "C" fn seat_destroy(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn seat_focus_window(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    window_resource: *mut ffi::wl_resource,
+) {
+    let seat = ffi::wl_resource_get_user_data(resource) as *mut Seat;
+    if seat.is_null() {
+        return;
+    }
+    if !(*(*seat).server).wm.ensure_windowing() {
+        return;
+    }
+    if window_resource.is_null() {
+        (*seat).focus(Focus::None);
+        return;
+    }
+    let window = ffi::wl_resource_get_user_data(window_resource) as *mut crate::window::Window;
+    if !window.is_null() {
+        (*seat).focus(Focus::Window(window));
+    }
+}
+
+unsafe extern "C" fn seat_focus_shell_surface(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    shell_surface_resource: *mut ffi::wl_resource,
+) {
+    let seat = ffi::wl_resource_get_user_data(resource) as *mut Seat;
+    if seat.is_null() {
+        return;
+    }
+    if !(*(*seat).server).wm.ensure_windowing() {
+        return;
+    }
+    if shell_surface_resource.is_null() {
+        (*seat).focus(Focus::None);
+        return;
+    }
+    let shell_surface = ffi::wl_resource_get_user_data(shell_surface_resource) as *mut crate::shell_surface::ShellSurface;
+    if !shell_surface.is_null() {
+        (*seat).focus(Focus::ShellSurface(shell_surface));
+    }
+}
+
+unsafe extern "C" fn seat_clear_focus(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let seat = ffi::wl_resource_get_user_data(resource) as *mut Seat;
+    if !seat.is_null() {
+        if (*(*seat).server).wm.ensure_windowing() {
+            (*seat).focus(Focus::None);
+        }
+    }
+}
+
+unsafe extern "C" fn seat_op_start_pointer(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let seat = ffi::wl_resource_get_user_data(resource) as *mut Seat;
+    if seat.is_null() {
+        return;
+    }
+    if !(*(*seat).server).wm.ensure_windowing() {
+        return;
+    }
+    if (*seat).op.is_none() {
+        log::debug!("start seat op pointer");
+        let cursor_x = (*(*seat).cursor.wlr_cursor).x;
+        let cursor_y = (*(*seat).cursor.wlr_cursor).y;
+        (*seat).op = Some(SeatOp {
+            sent_release: false,
+            input: SeatOpInput::Pointer,
+            start_x: cursor_x as i32,
+            start_y: cursor_y as i32,
+            x: cursor_x as i32,
+            y: cursor_y as i32,
+        });
+        (*seat).cursor.op_start_pointer();
+    }
+}
+
+unsafe extern "C" fn seat_op_end(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let seat = ffi::wl_resource_get_user_data(resource) as *mut Seat;
+    if seat.is_null() {
+        return;
+    }
+    if !(*(*seat).server).wm.ensure_windowing() {
+        return;
+    }
+    (*seat).op_end();
+}
+
+unsafe extern "C" fn seat_get_pointer_binding(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    id: u32,
+    button: u32,
+    modifiers: u32,
+) {
+    let seat = ffi::wl_resource_get_user_data(resource) as *mut Seat;
+    if seat.is_null() {
+        return;
+    }
+    let version = ffi::wl_resource_get_version(resource) as u32;
+    if let Err(err) = crate::pointer_binding::PointerBinding::create(
+        seat,
+        client,
+        version,
+        id,
+        button,
+        modifiers,
+    ) {
+        log::error!("failed to create pointer binding: {}", err);
+        ffi::wl_client_post_no_memory(client);
+    }
+}
+
+unsafe extern "C" fn seat_set_xcursor_theme(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    name: *const ::std::os::raw::c_char,
+    size: u32,
+) {
+    let seat = ffi::wl_resource_get_user_data(resource) as *mut Seat;
+    if seat.is_null() {
+        return;
+    }
+    if let Err(err) = (*seat).cursor.set_theme(name, size) {
+        log::error!("failed to set xcursor theme: {}", err);
+        ffi::wl_client_post_no_memory(client);
+    }
+}
+
+unsafe extern "C" fn seat_pointer_warp(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    x: i32,
+    y: i32,
+) {
+    let seat = ffi::wl_resource_get_user_data(resource) as *mut Seat;
+    if seat.is_null() {
+        return;
+    }
+    if !(*(*seat).server).wm.ensure_windowing() {
+        return;
+    }
+    let cursor = &mut (*seat).cursor;
+    ffi::wlr_cursor_warp_absolute(cursor.wlr_cursor, std::ptr::null_mut(), x as f64, y as f64);
+}
+
+static SEAT_INTERFACE: ffi::river_seat_v1_interface = ffi::river_seat_v1_interface {
+    destroy: Some(seat_destroy),
+    focus_window: Some(seat_focus_window),
+    focus_shell_surface: Some(seat_focus_shell_surface),
+    clear_focus: Some(seat_clear_focus),
+    op_start_pointer: Some(seat_op_start_pointer),
+    op_end: Some(seat_op_end),
+    get_pointer_binding: Some(seat_get_pointer_binding),
+    set_xcursor_theme: Some(seat_set_xcursor_theme),
+    pointer_warp: Some(seat_pointer_warp),
+};
+
+unsafe extern "C" fn handle_destroy_resource(resource: *mut ffi::wl_resource) {
+    let seat = ffi::wl_resource_get_user_data(resource) as *mut Seat;
+    if !seat.is_null() {
+        (*seat).object = std::ptr::null_mut();
+    }
+}
diff --git a/src/server.rs b/src/server.rs
new file mode 100644
index 0000000..f284aac
--- /dev/null
+++ b/src/server.rs
@@ -0,0 +1,697 @@
+// SPDX-FileCopyrightText: © 2020 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use std::ptr;
+
+use crate::window_manager::WindowManager;
+use crate::xkb_bindings::XkbBindings;
+use crate::layer_shell::LayerShell;
+use crate::scene::Scene;
+use crate::output_manager::OutputManager;
+use crate::input_manager::InputManager;
+use crate::libinput_config::LibinputConfig;
+use crate::xkb_config::XkbConfig;
+use crate::idle_inhibit_manager::IdleInhibitManager;
+use crate::lock_manager::LockManager;
+
+// Helper macro equivalent to @fieldParentPtr in Zig
+#[macro_export]
+macro_rules! container_of {
+    ($ptr:expr, $container:path, $field:ident) => {{
+        let offset = {
+            let dummy = std::mem::MaybeUninit::<$container>::uninit();
+            let dummy_ptr = dummy.as_ptr();
+            let field_ptr = std::ptr::addr_of!((*dummy_ptr).$field);
+            (field_ptr as usize).wrapping_sub(dummy_ptr as usize)
+        };
+        ((($ptr as *const _) as usize).wrapping_sub(offset)) as *mut $container
+    }};
+}
+
+// Custom non-opaque layouts for FFI casting
+#[repr(C)]
+pub struct WlList {
+    pub prev: *mut WlList,
+    pub next: *mut WlList,
+}
+
+#[repr(C)]
+pub struct WlListener {
+    pub link: WlList,
+    pub notify: Option<unsafe extern "C" fn(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void)>,
+}
+
+#[repr(C)]
+pub struct WlrRendererEvents {
+    pub destroy: ffi::wl_signal,
+    pub lost: ffi::wl_signal,
+}
+
+#[repr(C)]
+pub struct WlrRendererFeatures {
+    pub input_color_transform: bool,
+    pub output_color_transform: bool,
+    pub timeline: bool,
+}
+
+#[repr(C)]
+pub struct WlrRenderer {
+    pub render_buffer_caps: u32,
+    pub color_encodings: u32,
+    pub events: WlrRendererEvents,
+    pub features: WlrRendererFeatures,
+}
+
+#[repr(C)]
+pub struct WlrBackendFeatures {
+    pub timeline: bool,
+}
+
+#[repr(C)]
+pub struct WlrBackendEvents {
+    pub destroy: ffi::wl_signal,
+    pub new_input: ffi::wl_signal,
+    pub new_output: ffi::wl_signal,
+}
+
+#[repr(C)]
+pub struct WlrBackend {
+    pub impl_: *const std::ffi::c_void,
+    pub buffer_caps: u32,
+    pub features: WlrBackendFeatures,
+    pub events: WlrBackendEvents,
+}
+
+#[repr(C)]
+pub struct WlrXdgShellEvents {
+    pub new_surface: ffi::wl_signal,
+    pub new_toplevel: ffi::wl_signal,
+    pub new_popup: ffi::wl_signal,
+    pub destroy: ffi::wl_signal,
+}
+
+#[repr(C)]
+pub struct WlrXdgShell {
+    pub global: *mut ffi::wl_global,
+    pub version: u32,
+    pub clients: ffi::wl_list,
+    pub popup_grabs: ffi::wl_list,
+    pub ping_timeout: u32,
+    pub events: WlrXdgShellEvents,
+}
+
+#[repr(C)]
+pub struct WlrXdgDecorationManagerV1Events {
+    pub new_toplevel_decoration: ffi::wl_signal,
+    pub destroy: ffi::wl_signal,
+}
+
+#[repr(C)]
+pub struct WlrXdgDecorationManagerV1 {
+    pub global: *mut ffi::wl_global,
+    pub decorations: ffi::wl_list,
+    pub events: WlrXdgDecorationManagerV1Events,
+}
+
+#[repr(C)]
+pub struct WlrXdgActivationV1Events {
+    pub destroy: ffi::wl_signal,
+    pub request_activate: ffi::wl_signal,
+    pub new_token: ffi::wl_signal,
+}
+
+#[repr(C)]
+pub struct WlrXdgActivationV1 {
+    pub global: *mut ffi::wl_global,
+    pub token_timeout_msec: u32,
+    pub tokens: ffi::wl_list,
+    pub events: WlrXdgActivationV1Events,
+}
+
+#[repr(C)]
+pub struct WlrCursorShapeManagerV1Events {
+    pub request_set_shape: ffi::wl_signal,
+    pub destroy: ffi::wl_signal,
+}
+
+#[repr(C)]
+pub struct WlrCursorShapeManagerV1 {
+    pub global: *mut ffi::wl_global,
+    pub events: WlrCursorShapeManagerV1Events,
+}
+
+#[repr(C)]
+pub struct WlrExtForeignToplevelImageCaptureSourceManagerV1Events {
+    pub destroy: ffi::wl_signal,
+    pub new_request: ffi::wl_signal,
+}
+
+#[repr(C)]
+pub struct WlrExtForeignToplevelImageCaptureSourceManagerV1 {
+    pub global: *mut ffi::wl_global,
+    pub events: WlrExtForeignToplevelImageCaptureSourceManagerV1Events,
+}
+
+#[repr(C)]
+pub struct WlrXwaylandEvents {
+    pub destroy: ffi::wl_signal,
+    pub ready: ffi::wl_signal,
+    pub new_surface: ffi::wl_signal,
+    pub remove_startup_info: ffi::wl_signal,
+}
+
+#[repr(C)]
+pub struct WlrXwayland {
+    pub server: *mut std::ffi::c_void,
+    pub own_server: bool,
+    pub xwm: *mut std::ffi::c_void,
+    pub shell_v1: *mut std::ffi::c_void,
+    pub display_name: *const std::os::raw::c_char,
+    pub wl_display: *mut ffi::wl_display,
+    pub compositor: *mut ffi::wlr_compositor,
+    pub seat: *mut std::ffi::c_void,
+    pub events: WlrXwaylandEvents,
+}
+
+// Wayland list manipulation utilities
+pub unsafe fn wl_list_insert(list: *mut WlList, elm: *mut WlList) {
+    (*elm).prev = list;
+    (*elm).next = (*list).next;
+    (*(*list).next).prev = elm;
+    (*list).next = elm;
+}
+
+pub unsafe fn wl_list_remove(elm: *mut WlList) {
+    (*(*elm).next).prev = (*elm).prev;
+    (*(*elm).prev).next = (*elm).next;
+    (*elm).next = std::ptr::null_mut();
+    (*elm).prev = std::ptr::null_mut();
+}
+
+pub unsafe fn wl_signal_add(signal: *mut ffi::wl_signal, listener: *mut ffi::wl_listener) {
+    let sig_list = &mut (*signal).listener_list as *mut ffi::wl_list as *mut WlList;
+    let listener_custom = listener as *mut WlListener;
+    wl_list_insert((*sig_list).prev, &mut (*listener_custom).link);
+}
+
+pub unsafe fn wl_listener_remove(listener: *mut ffi::wl_listener) {
+    let listener_custom = listener as *mut WlListener;
+    wl_list_remove(&mut (*listener_custom).link);
+}
+
+
+pub struct Server {
+    pub wl_server: *mut ffi::wl_display,
+    pub sigint_source: *mut ffi::wl_event_source,
+    pub sigterm_source: *mut ffi::wl_event_source,
+    pub fixes: *mut ffi::wlr_fixes,
+    pub backend: *mut ffi::wlr_backend,
+    pub session: *mut ffi::wlr_session,
+    pub renderer: *mut ffi::wlr_renderer,
+    pub allocator: *mut ffi::wlr_allocator,
+    pub gpu_reset_recover: *mut ffi::wl_event_source,
+    pub security_context_manager: *mut ffi::wlr_security_context_manager_v1,
+    pub shm: *mut ffi::wlr_shm,
+    pub linux_dmabuf: *mut ffi::wlr_linux_dmabuf_v1,
+    pub linux_drm_syncobj_manager: *mut ffi::wlr_linux_drm_syncobj_manager_v1,
+    pub single_pixel_buffer_manager: *mut ffi::wlr_single_pixel_buffer_manager_v1,
+    pub alpha_modifier: *mut ffi::wlr_alpha_modifier_v1,
+    pub color_manager: *mut ffi::wlr_color_manager_v1,
+    pub color_representation_manager: *mut ffi::wlr_color_representation_manager_v1,
+    pub viewporter: *mut ffi::wlr_viewporter,
+    pub fractional_scale_manager: *mut ffi::wlr_fractional_scale_manager_v1,
+    pub compositor: *mut ffi::wlr_compositor,
+    pub subcompositor: *mut ffi::wlr_subcompositor,
+    pub cursor_shape_manager: *mut ffi::wlr_cursor_shape_manager_v1,
+    pub xdg_shell: *mut ffi::wlr_xdg_shell,
+    pub xdg_decoration_manager: *mut ffi::wlr_xdg_decoration_manager_v1,
+    pub xdg_activation: *mut ffi::wlr_xdg_activation_v1,
+    pub xdg_foreign_registry: *mut ffi::wlr_xdg_foreign_registry,
+    pub xdg_foreign_v2: *mut ffi::wlr_xdg_foreign_v2,
+    pub data_device_manager: *mut ffi::wlr_data_device_manager,
+    pub primary_selection_manager: *mut ffi::wlr_primary_selection_v1_device_manager,
+    pub data_control_manager: *mut ffi::wlr_ext_data_control_manager_v1,
+    pub wlr_data_control_manager: *mut ffi::wlr_data_control_manager_v1,
+    pub export_dmabuf_manager: *mut ffi::wlr_export_dmabuf_manager_v1,
+    pub screencopy_manager: *mut ffi::wlr_screencopy_manager_v1,
+    pub image_copy_capture_manager: *mut ffi::wlr_ext_image_copy_capture_manager_v1,
+    pub output_image_capture_source_manager: *mut ffi::wlr_ext_output_image_capture_source_manager_v1,
+    pub wlr_foreign_toplevel_manager: *mut ffi::wlr_foreign_toplevel_manager_v1,
+    pub foreign_toplevel_list: *mut ffi::wlr_ext_foreign_toplevel_list_v1,
+    pub toplevel_capture_source_manager: *mut ffi::wlr_ext_foreign_toplevel_image_capture_source_manager_v1,
+    pub tearing_control_manager: *mut ffi::wlr_tearing_control_manager_v1,
+
+    pub xwayland: *mut ffi::wlr_xwayland,
+
+    // Subcomponents
+    pub wm: WindowManager,
+    pub xkb_bindings: XkbBindings,
+    pub layer_shell: LayerShell,
+    pub scene: Scene,
+    pub om: OutputManager,
+    pub input_manager: InputManager,
+    pub libinput_config: LibinputConfig,
+    pub xkb_config: XkbConfig,
+    pub idle_inhibit_manager: IdleInhibitManager,
+    pub lock_manager: LockManager,
+
+    // Event listeners
+    pub renderer_lost: ffi::wl_listener,
+    pub new_xdg_toplevel: ffi::wl_listener,
+    pub new_toplevel_decoration: ffi::wl_listener,
+    pub request_activate: ffi::wl_listener,
+    pub request_set_cursor_shape: ffi::wl_listener,
+    pub toplevel_capture_request: ffi::wl_listener,
+    pub new_xsurface: ffi::wl_listener,
+}
+
+unsafe extern "C" fn terminate(_signum: std::os::raw::c_int, data: *mut std::ffi::c_void) -> std::os::raw::c_int {
+    let wl_server = data as *mut ffi::wl_display;
+    ffi::wl_display_terminate(wl_server);
+    0
+}
+
+unsafe extern "C" fn handle_renderer_lost(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let _server = container_of!(listener, Server, renderer_lost);
+    log::info!("received GPU reset event");
+}
+
+unsafe extern "C" fn handle_new_xdg_toplevel(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let server = container_of!(listener, Server, new_xdg_toplevel);
+    let xdg_toplevel = data as *mut ffi::wlr_xdg_toplevel;
+    log::info!("new xdg toplevel surface");
+    if let Err(e) = crate::xdg_toplevel::XdgToplevel::create(xdg_toplevel, server) {
+        log::error!("Failed to create xdg toplevel: {}", e);
+        let client = ffi::wl_resource_get_client((*xdg_toplevel).resource);
+        ffi::wl_client_post_no_memory(client);
+    }
+}
+
+unsafe extern "C" fn handle_new_toplevel_decoration(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let _server = container_of!(listener, Server, new_toplevel_decoration);
+    let decoration = data as *mut ffi::wlr_xdg_toplevel_decoration_v1;
+    log::info!("new toplevel decoration");
+    crate::xdg_toplevel::XdgDecoration::init(decoration);
+}
+
+unsafe extern "C" fn handle_request_activate(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let _server = container_of!(listener, Server, request_activate);
+    log::info!("xdg activation request");
+}
+
+unsafe extern "C" fn handle_request_set_cursor_shape(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let _server = container_of!(listener, Server, request_set_cursor_shape);
+    log::info!("request set cursor shape");
+}
+
+unsafe extern "C" fn handle_toplevel_capture_request(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let _server = container_of!(listener, Server, toplevel_capture_request);
+    log::info!("toplevel capture request");
+}
+
+unsafe extern "C" fn handle_new_xwayland_surface(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+    let server = container_of!(listener, Server, new_xsurface);
+    let xsurface = data as *mut ffi::wlr_xwayland_surface;
+    log::info!("new xwayland surface");
+
+    if (*xsurface).override_redirect {
+        if let Err(e) = crate::xwayland_override_redirect::XwaylandOverrideRedirect::create(xsurface, server) {
+            log::error!("Failed to create xwayland override redirect surface: {}", e);
+        }
+    } else {
+        if let Err(e) = crate::xwayland_window::XwaylandWindow::create(xsurface, server) {
+            log::error!("Failed to create xwayland window surface: {}", e);
+        }
+    }
+}
+
+impl Server {
+    pub fn init(&mut self, runtime_xwayland: bool) -> Result<(), &'static str> {
+        unsafe {
+            let wl_server = ffi::wl_display_create();
+            if wl_server.is_null() {
+                return Err("Failed to create wayland server");
+            }
+            self.wl_server = wl_server;
+
+            let loop_ = ffi::wl_display_get_event_loop(wl_server);
+            if loop_.is_null() {
+                return Err("Failed to get event loop");
+            }
+
+            self.sigint_source = ffi::wl_event_loop_add_signal(loop_, libc::SIGINT, Some(terminate), wl_server as *mut _);
+            self.sigterm_source = ffi::wl_event_loop_add_signal(loop_, libc::SIGTERM, Some(terminate), wl_server as *mut _);
+
+            let mut session: *mut ffi::wlr_session = ptr::null_mut();
+            let backend = ffi::wlr_backend_autocreate(loop_, &mut session);
+            if backend.is_null() {
+                return Err("Failed to autocreate wlr_backend");
+            }
+            self.backend = backend;
+            self.session = session;
+
+            let renderer = ffi::wlr_renderer_autocreate(backend);
+            if renderer.is_null() {
+                return Err("Failed to autocreate wlr_renderer");
+            }
+            self.renderer = renderer;
+
+            let compositor = ffi::wlr_compositor_create(wl_server, 6, renderer);
+            if compositor.is_null() {
+                return Err("Failed to create wlr_compositor");
+            }
+            self.compositor = compositor;
+
+            let xdg_foreign_registry = ffi::wlr_xdg_foreign_registry_create(wl_server);
+            if xdg_foreign_registry.is_null() {
+                return Err("Failed to create xdg_foreign_registry");
+            }
+            self.xdg_foreign_registry = xdg_foreign_registry;
+
+            let fixes = ffi::wlr_fixes_create(wl_server, 1);
+            if fixes.is_null() {
+                return Err("Failed to create wlr_fixes");
+            }
+            self.fixes = fixes;
+
+            let allocator = ffi::wlr_allocator_autocreate(backend, renderer);
+            if allocator.is_null() {
+                return Err("Failed to autocreate allocator");
+            }
+            self.allocator = allocator;
+
+            let security_context_manager = ffi::wlr_security_context_manager_v1_create(wl_server);
+            if security_context_manager.is_null() {
+                return Err("Failed to create security context manager");
+            }
+            self.security_context_manager = security_context_manager;
+
+            let shm = ffi::wlr_shm_create_with_renderer(wl_server, 2, renderer);
+            if shm.is_null() {
+                return Err("Failed to create shm");
+            }
+            self.shm = shm;
+
+            let single_pixel_buffer_manager = ffi::wlr_single_pixel_buffer_manager_v1_create(wl_server);
+            if single_pixel_buffer_manager.is_null() {
+                return Err("Failed to create single pixel buffer manager");
+            }
+            self.single_pixel_buffer_manager = single_pixel_buffer_manager;
+
+            let alpha_modifier = ffi::wlr_alpha_modifier_v1_create(wl_server);
+            if alpha_modifier.is_null() {
+                return Err("Failed to create alpha modifier");
+            }
+            self.alpha_modifier = alpha_modifier;
+
+            let color_representation_manager = ffi::wlr_color_representation_manager_v1_create_with_renderer(wl_server, 1, renderer);
+            if color_representation_manager.is_null() {
+                return Err("Failed to create color representation manager");
+            }
+            self.color_representation_manager = color_representation_manager;
+
+            let viewporter = ffi::wlr_viewporter_create(wl_server);
+            if viewporter.is_null() {
+                return Err("Failed to create viewporter");
+            }
+            self.viewporter = viewporter;
+
+            let fractional_scale_manager = ffi::wlr_fractional_scale_manager_v1_create(wl_server, 1);
+            if fractional_scale_manager.is_null() {
+                return Err("Failed to create fractional scale manager");
+            }
+            self.fractional_scale_manager = fractional_scale_manager;
+
+            let subcompositor = ffi::wlr_subcompositor_create(wl_server);
+            if subcompositor.is_null() {
+                return Err("Failed to create subcompositor");
+            }
+            self.subcompositor = subcompositor;
+
+            let cursor_shape_manager = ffi::wlr_cursor_shape_manager_v1_create(wl_server, 2);
+            if cursor_shape_manager.is_null() {
+                return Err("Failed to create cursor shape manager");
+            }
+            self.cursor_shape_manager = cursor_shape_manager;
+
+            let xdg_shell = ffi::wlr_xdg_shell_create(wl_server, 5);
+            if xdg_shell.is_null() {
+                return Err("Failed to create xdg shell");
+            }
+            self.xdg_shell = xdg_shell;
+
+            let xdg_decoration_manager = ffi::wlr_xdg_decoration_manager_v1_create(wl_server);
+            if xdg_decoration_manager.is_null() {
+                return Err("Failed to create xdg decoration manager");
+            }
+            self.xdg_decoration_manager = xdg_decoration_manager;
+
+            let xdg_activation = ffi::wlr_xdg_activation_v1_create(wl_server);
+            if xdg_activation.is_null() {
+                return Err("Failed to create xdg activation");
+            }
+            self.xdg_activation = xdg_activation;
+
+            let xdg_foreign_v2 = ffi::wlr_xdg_foreign_v2_create(wl_server, xdg_foreign_registry);
+            if xdg_foreign_v2.is_null() {
+                return Err("Failed to create xdg foreign v2");
+            }
+            self.xdg_foreign_v2 = xdg_foreign_v2;
+
+            let data_device_manager = ffi::wlr_data_device_manager_create(wl_server);
+            if data_device_manager.is_null() {
+                return Err("Failed to create data device manager");
+            }
+            self.data_device_manager = data_device_manager;
+
+            let primary_selection_manager = ffi::wlr_primary_selection_v1_device_manager_create(wl_server);
+            if primary_selection_manager.is_null() {
+                return Err("Failed to create primary selection manager");
+            }
+            self.primary_selection_manager = primary_selection_manager;
+
+            let data_control_manager = ffi::wlr_ext_data_control_manager_v1_create(wl_server, 1);
+            if data_control_manager.is_null() {
+                return Err("Failed to create ext data control manager");
+            }
+            self.data_control_manager = data_control_manager;
+
+            let wlr_data_control_manager = ffi::wlr_data_control_manager_v1_create(wl_server);
+            if wlr_data_control_manager.is_null() {
+                return Err("Failed to create data control manager");
+            }
+            self.wlr_data_control_manager = wlr_data_control_manager;
+
+            let export_dmabuf_manager = ffi::wlr_export_dmabuf_manager_v1_create(wl_server);
+            if export_dmabuf_manager.is_null() {
+                return Err("Failed to create export dmabuf manager");
+            }
+            self.export_dmabuf_manager = export_dmabuf_manager;
+
+            let screencopy_manager = ffi::wlr_screencopy_manager_v1_create(wl_server);
+            if screencopy_manager.is_null() {
+                return Err("Failed to create screencopy manager");
+            }
+            self.screencopy_manager = screencopy_manager;
+
+            let image_copy_capture_manager = ffi::wlr_ext_image_copy_capture_manager_v1_create(wl_server, 1);
+            if image_copy_capture_manager.is_null() {
+                return Err("Failed to create image copy capture manager");
+            }
+            self.image_copy_capture_manager = image_copy_capture_manager;
+
+            let output_image_capture_source_manager = ffi::wlr_ext_output_image_capture_source_manager_v1_create(wl_server, 1);
+            if output_image_capture_source_manager.is_null() {
+                return Err("Failed to create output image capture source manager");
+            }
+            self.output_image_capture_source_manager = output_image_capture_source_manager;
+
+            let wlr_foreign_toplevel_manager = ffi::wlr_foreign_toplevel_manager_v1_create(wl_server);
+            if wlr_foreign_toplevel_manager.is_null() {
+                return Err("Failed to create foreign toplevel manager");
+            }
+            self.wlr_foreign_toplevel_manager = wlr_foreign_toplevel_manager;
+
+            let foreign_toplevel_list = ffi::wlr_ext_foreign_toplevel_list_v1_create(wl_server, 1);
+            if foreign_toplevel_list.is_null() {
+                return Err("Failed to create foreign toplevel list");
+            }
+            self.foreign_toplevel_list = foreign_toplevel_list;
+
+            let toplevel_capture_source_manager = ffi::wlr_ext_foreign_toplevel_image_capture_source_manager_v1_create(wl_server, 1);
+            if toplevel_capture_source_manager.is_null() {
+                return Err("Failed to create toplevel capture source manager");
+            }
+            self.toplevel_capture_source_manager = toplevel_capture_source_manager;
+
+            let tearing_control_manager = ffi::wlr_tearing_control_manager_v1_create(wl_server, 1);
+            if tearing_control_manager.is_null() {
+                return Err("Failed to create tearing control manager");
+            }
+            self.tearing_control_manager = tearing_control_manager;
+
+            // Setup Xwayland if runtime requested
+            if runtime_xwayland {
+                let xwayland = ffi::wlr_xwayland_create(wl_server, compositor, false);
+                if xwayland.is_null() {
+                    return Err("Failed to create xwayland server");
+                }
+                self.xwayland = xwayland;
+            }
+
+            // Setup linux dmabuf if supported
+            if !ffi::wlr_renderer_get_texture_formats(renderer, ffi::wlr_buffer_cap_WLR_BUFFER_CAP_DMABUF).is_null() {
+                self.linux_dmabuf = ffi::wlr_linux_dmabuf_v1_create_with_renderer(wl_server, 5, renderer);
+            }
+
+            // Setup linux drm syncobj if supported
+            let renderer_cast = renderer as *mut WlrRenderer;
+            let backend_cast = backend as *mut WlrBackend;
+            if (*renderer_cast).features.timeline && (*backend_cast).features.timeline {
+                let drm_fd = ffi::wlr_renderer_get_drm_fd(renderer);
+                if drm_fd >= 0 {
+                    self.linux_drm_syncobj_manager = ffi::wlr_linux_drm_syncobj_manager_v1_create(wl_server, 1, drm_fd);
+                }
+            }
+
+            // Setup color manager if supported
+            if (*renderer_cast).features.input_color_transform {
+                let mut len: usize = 0;
+                let primaries = ffi::wlr_color_manager_v1_primaries_list_from_renderer(renderer, &mut len);
+                let transfer_functions = ffi::wlr_color_manager_v1_transfer_function_list_from_renderer(renderer, &mut len);
+
+                let render_intents = [ffi::wp_color_manager_v1_render_intent_WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL];
+                
+                self.color_manager = ffi::wlr_color_manager_v1_create(
+                    wl_server,
+                    2,
+                    &ffi::wlr_color_manager_v1_options {
+                        features: ffi::wlr_color_manager_v1_features {
+                            icc_v2_v4: false,
+                            parametric: true,
+                            set_primaries: false,
+                            set_tf_power: false,
+                            set_luminances: false,
+                            set_mastering_display_primaries: true,
+                            extended_target_volume: false,
+                            windows_scrgb: false,
+                        },
+                        render_intents: render_intents.as_ptr(),
+                        render_intents_len: render_intents.len(),
+                        primaries,
+                        primaries_len: len,
+                        transfer_functions,
+                        transfer_functions_len: len,
+                    }
+                );
+
+                libc::free(primaries as *mut _);
+                libc::free(transfer_functions as *mut _);
+            }
+
+            // Setup subcomponents stubs
+            let server_ptr = self as *mut Server;
+            self.wm.init_with_server(server_ptr).map_err(|_| "Failed to init wm")?;
+            self.xkb_bindings.init(server_ptr, self.wl_server).map_err(|_| "Failed to init xkb_bindings")?;
+            self.layer_shell.init(self.wl_server).map_err(|_| "Failed to init layer_shell")?;
+            self.scene.init(self.linux_dmabuf, self.color_manager).map_err(|_| "Failed to init scene")?;
+            self.om.init(server_ptr).map_err(|_| "Failed to init om")?;
+            self.input_manager.init(server_ptr).map_err(|_| "Failed to init input_manager")?;
+            self.libinput_config.init(server_ptr).map_err(|_| "Failed to init libinput_config")?;
+            self.xkb_config.init(server_ptr).map_err(|_| "Failed to init xkb_config")?;
+            self.idle_inhibit_manager.init(server_ptr).map_err(|_| "Failed to init idle_inhibit_manager")?;
+            self.lock_manager.init(server_ptr).map_err(|_| "Failed to init lock_manager")?;
+
+            // Setup listeners
+            let r_lost = &mut self.renderer_lost as *mut ffi::wl_listener as *mut WlListener;
+            (*r_lost).notify = Some(handle_renderer_lost);
+
+            let new_xdg = &mut self.new_xdg_toplevel as *mut ffi::wl_listener as *mut WlListener;
+            (*new_xdg).notify = Some(handle_new_xdg_toplevel);
+
+            let new_dec = &mut self.new_toplevel_decoration as *mut ffi::wl_listener as *mut WlListener;
+            (*new_dec).notify = Some(handle_new_toplevel_decoration);
+
+            let req_act = &mut self.request_activate as *mut ffi::wl_listener as *mut WlListener;
+            (*req_act).notify = Some(handle_request_activate);
+
+            let req_cursor = &mut self.request_set_cursor_shape as *mut ffi::wl_listener as *mut WlListener;
+            (*req_cursor).notify = Some(handle_request_set_cursor_shape);
+
+            let cap_req = &mut self.toplevel_capture_request as *mut ffi::wl_listener as *mut WlListener;
+            (*cap_req).notify = Some(handle_toplevel_capture_request);
+
+            let xdg_shell_cast = self.xdg_shell as *mut WlrXdgShell;
+            let xdg_decoration_manager_cast = self.xdg_decoration_manager as *mut WlrXdgDecorationManagerV1;
+            let xdg_activation_cast = self.xdg_activation as *mut WlrXdgActivationV1;
+            let cursor_shape_manager_cast = self.cursor_shape_manager as *mut WlrCursorShapeManagerV1;
+            let toplevel_capture_source_manager_cast = self.toplevel_capture_source_manager as *mut WlrExtForeignToplevelImageCaptureSourceManagerV1;
+
+            wl_signal_add(&mut (*renderer_cast).events.lost, &mut self.renderer_lost);
+            wl_signal_add(&mut (*xdg_shell_cast).events.new_toplevel, &mut self.new_xdg_toplevel);
+            wl_signal_add(&mut (*xdg_decoration_manager_cast).events.new_toplevel_decoration, &mut self.new_toplevel_decoration);
+            wl_signal_add(&mut (*xdg_activation_cast).events.request_activate, &mut self.request_activate);
+            wl_signal_add(&mut (*cursor_shape_manager_cast).events.request_set_shape, &mut self.request_set_cursor_shape);
+            wl_signal_add(&mut (*toplevel_capture_source_manager_cast).events.new_request, &mut self.toplevel_capture_request);
+
+            // Register Xwayland surface listener if active
+            if !self.xwayland.is_null() {
+                let new_x = &mut self.new_xsurface as *mut ffi::wl_listener as *mut WlListener;
+                (*new_x).notify = Some(handle_new_xwayland_surface);
+
+                let xwayland_cast = self.xwayland as *mut WlrXwayland;
+                wl_signal_add(&mut (*xwayland_cast).events.new_surface, &mut self.new_xsurface);
+            }
+        }
+
+        Ok(())
+    }
+
+    pub fn deinit(&mut self) {
+        unsafe {
+            ffi::wl_event_source_remove(self.sigint_source);
+            ffi::wl_event_source_remove(self.sigterm_source);
+
+            wl_listener_remove(&mut self.renderer_lost);
+            wl_listener_remove(&mut self.new_xdg_toplevel);
+            wl_listener_remove(&mut self.new_toplevel_decoration);
+            wl_listener_remove(&mut self.request_activate);
+            wl_listener_remove(&mut self.request_set_cursor_shape);
+            wl_listener_remove(&mut self.toplevel_capture_request);
+
+            if !self.xwayland.is_null() {
+                wl_listener_remove(&mut self.new_xsurface);
+                ffi::wlr_xwayland_destroy(self.xwayland);
+            }
+
+            ffi::wl_display_destroy_clients(self.wl_server);
+            ffi::wlr_backend_destroy(self.backend);
+
+            ffi::wlr_renderer_destroy(self.renderer);
+            ffi::wlr_allocator_destroy(self.allocator);
+
+            self.om.deinit();
+            self.input_manager.deinit();
+            self.idle_inhibit_manager.deinit();
+            self.lock_manager.deinit();
+            self.layer_shell.deinit();
+
+            ffi::wl_display_destroy(self.wl_server);
+        }
+    }
+}
+
+impl Default for Server {
+    fn default() -> Self {
+        let mut server = std::mem::MaybeUninit::<Server>::uninit();
+        unsafe {
+            // Zero-initialize the memory (C structures and primitive fields)
+            std::ptr::write_bytes(server.as_mut_ptr(), 0, 1);
+            // Overwrite SlotMap with a valid SlotMap::new() to avoid UB from null vec pointers
+            std::ptr::write(&mut (*server.as_mut_ptr()).wm.windows, crate::slotmap::SlotMap::new());
+            std::ptr::write(&mut (*server.as_mut_ptr()).layer_shell.surfaces, crate::slotmap::SlotMap::new());
+            server.assume_init()
+        }
+    }
+}
diff --git a/src/shell_surface.rs b/src/shell_surface.rs
new file mode 100644
index 0000000..d397625
--- /dev/null
+++ b/src/shell_surface.rs
@@ -0,0 +1,266 @@
+// SPDX-FileCopyrightText: © 2024 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::server::{Server, WlList, wl_list_insert, wl_list_remove};
+use crate::wm_node::WmNode;
+
+pub struct ShellSurfaceRenderingRequested {
+    pub x: i32,
+    pub y: i32,
+    pub sync_next_commit: bool,
+}
+
+pub struct ShellSurface {
+    pub server: *mut Server,
+    pub object: *mut ffi::wl_resource, // river_shell_surface_v1
+    pub surface: *mut ffi::wlr_surface,
+    pub tree: *mut ffi::wlr_scene_tree,
+    pub surfaces: crate::scene::SaveableSurfaces,
+    pub popup_tree: *mut ffi::wlr_scene_tree,
+    pub node: WmNode,
+    pub rendering_requested: ShellSurfaceRenderingRequested,
+}
+
+impl ShellSurface {
+    pub unsafe fn create(
+        client: *mut ffi::wl_client,
+        version: u32,
+        id: u32,
+        surface: *mut ffi::wlr_surface,
+        server: *mut Server,
+    ) -> Result<(), &'static str> {
+        log::debug!("new river_shell_surface_v1");
+
+        let shell_surface_v1 = ffi::wl_resource_create(client, &ffi::river_shell_surface_v1_interface, version as i32, id);
+        if shell_surface_v1.is_null() {
+            ffi::wl_client_post_no_memory(client);
+            return Err("wl_resource_create failed");
+        }
+
+        if !ffi::wlr_surface_set_role(
+            surface,
+            &SHELL_SURFACE_ROLE,
+            shell_surface_v1,
+            ffi::river_window_manager_v1_error_RIVER_WINDOW_MANAGER_V1_ERROR_ROLE,
+        ) {
+            return Err("wlr_surface_set_role failed");
+        }
+        ffi::river_wlr_surface_set_role_object(surface, shell_surface_v1);
+
+        let shell_surface = Box::new(ShellSurface {
+            server,
+            object: shell_surface_v1,
+            surface,
+            tree: std::ptr::null_mut(),
+            surfaces: std::mem::zeroed(),
+            popup_tree: std::ptr::null_mut(),
+            node: std::mem::zeroed(),
+            rendering_requested: ShellSurfaceRenderingRequested {
+                x: 0,
+                y: 0,
+                sync_next_commit: false,
+            },
+        });
+        let raw = Box::into_raw(shell_surface);
+
+        ffi::wl_resource_set_implementation(
+            shell_surface_v1,
+            &SHELL_SURFACE_INTERFACE as *const _ as *const _,
+            raw as *mut _,
+            None,
+        );
+
+        let hidden_tree = (*server).scene.hidden_tree;
+        let tree = ffi::wlr_scene_tree_create(hidden_tree);
+        if tree.is_null() {
+            let _ = Box::from_raw(raw);
+            return Err("Failed to create scene tree");
+        }
+        let popup_tree = ffi::wlr_scene_tree_create(hidden_tree);
+        if popup_tree.is_null() {
+            ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
+            let _ = Box::from_raw(raw);
+            return Err("Failed to create popup tree");
+        }
+
+        let surfaces = match crate::scene::SaveableSurfaces::init(tree) {
+            Ok(s) => s,
+            Err(e) => {
+                ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
+                ffi::wlr_scene_node_destroy(popup_tree as *mut ffi::wlr_scene_node);
+                let _ = Box::from_raw(raw);
+                return Err(e);
+            }
+        };
+
+        let subsurface_tree = ffi::wlr_scene_subsurface_tree_create(surfaces.tree, surface);
+        if subsurface_tree.is_null() {
+            ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
+            ffi::wlr_scene_node_destroy(popup_tree as *mut ffi::wlr_scene_node);
+            let _ = Box::from_raw(raw);
+            return Err("Failed to create scene subsurface tree");
+        }
+
+        (*raw).tree = tree;
+        (*raw).popup_tree = popup_tree;
+        (*raw).surfaces = surfaces;
+        (*raw).node.init(crate::wm_node::WmNodeTag::ShellSurface);
+
+        let list_head = &mut (*server).wm.rendering_requested.list as *mut ffi::wl_list as *mut WlList;
+        wl_list_insert((*list_head).prev, &mut (*raw).node.link as *mut ffi::wl_list as *mut WlList);
+
+        crate::scene_node_data::SceneNodeData::attach(
+            tree as *mut ffi::wlr_scene_node,
+            crate::scene_node_data::SceneNodeDataVal::ShellSurface(raw),
+        );
+        crate::scene_node_data::SceneNodeData::attach(
+            popup_tree as *mut ffi::wlr_scene_node,
+            crate::scene_node_data::SceneNodeDataVal::ShellSurface(raw),
+        );
+
+        Ok(())
+    }
+
+    pub unsafe fn render_finish(&mut self) {
+        if self.rendering_requested.sync_next_commit {
+            self.rendering_requested.sync_next_commit = false;
+
+            if !self.surfaces.saved {
+                ffi::wl_resource_post_error(
+                    self.object,
+                    ffi::river_shell_surface_v1_error_RIVER_SHELL_SURFACE_V1_ERROR_NO_COMMIT,
+                    b"no wl_surface.commit after sync_next_commit and before update_rendering_finish\0".as_ptr() as *const _,
+                );
+            }
+        }
+
+        self.surfaces.drop_saved();
+
+        ffi::wlr_scene_node_set_position(
+            self.tree as *mut ffi::wlr_scene_node,
+            self.rendering_requested.x,
+            self.rendering_requested.y,
+        );
+        ffi::wlr_scene_node_set_position(
+            self.popup_tree as *mut ffi::wlr_scene_node,
+            self.rendering_requested.x,
+            self.rendering_requested.y,
+        );
+    }
+}
+
+pub unsafe fn from_wlr_surface(surface: *mut ffi::wlr_surface) -> *mut ShellSurface {
+    if surface.is_null() {
+        return std::ptr::null_mut();
+    }
+    let role_ptr = ffi::river_wlr_surface_get_role(surface);
+    if role_ptr != &SHELL_SURFACE_ROLE as *const _ {
+        return std::ptr::null_mut();
+    }
+    let resource = ffi::river_wlr_surface_get_role_resource(surface);
+    if resource.is_null() {
+        return std::ptr::null_mut();
+    }
+    ffi::wl_resource_get_user_data(resource) as *mut ShellSurface
+}
+
+unsafe extern "C" fn client_commit(surface: *mut ffi::wlr_surface) {
+    let shell_surface = from_wlr_surface(surface);
+    if shell_surface.is_null() {
+        return;
+    }
+    if (*shell_surface).rendering_requested.sync_next_commit {
+        (*shell_surface).surfaces.save();
+    }
+}
+
+unsafe extern "C" fn commit(surface: *mut ffi::wlr_surface) {
+    if ffi::wlr_surface_has_buffer(surface) {
+        ffi::wlr_surface_map(surface);
+    }
+}
+
+unsafe extern "C" fn role_destroy(surface: *mut ffi::wlr_surface) {
+    let shell_surface = from_wlr_surface(surface);
+    if shell_surface.is_null() {
+        return;
+    }
+
+    ffi::wlr_surface_unmap((*shell_surface).surface);
+
+    (*shell_surface).node.make_inert();
+    (*shell_surface).node.deinit();
+
+    ffi::wlr_scene_node_destroy((*shell_surface).tree as *mut ffi::wlr_scene_node);
+    ffi::wlr_scene_node_destroy((*shell_surface).popup_tree as *mut ffi::wlr_scene_node);
+
+    let _ = Box::from_raw(shell_surface);
+}
+
+unsafe extern "C" fn shell_surface_destroy(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn shell_surface_get_node(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    id: u32,
+) {
+    let shell_surface = ffi::wl_resource_get_user_data(resource) as *mut ShellSurface;
+    if shell_surface.is_null() {
+        return;
+    }
+
+    if !(*shell_surface).node.object.is_null() {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_shell_surface_v1_error_RIVER_SHELL_SURFACE_V1_ERROR_NODE_EXISTS,
+            b"shell surface already has a node object\0".as_ptr() as *const _,
+        );
+        return;
+    }
+
+    (*shell_surface).node.create_object(
+        client,
+        ffi::wl_resource_get_version(resource) as u32,
+        id,
+    );
+}
+
+unsafe extern "C" fn shell_surface_sync_next_commit(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let shell_surface = ffi::wl_resource_get_user_data(resource) as *mut ShellSurface;
+    if shell_surface.is_null() {
+        return;
+    }
+
+    let server = (*shell_surface).server;
+    if !(*server).wm.ensure_rendering() {
+        return;
+    }
+
+    (*shell_surface).rendering_requested.sync_next_commit = true;
+}
+
+static SHELL_SURFACE_INTERFACE: ffi::river_shell_surface_v1_interface = ffi::river_shell_surface_v1_interface {
+    destroy: Some(shell_surface_destroy),
+    get_node: Some(shell_surface_get_node),
+    sync_next_commit: Some(shell_surface_sync_next_commit),
+};
+
+#[no_mangle]
+pub static mut SHELL_SURFACE_ROLE: ffi::wlr_surface_role = ffi::wlr_surface_role {
+    name: b"river_shell_surface_v1\0".as_ptr() as *const _,
+    no_object: false,
+    client_commit: Some(client_commit),
+    commit: Some(commit),
+    map: None,
+    unmap: None,
+    destroy: Some(role_destroy),
+};
diff --git a/src/slotmap.rs b/src/slotmap.rs
new file mode 100644
index 0000000..5bab508
--- /dev/null
+++ b/src/slotmap.rs
@@ -0,0 +1,195 @@
+// SPDX-FileCopyrightText: © 2025 Isaac Freund
+// SPDX-License-Identifier: 0BSD
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Key {
+    pub generation: u32,
+    pub index: u32,
+}
+
+enum SlotData<T> {
+    Value(T),
+    NextFree(u32),
+}
+
+struct Slot<T> {
+    generation: u32,
+    data: SlotData<T>,
+}
+
+pub struct SlotMap<T> {
+    slots: Vec<Slot<T>>,
+    count: u32,
+    first_free: u32,
+}
+
+impl<T> SlotMap<T> {
+    pub fn new() -> Self {
+        Self {
+            slots: Vec::new(),
+            count: 0,
+            first_free: 1,
+        }
+    }
+}
+
+impl<T> Default for SlotMap<T> {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl<T> SlotMap<T> {
+
+    pub fn put(&mut self, value: T) -> Key {
+        let index = self.first_free as usize;
+        if index < self.slots.len() {
+            let slot = &mut self.slots[index];
+            if let SlotData::NextFree(next) = slot.data {
+                self.first_free = next;
+            } else {
+                panic!("SlotMap state corrupted: expected NextFree slot");
+            }
+            slot.data = SlotData::Value(value);
+            self.count += 1;
+            Key {
+                generation: slot.generation,
+                index: index as u32,
+            }
+        } else {
+            self.slots.push(Slot {
+                generation: 0,
+                data: SlotData::Value(value),
+            });
+            let new_index = self.slots.len() - 1;
+            self.count += 1;
+            self.first_free += 1;
+            Key {
+                generation: 0,
+                index: new_index as u32,
+            }
+        }
+    }
+
+    pub fn get(&self, key: Key) -> Option<&T> {
+        let idx = key.index as usize;
+        if idx < self.slots.len() {
+            let slot = &self.slots[idx];
+            if slot.generation == key.generation {
+                if let SlotData::Value(ref val) = slot.data {
+                    return Some(val);
+                }
+            }
+        }
+        None
+    }
+
+    pub fn get_mut(&mut self, key: Key) -> Option<&mut T> {
+        let idx = key.index as usize;
+        if idx < self.slots.len() {
+            let slot = &mut self.slots[idx];
+            if slot.generation == key.generation {
+                if let SlotData::Value(ref mut val) = slot.data {
+                    return Some(val);
+                }
+            }
+        }
+        None
+    }
+
+    pub fn remove(&mut self, key: Key) -> Option<T> {
+        let idx = key.index as usize;
+        if idx < self.slots.len() {
+            let slot = &mut self.slots[idx];
+            if slot.generation == key.generation {
+                if let SlotData::Value(_) = slot.data {
+                    let old_data = std::mem::replace(&mut slot.data, SlotData::NextFree(self.first_free));
+                    slot.generation = slot.generation.wrapping_add(1);
+                    self.count -= 1;
+                    self.first_free = key.index;
+                    if let SlotData::Value(val) = old_data {
+                        return Some(val);
+                    }
+                }
+            }
+        }
+        None
+    }
+
+    pub fn count(&self) -> u32 {
+        self.count
+    }
+
+    pub fn iter(&self) -> Iter<'_, T> {
+        Iter {
+            slots: &self.slots,
+            index: 0,
+        }
+    }
+}
+
+pub struct Iter<'a, T> {
+    slots: &'a [Slot<T>],
+    index: usize,
+}
+
+impl<'a, T> Iterator for Iter<'a, T> {
+    type Item = &'a T;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        while self.index < self.slots.len() {
+            let slot = &self.slots[self.index];
+            self.index += 1;
+            if let SlotData::Value(ref val) = slot.data {
+                return Some(val);
+            }
+        }
+        None
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_basic() {
+        let mut map = SlotMap::new();
+
+        let key5 = map.put(5);
+        assert_eq!(map.get(key5), Some(&5));
+
+        assert_eq!(map.remove(key5), Some(5));
+        assert_eq!(map.get(key5), None);
+
+        let key6 = map.put(6);
+        assert_eq!(map.get(key6), Some(&6));
+        assert_eq!(map.get(key5), None);
+
+        let key7 = map.put(7);
+        let key8 = map.put(8);
+        let key9 = map.put(9);
+
+        assert_eq!(map.get(key6), Some(&6));
+        assert_eq!(map.get(key7), Some(&7));
+        assert_eq!(map.get(key8), Some(&8));
+        assert_eq!(map.get(key9), Some(&9));
+
+        map.remove(key8);
+        assert_eq!(map.get(key8), None);
+        assert_eq!(map.get(key9), Some(&9));
+    }
+
+    #[test]
+    fn test_iteration() {
+        let mut map = SlotMap::new();
+        map.put(5);
+        map.put(6);
+        map.put(7);
+        map.put(8);
+        map.put(9);
+
+        let vals: Vec<&i32> = map.iter().collect();
+        assert_eq!(vals, vec![&5, &6, &7, &8, &9]);
+    }
+}
diff --git a/src/tablet.rs b/src/tablet.rs
new file mode 100644
index 0000000..166b44a
--- /dev/null
+++ b/src/tablet.rs
@@ -0,0 +1,51 @@
+use crate::ffi;
+use crate::input_device::InputDevice;
+use crate::seat::Seat;
+
+pub struct Tablet {
+    pub device: *mut InputDevice,
+    pub wp_tablet: *mut ffi::wlr_tablet_v2_tablet,
+}
+
+impl Tablet {
+    pub unsafe fn create(
+        seat: *mut Seat,
+        wlr_device: *mut ffi::wlr_input_device,
+        virtual_device: bool,
+    ) -> Result<*mut Self, &'static str> {
+        let server = (*seat).server;
+        let tablet_manager = (*server).input_manager.tablet_manager;
+        if tablet_manager.is_null() {
+            return Err("Tablet manager is null");
+        }
+
+        let wp_tablet = ffi::wlr_tablet_create(tablet_manager, (*seat).wlr_seat, wlr_device);
+        if wp_tablet.is_null() {
+            return Err("Failed to create wp_tablet");
+        }
+
+        let device = InputDevice::new(seat, wlr_device, virtual_device);
+        if device.is_null() {
+            return Err("Failed to create input device");
+        }
+
+        let tablet = Box::into_raw(Box::new(Self {
+            device,
+            wp_tablet,
+        }));
+
+        // Set up the custom destroy callback on the input device
+        (*device).destroy_fn = Some(destroy_tablet_callback);
+        (*device).destroy_data = tablet as *mut _;
+
+        Ok(tablet)
+    }
+
+    pub unsafe fn destroy(tablet: *mut Self) {
+        let _boxed = Box::from_raw(tablet);
+    }
+}
+
+unsafe extern "C" fn destroy_tablet_callback(data: *mut std::ffi::c_void) {
+    Tablet::destroy(data as *mut Tablet);
+}
diff --git a/src/tablet_tool.rs b/src/tablet_tool.rs
new file mode 100644
index 0000000..d9ef066
--- /dev/null
+++ b/src/tablet_tool.rs
@@ -0,0 +1,366 @@
+use crate::ffi;
+use crate::tablet::Tablet;
+use crate::seat::Seat;
+use crate::server::{WlListener, wl_listener_remove, wl_signal_add};
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum TabletToolMode {
+    Passthrough,
+    Down {
+        lx: f64,
+        ly: f64,
+        sx: f64,
+        sy: f64,
+    },
+}
+
+pub struct TabletTool {
+    pub wp_tool: *mut ffi::wlr_tablet_v2_tablet_tool,
+    pub wlr_cursor: *mut ffi::wlr_cursor,
+    pub mode: TabletToolMode,
+    pub tilt_x: f64,
+    pub tilt_y: f64,
+
+    pub destroy_listener: ffi::wl_listener,
+    pub set_cursor_listener: ffi::wl_listener,
+}
+
+impl TabletTool {
+    pub unsafe fn get(
+        wlr_seat: *mut ffi::wlr_seat,
+        wlr_tool: *mut ffi::wlr_tablet_tool,
+    ) -> Result<*mut Self, &'static str> {
+        let data_ptr = (*wlr_tool).data as *mut Self;
+        if !data_ptr.is_null() {
+            Ok(data_ptr)
+        } else {
+            Self::create(wlr_seat, wlr_tool)
+        }
+    }
+
+    pub unsafe fn create(
+        wlr_seat: *mut ffi::wlr_seat,
+        wlr_tool: *mut ffi::wlr_tablet_tool,
+    ) -> Result<*mut Self, &'static str> {
+        let wlr_cursor = ffi::wlr_cursor_create();
+        if wlr_cursor.is_null() {
+            return Err("Failed to create wlr_cursor");
+        }
+
+        let seat = ffi::river_wlr_seat_get_data(wlr_seat) as *mut Seat;
+        if seat.is_null() {
+            ffi::wlr_cursor_destroy(wlr_cursor);
+            return Err("Seat data is null");
+        }
+        let server = (*seat).server;
+        let output_layout = (*server).om.output_layout;
+
+        ffi::wlr_cursor_attach_output_layout(wlr_cursor, output_layout);
+
+        let tablet_manager = (*server).input_manager.tablet_manager;
+        let wp_tool = ffi::wlr_tablet_tool_create(tablet_manager, wlr_seat, wlr_tool);
+        if wp_tool.is_null() {
+            ffi::wlr_cursor_destroy(wlr_cursor);
+            return Err("Failed to create wp_tool");
+        }
+
+        let tool = Box::into_raw(Box::new(Self {
+            wp_tool,
+            wlr_cursor,
+            mode: TabletToolMode::Passthrough,
+            tilt_x: 0.0,
+            tilt_y: 0.0,
+            destroy_listener: std::mem::zeroed(),
+            set_cursor_listener: std::mem::zeroed(),
+        }));
+
+        (*wlr_tool).data = tool as *mut _;
+
+        let destroy_listener_ptr = &mut (*tool).destroy_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*destroy_listener_ptr).notify = Some(handle_destroy);
+        wl_signal_add(&mut (*wlr_tool).events.destroy, &mut (*tool).destroy_listener);
+
+        let set_cursor_listener_ptr = &mut (*tool).set_cursor_listener as *mut ffi::wl_listener as *mut WlListener;
+        (*set_cursor_listener_ptr).notify = Some(handle_set_cursor);
+        let set_cursor_signal = ffi::river_wlr_tablet_v2_tablet_tool_get_set_cursor_signal(wp_tool);
+        wl_signal_add(set_cursor_signal, &mut (*tool).set_cursor_listener);
+
+        Ok(tool)
+    }
+
+    pub unsafe fn cursor_x(&self) -> f64 {
+        ffi::river_wlr_cursor_get_x(self.wlr_cursor)
+    }
+
+    pub unsafe fn cursor_y(&self) -> f64 {
+        ffi::river_wlr_cursor_get_y(self.wlr_cursor)
+    }
+
+    pub unsafe fn allow_set_cursor(
+        &self,
+        seat_client: *mut ffi::wlr_seat_client,
+        serial: u32,
+    ) -> bool {
+        let focused_surface = ffi::river_wlr_tablet_v2_tablet_tool_get_focused_surface(self.wp_tool);
+        if focused_surface.is_null() {
+            log::debug!("client tried to set cursor without focus");
+            return false;
+        }
+
+        let surface_resource = ffi::river_wlr_surface_get_resource(focused_surface);
+        if surface_resource.is_null() {
+            return false;
+        }
+
+        let surface_client = ffi::wl_resource_get_client(surface_resource);
+        let seat_client_ptr = ffi::river_wlr_seat_client_get_client(seat_client);
+
+        if surface_client != seat_client_ptr {
+            log::debug!("client tried to set cursor without focus");
+            return false;
+        }
+
+        let proximity_serial = ffi::river_wlr_tablet_v2_tablet_tool_get_proximity_serial(self.wp_tool);
+        if serial != proximity_serial {
+            log::debug!("focused client tried to set cursor with incorrect serial");
+            return false;
+        }
+
+        true
+    }
+
+    pub unsafe fn attach(&mut self, tablet: *mut Tablet) {
+        let dev = (*(*tablet).device).wlr_device;
+        ffi::wlr_cursor_attach_input_device(self.wlr_cursor, dev);
+        ffi::wlr_cursor_map_input_to_output(self.wlr_cursor, dev, (*(*tablet).device).config.map_to_output);
+        ffi::wlr_cursor_map_input_to_region(self.wlr_cursor, dev, &mut (*(*tablet).device).config.map_to_rectangle as *mut _);
+    }
+
+    pub unsafe fn detach(&mut self, tablet: *mut Tablet) {
+        let dev = (*(*tablet).device).wlr_device;
+        ffi::wlr_cursor_detach_input_device(self.wlr_cursor, dev);
+    }
+
+    pub unsafe fn axis(&mut self, tablet: *mut Tablet, event: *mut ffi::wlr_tablet_tool_axis_event) {
+        self.attach(tablet);
+
+        let updated = (*event).updated_axes;
+        let has_x = (updated & ffi::wlr_tablet_tool_axes_WLR_TABLET_TOOL_AXIS_X) != 0;
+        let has_y = (updated & ffi::wlr_tablet_tool_axes_WLR_TABLET_TOOL_AXIS_Y) != 0;
+
+        let wlr_tool = ffi::river_wlr_tablet_v2_tablet_tool_get_wlr_tool(self.wp_tool);
+        let tool_type = (*wlr_tool).type_;
+
+        if has_x || has_y {
+            match tool_type {
+                ffi::wlr_tablet_tool_type_WLR_TABLET_TOOL_TYPE_PEN |
+                ffi::wlr_tablet_tool_type_WLR_TABLET_TOOL_TYPE_ERASER |
+                ffi::wlr_tablet_tool_type_WLR_TABLET_TOOL_TYPE_BRUSH |
+                ffi::wlr_tablet_tool_type_WLR_TABLET_TOOL_TYPE_PENCIL |
+                ffi::wlr_tablet_tool_type_WLR_TABLET_TOOL_TYPE_AIRBRUSH |
+                ffi::wlr_tablet_tool_type_WLR_TABLET_TOOL_TYPE_TOTEM => {
+                    let warp_x = if has_x { (*event).x } else { f64::NAN };
+                    let warp_y = if has_y { (*event).y } else { f64::NAN };
+                    ffi::wlr_cursor_warp_absolute(self.wlr_cursor, (*(*tablet).device).wlr_device, warp_x, warp_y);
+                }
+                ffi::wlr_tablet_tool_type_WLR_TABLET_TOOL_TYPE_LENS |
+                ffi::wlr_tablet_tool_type_WLR_TABLET_TOOL_TYPE_MOUSE => {
+                    ffi::wlr_cursor_move(self.wlr_cursor, (*(*tablet).device).wlr_device, (*event).dx, (*event).dy);
+                }
+                _ => {}
+            }
+
+            match self.mode {
+                TabletToolMode::Passthrough => {
+                    self.passthrough(tablet);
+                }
+                TabletToolMode::Down { lx, ly, sx, sy } => {
+                    let dx = self.cursor_x() - lx;
+                    let dy = self.cursor_y() - ly;
+                    ffi::wlr_tablet_v2_tablet_tool_notify_motion(self.wp_tool, sx + dx, sy + dy);
+                }
+            }
+        }
+
+        let has_dist = (updated & ffi::wlr_tablet_tool_axes_WLR_TABLET_TOOL_AXIS_DISTANCE) != 0;
+        if has_dist {
+            ffi::wlr_tablet_v2_tablet_tool_notify_distance(self.wp_tool, (*event).distance);
+        }
+
+        let has_press = (updated & ffi::wlr_tablet_tool_axes_WLR_TABLET_TOOL_AXIS_PRESSURE) != 0;
+        if has_press {
+            ffi::wlr_tablet_v2_tablet_tool_notify_pressure(self.wp_tool, (*event).pressure);
+        }
+
+        let has_tilt_x = (updated & ffi::wlr_tablet_tool_axes_WLR_TABLET_TOOL_AXIS_TILT_X) != 0;
+        let has_tilt_y = (updated & ffi::wlr_tablet_tool_axes_WLR_TABLET_TOOL_AXIS_TILT_Y) != 0;
+        if has_tilt_x || has_tilt_y {
+            if has_tilt_x {
+                self.tilt_x = (*event).tilt_x;
+            }
+            if has_tilt_y {
+                self.tilt_y = (*event).tilt_y;
+            }
+            ffi::wlr_tablet_v2_tablet_tool_notify_tilt(self.wp_tool, self.tilt_x, self.tilt_y);
+        }
+
+        let has_rot = (updated & ffi::wlr_tablet_tool_axes_WLR_TABLET_TOOL_AXIS_ROTATION) != 0;
+        if has_rot {
+            ffi::wlr_tablet_v2_tablet_tool_notify_rotation(self.wp_tool, (*event).rotation);
+        }
+
+        let has_slider = (updated & ffi::wlr_tablet_tool_axes_WLR_TABLET_TOOL_AXIS_SLIDER) != 0;
+        if has_slider {
+            ffi::wlr_tablet_v2_tablet_tool_notify_slider(self.wp_tool, (*event).slider);
+        }
+
+        let has_wheel = (updated & ffi::wlr_tablet_tool_axes_WLR_TABLET_TOOL_AXIS_WHEEL) != 0;
+        if has_wheel {
+            ffi::wlr_tablet_v2_tablet_tool_notify_wheel(self.wp_tool, (*event).wheel_delta, 0);
+        }
+
+        self.detach(tablet);
+    }
+
+    pub unsafe fn proximity(
+        &mut self,
+        tablet: *mut Tablet,
+        event: *mut ffi::wlr_tablet_tool_proximity_event,
+    ) {
+        let state = (*event).state;
+        if state == ffi::wlr_tablet_tool_proximity_state_WLR_TABLET_TOOL_PROXIMITY_IN {
+            self.attach(tablet);
+
+            ffi::wlr_cursor_warp_absolute(
+                self.wlr_cursor,
+                (*(*tablet).device).wlr_device,
+                (*event).x,
+                (*event).y,
+            );
+
+            let xcursor_manager = (*(*(*tablet).device).seat).cursor.xcursor_manager;
+            ffi::wlr_cursor_set_xcursor(
+                self.wlr_cursor,
+                xcursor_manager,
+                b"pencil\0".as_ptr() as *const _,
+            );
+
+            self.passthrough(tablet);
+            self.detach(tablet);
+        } else {
+            ffi::wlr_tablet_v2_tablet_tool_notify_proximity_out(self.wp_tool);
+            ffi::wlr_cursor_unset_image(self.wlr_cursor);
+        }
+    }
+
+    pub unsafe fn tip(&mut self, tablet: *mut Tablet, event: *mut ffi::wlr_tablet_tool_tip_event) {
+        let state = (*event).state;
+        let is_down = ffi::river_wlr_tablet_v2_tablet_tool_get_is_down(self.wp_tool);
+        if state == ffi::wlr_tablet_tool_tip_state_WLR_TABLET_TOOL_TIP_DOWN {
+            if is_down {
+                return;
+            }
+            ffi::wlr_send_tablet_v2_tablet_tool_down(self.wp_tool);
+
+            let server = (*(*(*tablet).device).seat).server;
+            let lx = self.cursor_x();
+            let ly = self.cursor_y();
+            if let Some(result) = (*server).scene.at(lx, ly) {
+                if !result.surface.is_null() {
+                    self.mode = TabletToolMode::Down {
+                        lx,
+                        ly,
+                        sx: result.sx,
+                        sy: result.sy,
+                    };
+                }
+            }
+        } else {
+            if !is_down {
+                return;
+            }
+            ffi::wlr_send_tablet_v2_tablet_tool_up(self.wp_tool);
+            self.maybe_exit_down(tablet);
+        }
+    }
+
+    pub unsafe fn button(
+        &mut self,
+        tablet: *mut Tablet,
+        event: *mut ffi::wlr_tablet_tool_button_event,
+    ) {
+        ffi::wlr_tablet_v2_tablet_tool_notify_button(
+            self.wp_tool,
+            (*event).button,
+            (*event).state,
+        );
+        self.maybe_exit_down(tablet);
+    }
+
+    pub unsafe fn maybe_exit_down(&mut self, tablet: *mut Tablet) {
+        let is_down = ffi::river_wlr_tablet_v2_tablet_tool_get_is_down(self.wp_tool);
+        let num_buttons = ffi::river_wlr_tablet_v2_tablet_tool_get_num_buttons(self.wp_tool);
+        if !matches!(self.mode, TabletToolMode::Down { .. }) || is_down || num_buttons > 0 {
+            return;
+        }
+        self.mode = TabletToolMode::Passthrough;
+        self.passthrough(tablet);
+    }
+
+    pub unsafe fn passthrough(&mut self, tablet: *mut Tablet) {
+        let server = (*(*(*tablet).device).seat).server;
+        let lx = self.cursor_x();
+        let ly = self.cursor_y();
+
+        if let Some(result) = (*server).scene.at(lx, ly) {
+            if matches!(result.data, crate::scene_node_data::SceneNodeDataVal::LockSurface(_)) {
+                assert!((*server).lock_manager.state != crate::lock_manager::LockState::Unlocked);
+            } else {
+                assert!((*server).lock_manager.state != crate::lock_manager::LockState::Locked);
+            }
+
+            if !result.surface.is_null() {
+                ffi::wlr_send_tablet_v2_tablet_tool_proximity_in(self.wp_tool, (*tablet).wp_tablet, result.surface);
+                ffi::wlr_tablet_v2_tablet_tool_notify_motion(self.wp_tool, result.sx, result.sy);
+                return;
+            }
+        } else {
+            let xcursor_manager = (*(*(*tablet).device).seat).cursor.xcursor_manager;
+            ffi::wlr_cursor_set_xcursor(self.wlr_cursor, xcursor_manager, b"pencil\0".as_ptr() as *const _);
+        }
+
+        ffi::wlr_tablet_v2_tablet_tool_notify_proximity_out(self.wp_tool);
+    }
+}
+
+unsafe extern "C" fn handle_destroy(
+    listener: *mut ffi::wl_listener,
+    _data: *mut std::ffi::c_void,
+) {
+    let tool_ptr = crate::container_of!(listener, TabletTool, destroy_listener) as *mut TabletTool;
+    let tool = &mut *tool_ptr;
+
+    ffi::wlr_cursor_destroy(tool.wlr_cursor);
+
+    wl_listener_remove(&mut tool.destroy_listener);
+    wl_listener_remove(&mut tool.set_cursor_listener);
+
+    let _boxed = Box::from_raw(tool_ptr);
+}
+
+unsafe extern "C" fn handle_set_cursor(
+    listener: *mut ffi::wl_listener,
+    data: *mut std::ffi::c_void,
+) {
+    let tool = &mut *crate::container_of!(listener, TabletTool, set_cursor_listener);
+    let event = data as *mut ffi::wlr_tablet_v2_event_cursor;
+
+    if tool.allow_set_cursor((*event).seat_client, (*event).serial) {
+        ffi::wlr_cursor_set_surface(
+            tool.wlr_cursor,
+            (*event).surface,
+            (*event).hotspot_x,
+            (*event).hotspot_y,
+        );
+    }
+}
diff --git a/src/text_input.rs b/src/text_input.rs
new file mode 100644
index 0000000..23cab0e
--- /dev/null
+++ b/src/text_input.rs
@@ -0,0 +1,154 @@
+// SPDX-FileCopyrightText: © 2021 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::server::{WlListener, wl_listener_remove, wl_signal_add, WlList};
+use crate::seat::Seat;
+
+#[repr(C)]
+pub struct TextInput {
+    pub link: ffi::wl_list,
+    pub wlr_text_input: *mut ffi::wlr_text_input_v3,
+
+    pub enable: ffi::wl_listener,
+    pub commit: ffi::wl_listener,
+    pub disable: ffi::wl_listener,
+    pub destroy: ffi::wl_listener,
+}
+
+unsafe fn connect_listener(
+    signal: *mut ffi::wl_signal,
+    listener: *mut ffi::wl_listener,
+    callback: unsafe extern "C" fn(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void),
+) {
+    let wl_lis = listener as *mut WlListener;
+    (*wl_lis).notify = Some(callback);
+    wl_signal_add(signal, listener);
+}
+
+unsafe fn wl_listener_remove_safe(listener: *mut ffi::wl_listener) {
+    let prev = (*listener).link.prev;
+    let next = (*listener).link.next;
+    if !prev.is_null() && !next.is_null() && prev != listener as *mut ffi::wl_list && next != listener as *mut ffi::wl_list {
+        ffi::wl_list_remove(&mut (*listener).link);
+        (*listener).link.prev = std::ptr::null_mut();
+        (*listener).link.next = std::ptr::null_mut();
+    }
+}
+
+impl TextInput {
+    pub unsafe fn create(wlr_text_input: *mut ffi::wlr_text_input_v3) -> Result<(), &'static str> {
+        let wlr_seat = (*wlr_text_input).seat;
+        let seat = ffi::river_wlr_seat_get_data(wlr_seat) as *mut Seat;
+        if seat.is_null() {
+            return Err("Seat is null");
+        }
+
+        let seat_name = std::ffi::CStr::from_ptr(ffi::river_wlr_seat_get_name(wlr_seat))
+            .to_str()
+            .unwrap_or("unknown");
+        log::debug!("new text input on seat {}", seat_name);
+
+        let mut text_input = Box::new(TextInput {
+            link: std::mem::zeroed(),
+            wlr_text_input,
+            enable: std::mem::zeroed(),
+            commit: std::mem::zeroed(),
+            disable: std::mem::zeroed(),
+            destroy: std::mem::zeroed(),
+        });
+
+        ffi::wl_list_init(&mut text_input.link);
+
+        let raw = Box::into_raw(text_input);
+
+        // Append to seat.relay.text_inputs
+        let text_inputs_list = &mut (*seat).relay.text_inputs as *mut ffi::wl_list as *mut WlList;
+        crate::server::wl_list_insert((*text_inputs_list).prev, &mut (*raw).link as *mut ffi::wl_list as *mut WlList);
+
+        connect_listener(&mut (*wlr_text_input).events.enable, &mut (*raw).enable, handle_enable);
+        connect_listener(&mut (*wlr_text_input).events.commit, &mut (*raw).commit, handle_commit);
+        connect_listener(&mut (*wlr_text_input).events.disable, &mut (*raw).disable, handle_disable);
+        connect_listener(&mut (*wlr_text_input).events.destroy, &mut (*raw).destroy, handle_destroy);
+
+        Ok(())
+    }
+}
+
+unsafe extern "C" fn handle_enable(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let text_input = crate::container_of!(listener, TextInput, enable);
+    let seat = ffi::river_wlr_seat_get_data((*(*text_input).wlr_text_input).seat) as *mut Seat;
+    if seat.is_null() {
+        return;
+    }
+
+    if (*(*text_input).wlr_text_input).focused_surface.is_null() {
+        log::error!("client requested to enable text input without focus, ignoring request");
+        return;
+    }
+
+    if !(*seat).relay.text_input.is_null() {
+        if text_input != (*seat).relay.text_input {
+            log::error!("client requested to enable more than one text input on a single seat, ignoring request");
+            return;
+        }
+    }
+
+    (*seat).relay.text_input = text_input;
+
+    let input_method = (*seat).relay.input_method;
+    if !input_method.is_null() {
+        ffi::wlr_input_method_v2_send_activate(input_method);
+        (*seat).relay.send_input_method_state();
+    }
+}
+
+unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let text_input = crate::container_of!(listener, TextInput, commit);
+    let seat = ffi::river_wlr_seat_get_data((*(*text_input).wlr_text_input).seat) as *mut Seat;
+    if seat.is_null() {
+        return;
+    }
+
+    if (*seat).relay.text_input != text_input {
+        log::error!("inactive text input tried to commit an update, client bug?");
+        return;
+    }
+
+    if !(*seat).relay.input_method.is_null() {
+        (*seat).relay.send_input_method_state();
+    }
+}
+
+unsafe extern "C" fn handle_disable(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let text_input = crate::container_of!(listener, TextInput, disable);
+    let seat = ffi::river_wlr_seat_get_data((*(*text_input).wlr_text_input).seat) as *mut Seat;
+    if seat.is_null() {
+        return;
+    }
+
+    if (*seat).relay.text_input == text_input {
+        (*seat).relay.disable_text_input();
+    }
+}
+
+unsafe extern "C" fn handle_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
+    let text_input = crate::container_of!(listener, TextInput, destroy);
+    let seat = ffi::river_wlr_seat_get_data((*(*text_input).wlr_text_input).seat) as *mut Seat;
+    if seat.is_null() {
+        return;
+    }
+
+    if (*seat).relay.text_input == text_input {
+        (*seat).relay.disable_text_input();
+    }
+
+    wl_listener_remove_safe(&mut (*text_input).enable);
+    wl_listener_remove_safe(&mut (*text_input).commit);
+    wl_listener_remove_safe(&mut (*text_input).disable);
+    wl_listener_remove_safe(&mut (*text_input).destroy);
+
+    ffi::wl_list_remove(&mut (*text_input).link);
+
+    let _ = Box::from_raw(text_input);
+}
diff --git a/src/util.rs b/src/util.rs
new file mode 100644
index 0000000..2eddfa4
--- /dev/null
+++ b/src/util.rs
@@ -0,0 +1,21 @@
+// SPDX-FileCopyrightText: © 2022 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub fn timestamp() -> libc::timespec {
+    let mut ts = libc::timespec { tv_sec: 0, tv_nsec: 0 };
+    unsafe {
+        if libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts) < 0 {
+            panic!("CLOCK_MONOTONIC not supported");
+        }
+    }
+    ts
+}
+
+pub fn msec_timestamp() -> u32 {
+    let now = timestamp();
+    // 2^32-1 milliseconds is ~50 days.
+    // Wrap it using wrapping arithmetic.
+    let secs_ms = (now.tv_sec as u64).wrapping_mul(1000);
+    let nsecs_ms = (now.tv_nsec as u64) / 1_000_000;
+    secs_ms.wrapping_add(nsecs_ms) as u32
+}
diff --git a/src/window.rs b/src/window.rs
new file mode 100644
index 0000000..15ecd5c
--- /dev/null
+++ b/src/window.rs
@@ -0,0 +1,2038 @@
+// SPDX-FileCopyrightText: © 2020 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::server::{Server, WlList, wl_list_insert, wl_list_remove};
+use crate::wm_node::WmNode;
+use crate::xdg_toplevel::ConfigureState;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum WindowState {
+    Init,
+    Ready,
+    Initialized,
+    Mapped,
+    Closing,
+}
+
+#[derive(Clone, Copy)]
+pub enum WindowImpl {
+    Toplevel(*mut crate::xdg_toplevel::XdgToplevel),
+    Xwayland(*mut crate::xwayland_window::XwaylandWindow),
+    Destroying,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum FullscreenRequest {
+    NoRequest,
+    Fullscreen(*mut crate::output::Output),
+    Exit,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MaximizeRequest {
+    NoRequest,
+    Maximize,
+    Unmaximize,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct Dimensions {
+    pub width: u32,
+    pub height: u32,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct DimensionsHint {
+    pub min_width: u32,
+    pub min_height: u32,
+    pub max_width: u32,
+    pub max_height: u32,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct Edges {
+    pub top: bool,
+    pub bottom: bool,
+    pub left: bool,
+    pub right: bool,
+}
+
+impl Edges {
+    pub fn new() -> Self {
+        Self { top: false, bottom: false, left: false, right: false }
+    }
+    pub fn from_u32(val: u32) -> Self {
+        Self {
+            top: (val & 1) != 0,
+            bottom: (val & 2) != 0,
+            left: (val & 4) != 0,
+            right: (val & 8) != 0,
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct Border {
+    pub edges: Edges,
+    pub width: u32,
+    pub r: u32,
+    pub g: u32,
+    pub b: u32,
+    pub a: u32,
+}
+
+pub struct BorderRects {
+    pub left: *mut ffi::wlr_scene_rect,
+    pub right: *mut ffi::wlr_scene_rect,
+    pub top: *mut ffi::wlr_scene_rect,
+    pub bottom: *mut ffi::wlr_scene_rect,
+}
+
+pub struct ShowWindowMenuRequest {
+    pub x: i32,
+    pub y: i32,
+}
+
+pub struct PointerResizeRequest {
+    pub seat: *mut crate::seat::Seat,
+    pub edges: u32,
+}
+
+pub struct WmScheduledState {
+    pub dimensions_hint: DimensionsHint,
+    pub decoration_hint: ffi::river_window_v1_decoration_hint,
+    pub show_window_menu_requested: Option<ShowWindowMenuRequest>,
+    pub fullscreen_requested: FullscreenRequest,
+    pub maximize_requested: MaximizeRequest,
+    pub minimize_requested: bool,
+    pub dirty_app_id: bool,
+    pub dirty_title: bool,
+    pub pointer_move_requested: *mut crate::seat::Seat,
+    pub pointer_resize_requested: Option<PointerResizeRequest>,
+}
+
+pub struct WmSentState {
+    pub dimensions_hint: DimensionsHint,
+    pub decoration_hint: ffi::river_window_v1_decoration_hint,
+    pub parent: Option<crate::slotmap::Key>,
+}
+
+pub struct WmRequestedState {
+    pub dimensions: Option<Dimensions>,
+    pub bounds: Dimensions,
+    pub ssd: bool,
+    pub tiled: u32,
+    pub capabilities: u32,
+    pub resizing: bool,
+    pub maximized: bool,
+    pub fullscreen: *mut crate::output::Output,
+    pub inform_fullscreen: bool,
+    pub close: bool,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Configure {
+    pub width: Option<u32>,
+    pub height: Option<u32>,
+    pub bounds: Dimensions,
+    pub activated: bool,
+    pub ssd: bool,
+    pub tiled: u32,
+    pub capabilities: u32,
+    pub maximized: bool,
+    pub inform_fullscreen: bool,
+    pub resizing: bool,
+}
+
+impl Configure {
+    pub fn new() -> Self {
+        Self {
+            width: None,
+            height: None,
+            bounds: Dimensions { width: 0, height: 0 },
+            activated: false,
+            ssd: false,
+            tiled: 0,
+            capabilities: 0,
+            maximized: false,
+            inform_fullscreen: false,
+            resizing: false,
+        }
+    }
+}
+
+pub struct WindowRenderingScheduled {
+    pub width: u32,
+    pub height: u32,
+    pub resend_dimensions: bool,
+}
+
+pub struct WindowRenderingSent {
+    pub width: u32,
+    pub height: u32,
+    pub presentation_hint: ffi::river_output_v1_presentation_mode,
+}
+
+pub struct WindowRenderingRequested {
+    pub x: i32,
+    pub y: i32,
+    pub hidden: bool,
+    pub border: Border,
+    pub clip: ffi::wlr_box,
+    pub content_clip: ffi::wlr_box,
+}
+
+pub struct Window {
+    pub ref_key: crate::slotmap::Key,
+    pub server: *mut Server,
+    pub object: *mut ffi::wl_resource, // river_window_v1
+    pub node: WmNode,
+    pub state: WindowState,
+    pub impl_type: WindowImpl,
+
+    pub tree: *mut ffi::wlr_scene_tree,
+    pub fullscreen_background: *mut ffi::wlr_scene_rect,
+    pub decorations_below: ffi::wl_list,
+    pub decorations_below_tree: *mut ffi::wlr_scene_tree,
+    pub surfaces: crate::scene::SaveableSurfaces,
+    pub border: BorderRects,
+    pub decorations_above: ffi::wl_list,
+    pub decorations_above_tree: *mut ffi::wlr_scene_tree,
+    pub popup_tree: *mut ffi::wlr_scene_tree,
+    pub capture_scene: *mut ffi::wlr_scene,
+    pub capture_source: *mut ffi::wlr_ext_image_capture_source_v1,
+
+    pub wm_scheduled: WmScheduledState,
+    pub wm_sent: WmSentState,
+    pub wm_requested: WmRequestedState,
+    pub configure_scheduled: Configure,
+    pub configure_sent: Configure,
+    pub rendering_scheduled: WindowRenderingScheduled,
+    pub rendering_sent: WindowRenderingSent,
+    pub rendering_requested: WindowRenderingRequested,
+    pub box_geom: ffi::wlr_box,
+    pub foreign_toplevel_handle: *mut ffi::wlr_ext_foreign_toplevel_handle_v1,
+    pub wlr_toplevel_handle: *mut ffi::wlr_foreign_toplevel_handle_v1,
+}
+
+impl Window {
+    pub unsafe fn create(impl_type: WindowImpl, server: *mut Server) -> Result<*mut Self, &'static str> {
+        let hidden_tree = (*server).scene.hidden_tree;
+        let tree = ffi::wlr_scene_tree_create(hidden_tree);
+        if tree.is_null() {
+            return Err("Failed to create tree");
+        }
+
+        let popup_tree = ffi::wlr_scene_tree_create(hidden_tree);
+        if popup_tree.is_null() {
+            ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
+            return Err("Failed to create popup_tree");
+        }
+
+        let capture_scene = ffi::wlr_scene_create();
+        if capture_scene.is_null() {
+            ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
+            ffi::wlr_scene_node_destroy(popup_tree as *mut ffi::wlr_scene_node);
+            return Err("Failed to create capture_scene");
+        }
+        (*capture_scene).restack_xwayland_surfaces = false;
+
+        let black_color = [0.0f32, 0.0f32, 0.0f32, 1.0f32];
+        let fullscreen_background = ffi::wlr_scene_rect_create(tree, 0, 0, black_color.as_ptr());
+        if fullscreen_background.is_null() {
+            ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
+            ffi::wlr_scene_node_destroy(popup_tree as *mut ffi::wlr_scene_node);
+            ffi::wlr_scene_node_destroy(&mut (*capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node);
+            return Err("Failed to create fullscreen rect");
+        }
+
+        let clear_color = [0.0f32, 0.0f32, 0.0f32, 0.0f32];
+        let border_left = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
+        let border_right = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
+        let border_top = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
+        let border_bottom = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
+
+        let decorations_below_tree = ffi::wlr_scene_tree_create(tree);
+        let decorations_above_tree = ffi::wlr_scene_tree_create(tree);
+
+        let surfaces = match crate::scene::SaveableSurfaces::init(tree) {
+            Ok(s) => s,
+            Err(e) => {
+                ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
+                ffi::wlr_scene_node_destroy(popup_tree as *mut ffi::wlr_scene_node);
+                ffi::wlr_scene_node_destroy(&mut (*capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node);
+                return Err(e);
+            }
+        };
+
+        let mut window = Box::new(Window {
+            ref_key: crate::slotmap::Key { generation: 0, index: 0 },
+            server,
+            object: std::ptr::null_mut(),
+            node: std::mem::zeroed(),
+            state: WindowState::Init,
+            impl_type,
+            tree,
+            fullscreen_background,
+            decorations_below: std::mem::zeroed(),
+            decorations_below_tree,
+            surfaces,
+            border: BorderRects {
+                left: border_left,
+                right: border_right,
+                top: border_top,
+                bottom: border_bottom,
+            },
+            decorations_above: std::mem::zeroed(),
+            decorations_above_tree,
+            popup_tree,
+            capture_scene,
+            capture_source: std::ptr::null_mut(),
+            wm_scheduled: WmScheduledState {
+                dimensions_hint: DimensionsHint { min_width: 0, min_height: 0, max_width: 0, max_height: 0 },
+                decoration_hint: ffi::river_window_v1_decoration_hint_RIVER_WINDOW_V1_DECORATION_HINT_ONLY_SUPPORTS_CSD,
+                show_window_menu_requested: None,
+                fullscreen_requested: FullscreenRequest::NoRequest,
+                maximize_requested: MaximizeRequest::NoRequest,
+                minimize_requested: false,
+                dirty_app_id: false,
+                dirty_title: false,
+                pointer_move_requested: std::ptr::null_mut(),
+                pointer_resize_requested: None,
+            },
+            wm_sent: WmSentState {
+                dimensions_hint: DimensionsHint { min_width: 0, min_height: 0, max_width: 0, max_height: 0 },
+                decoration_hint: ffi::river_window_v1_decoration_hint_RIVER_WINDOW_V1_DECORATION_HINT_ONLY_SUPPORTS_CSD,
+                parent: None,
+            },
+            wm_requested: WmRequestedState {
+                dimensions: None,
+                bounds: Dimensions { width: 0, height: 0 },
+                ssd: false,
+                tiled: 0,
+                capabilities: 1 | 2 | 4 | 8,
+                resizing: false,
+                maximized: false,
+                fullscreen: std::ptr::null_mut(),
+                inform_fullscreen: false,
+                close: false,
+            },
+            configure_scheduled: Configure::new(),
+            configure_sent: Configure::new(),
+            rendering_scheduled: WindowRenderingScheduled {
+                width: 0,
+                height: 0,
+                resend_dimensions: false,
+            },
+            rendering_sent: WindowRenderingSent {
+                width: 0,
+                height: 0,
+                presentation_hint: ffi::river_output_v1_presentation_mode_RIVER_OUTPUT_V1_PRESENTATION_MODE_VSYNC,
+            },
+            rendering_requested: WindowRenderingRequested {
+                x: 0,
+                y: 0,
+                hidden: false,
+                border: Border { edges: Edges::new(), width: 0, r: 0, g: 0, b: 0, a: 0 },
+                clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
+                content_clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
+            },
+            box_geom: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
+            foreign_toplevel_handle: std::ptr::null_mut(),
+            wlr_toplevel_handle: std::ptr::null_mut(),
+        });
+
+        ffi::wl_list_init(&mut window.decorations_below);
+        ffi::wl_list_init(&mut window.decorations_above);
+
+        let raw = Box::into_raw(window);
+        (*raw).node.init(crate::wm_node::WmNodeTag::Window);
+
+        ffi::wlr_scene_node_set_enabled(tree as *mut ffi::wlr_scene_node, false);
+        ffi::wlr_scene_node_set_enabled(popup_tree as *mut ffi::wlr_scene_node, false);
+        ffi::wlr_scene_node_set_enabled(fullscreen_background as *mut ffi::wlr_scene_node, false);
+
+        crate::scene_node_data::SceneNodeData::attach(
+            tree as *mut ffi::wlr_scene_node,
+            crate::scene_node_data::SceneNodeDataVal::Window(raw),
+        );
+        crate::scene_node_data::SceneNodeData::attach(
+            popup_tree as *mut ffi::wlr_scene_node,
+            crate::scene_node_data::SceneNodeDataVal::Window(raw),
+        );
+
+        Ok(raw)
+    }
+
+    pub unsafe fn set_impl(&mut self, impl_type: WindowImpl) {
+        self.impl_type = impl_type;
+    }
+
+    pub unsafe fn impl_destroying(&mut self) {
+        self.impl_type = WindowImpl::Destroying;
+    }
+
+    pub unsafe fn get_title(&self) -> *const libc::c_char {
+        match self.impl_type {
+            WindowImpl::Toplevel(toplevel) => {
+                if toplevel.is_null() {
+                    std::ptr::null()
+                } else {
+                    ffi::river_wlr_xdg_toplevel_get_title((*toplevel).wlr_toplevel)
+                }
+            }
+            WindowImpl::Xwayland(xwindow) => {
+                if xwindow.is_null() {
+                    std::ptr::null()
+                } else {
+                    (*(*xwindow).xsurface).title
+                }
+            }
+            WindowImpl::Destroying => std::ptr::null(),
+        }
+    }
+
+    pub unsafe fn get_app_id(&self) -> *const libc::c_char {
+        match self.impl_type {
+            WindowImpl::Toplevel(toplevel) => {
+                if toplevel.is_null() {
+                    std::ptr::null()
+                } else {
+                    ffi::river_wlr_xdg_toplevel_get_app_id((*toplevel).wlr_toplevel)
+                }
+            }
+            WindowImpl::Xwayland(xwindow) => {
+                if xwindow.is_null() {
+                    std::ptr::null()
+                } else {
+                    (*(*xwindow).xsurface).class
+                }
+            }
+            WindowImpl::Destroying => std::ptr::null(),
+        }
+    }
+
+    pub unsafe fn get_parent(&self) -> *mut Window {
+        match self.impl_type {
+            WindowImpl::Toplevel(toplevel) => {
+                if toplevel.is_null() {
+                    std::ptr::null_mut()
+                } else {
+                    let wlr_parent = ffi::river_wlr_xdg_toplevel_get_parent((*toplevel).wlr_toplevel);
+                    if wlr_parent.is_null() {
+                        std::ptr::null_mut()
+                    } else {
+                        let base = ffi::river_wlr_xdg_toplevel_get_base(wlr_parent);
+                        let parent_xdg = ffi::river_wlr_xdg_surface_get_data(base) as *mut crate::xdg_toplevel::XdgToplevel;
+                        if parent_xdg.is_null() {
+                            std::ptr::null_mut()
+                        } else {
+                            (*parent_xdg).window
+                        }
+                    }
+                }
+            }
+            WindowImpl::Xwayland(xwindow) => {
+                if xwindow.is_null() {
+                    std::ptr::null_mut()
+                } else {
+                    let parent_xsurface = (*(*xwindow).xsurface).parent;
+                    if parent_xsurface.is_null() {
+                        std::ptr::null_mut()
+                    } else {
+                        let parent_data = (*parent_xsurface).data;
+                        if parent_data.is_null() {
+                            std::ptr::null_mut()
+                        } else {
+                            let parent_xwindow = parent_data as *mut crate::xwayland_window::XwaylandWindow;
+                            (*parent_xwindow).window
+                        }
+                    }
+                }
+            }
+            WindowImpl::Destroying => std::ptr::null_mut(),
+        }
+    }
+
+    pub unsafe fn unreliable_pid(&self) -> i32 {
+        match self.impl_type {
+            WindowImpl::Toplevel(toplevel) => {
+                if toplevel.is_null() {
+                    0
+                } else {
+                    let base = ffi::river_wlr_xdg_toplevel_get_base((*toplevel).wlr_toplevel);
+                    let surface = ffi::river_wlr_xdg_surface_get_surface(base);
+                    if surface.is_null() {
+                        0
+                    } else {
+                        let res = ffi::river_wlr_surface_get_resource(surface);
+                        if res.is_null() {
+                            0
+                        } else {
+                            let client = ffi::wl_resource_get_client(res);
+                            if client.is_null() {
+                                0
+                            } else {
+                                let mut pid = 0;
+                                let mut uid = 0;
+                                let mut gid = 0;
+                                ffi::wl_client_get_credentials(client, &mut pid, &mut uid, &mut gid);
+                                pid
+                            }
+                        }
+                    }
+                }
+            }
+            WindowImpl::Xwayland(xwindow) => {
+                if xwindow.is_null() {
+                    0
+                } else {
+                    (*(*xwindow).xsurface).pid
+                }
+            }
+            WindowImpl::Destroying => 0,
+        }
+    }
+
+    pub unsafe fn map(&mut self) -> Result<(), &'static str> {
+        log::debug!("window '{:?}' mapped", self.get_title());
+        assert!(!matches!(self.impl_type, WindowImpl::Destroying));
+        assert_eq!(self.state, WindowState::Initialized);
+        self.state = WindowState::Mapped;
+        Ok(())
+    }
+
+    pub unsafe fn unmap(&mut self) {
+        log::debug!("window '{:?}' unmapped", self.get_title());
+        self.surfaces.save();
+        assert!(!matches!(self.impl_type, WindowImpl::Destroying));
+        assert_eq!(self.state, WindowState::Mapped);
+        self.state = WindowState::Closing;
+        (*self.server).wm.dirty_windowing();
+
+        if !self.foreign_toplevel_handle.is_null() {
+            ffi::wlr_ext_foreign_toplevel_handle_v1_destroy(self.foreign_toplevel_handle);
+            self.foreign_toplevel_handle = std::ptr::null_mut();
+        }
+        if !self.wlr_toplevel_handle.is_null() {
+            ffi::wlr_foreign_toplevel_handle_v1_destroy(self.wlr_toplevel_handle);
+            self.wlr_toplevel_handle = std::ptr::null_mut();
+        }
+    }
+
+    pub unsafe fn close(&mut self) {
+        match self.impl_type {
+            WindowImpl::Toplevel(toplevel) => {
+                if !toplevel.is_null() {
+                    ffi::wlr_xdg_toplevel_send_close((*toplevel).wlr_toplevel);
+                }
+            }
+            WindowImpl::Xwayland(xwindow) => {
+                if !xwindow.is_null() {
+                    ffi::wlr_xwayland_surface_close((*xwindow).xsurface);
+                }
+            }
+            WindowImpl::Destroying => {}
+        }
+    }
+
+    pub unsafe fn destroy(window: *mut Window) {
+        assert!(matches!((*window).impl_type, WindowImpl::Destroying));
+        match (*window).state {
+            WindowState::Init => {}
+            WindowState::Closing => {
+                (*(*window).server).wm.dirty_windowing();
+                return;
+            }
+            _ => unreachable!(),
+        }
+        assert!((*window).object.is_null());
+
+        let seats = &mut (*(*window).server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*seats).next;
+        while curr != seats {
+            let next = (*curr).next;
+            let seat = crate::container_of!(curr, crate::seat::Seat, link);
+            if let crate::seat::Focus::Window(w) = (*seat).focused {
+                if w == window {
+                    (*seat).focus(crate::seat::Focus::None);
+                }
+            }
+            curr = next;
+        }
+
+        // Destroy decorations
+        for decorations in [&mut (*window).decorations_above as *mut ffi::wl_list, &mut (*window).decorations_below as *mut ffi::wl_list] {
+            let list_head = decorations as *mut WlList;
+            let mut curr = (*list_head).next;
+            while curr != list_head {
+                let next = (*curr).next;
+                let dec = crate::container_of!(curr, Decoration, link);
+                (*dec).destroy();
+                curr = next;
+            }
+        }
+
+        ffi::wlr_scene_node_destroy((*window).tree as *mut ffi::wlr_scene_node);
+        ffi::wlr_scene_node_destroy((*window).popup_tree as *mut ffi::wlr_scene_node);
+        ffi::wlr_scene_node_destroy(&mut (*(*window).capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node);
+
+        (*window).node.deinit();
+
+        let _ = Box::from_raw(window);
+    }
+
+    pub unsafe fn set_dimensions_hint(&mut self, hint: DimensionsHint) {
+        self.wm_scheduled.dimensions_hint = hint;
+        if self.wm_sent.dimensions_hint != hint {
+            (*self.server).wm.dirty_windowing();
+        }
+    }
+
+    pub unsafe fn set_dimensions(&mut self, width: u32, height: u32) {
+        self.rendering_scheduled.width = width;
+        self.rendering_scheduled.height = height;
+
+        if self.rendering_scheduled.resend_dimensions ||
+           self.rendering_scheduled.width != self.rendering_sent.width ||
+           self.rendering_scheduled.height != self.rendering_sent.height {
+            (*self.server).wm.dirty_rendering();
+        }
+    }
+
+    pub unsafe fn set_decoration_hint(&mut self, hint: ffi::river_window_v1_decoration_hint) {
+        self.wm_scheduled.decoration_hint = hint;
+        if hint != self.wm_sent.decoration_hint {
+            (*self.server).wm.dirty_windowing();
+        }
+    }
+
+    pub unsafe fn root_surface(&self) -> *mut ffi::wlr_surface {
+        match self.impl_type {
+            WindowImpl::Toplevel(toplevel) => {
+                if toplevel.is_null() {
+                    std::ptr::null_mut()
+                } else {
+                    let base = ffi::river_wlr_xdg_toplevel_get_base((*toplevel).wlr_toplevel);
+                    ffi::river_wlr_xdg_surface_get_surface(base)
+                }
+            }
+            _ => std::ptr::null_mut(),
+        }
+    }
+
+    pub unsafe fn send_frame_done(&self) {
+        assert_eq!(self.state, WindowState::Mapped);
+        if !matches!(self.impl_type, WindowImpl::Destroying) {
+            let mut now = std::mem::zeroed();
+            clock_gettime(libc::CLOCK_MONOTONIC, &mut now);
+            let now_ffi = ffi::timespec {
+                tv_sec: now.tv_sec as _,
+                tv_nsec: now.tv_nsec as _,
+            };
+            ffi::wlr_surface_send_frame_done(self.root_surface(), &now_ffi);
+        }
+    }
+
+    pub unsafe fn manage_start(&mut self) {
+        match self.state {
+            WindowState::Init => {}
+            WindowState::Closing => {
+                self.state = WindowState::Init;
+                self.wm_sent = WmSentState {
+                    dimensions_hint: DimensionsHint { min_width: 0, min_height: 0, max_width: 0, max_height: 0 },
+                    decoration_hint: ffi::river_window_v1_decoration_hint_RIVER_WINDOW_V1_DECORATION_HINT_ONLY_SUPPORTS_CSD,
+                    parent: None,
+                };
+                self.wm_requested = WmRequestedState {
+                    dimensions: None,
+                    bounds: Dimensions { width: 0, height: 0 },
+                    ssd: false,
+                    tiled: 0,
+                    capabilities: 1 | 2 | 4 | 8,
+                    resizing: false,
+                    maximized: false,
+                    fullscreen: std::ptr::null_mut(),
+                    inform_fullscreen: false,
+                    close: false,
+                };
+                self.rendering_sent = WindowRenderingSent {
+                    width: 0,
+                    height: 0,
+                    presentation_hint: ffi::river_output_v1_presentation_mode_RIVER_OUTPUT_V1_PRESENTATION_MODE_VSYNC,
+                };
+                self.rendering_requested = WindowRenderingRequested {
+                    x: 0,
+                    y: 0,
+                    hidden: false,
+                    border: Border { edges: Edges::new(), width: 0, r: 0, g: 0, b: 0, a: 0 },
+                    clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
+                    content_clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
+                };
+
+                wl_list_remove(&mut self.node.link as *mut ffi::wl_list as *mut WlList);
+                self.node.link.prev = &mut self.node.link;
+                self.node.link.next = &mut self.node.link;
+
+                self.make_inert();
+            }
+            WindowState::Ready | WindowState::Initialized | WindowState::Mapped => {
+                let wm_v1 = (*self.server).wm.object;
+                if wm_v1.is_null() {
+                    return;
+                }
+                let new_resource = self.object.is_null();
+                let window_v1 = if new_resource {
+                    let client = ffi::wl_resource_get_client(wm_v1);
+                    let res = ffi::wl_resource_create(client, &ffi::river_window_v1_interface, ffi::wl_resource_get_version(wm_v1), 0);
+                    if res.is_null() {
+                        log::error!("out of memory");
+                        return;
+                    }
+                    self.object = res;
+                    ffi::wl_resource_set_implementation(
+                        res,
+                        &WINDOW_INTERFACE as *const _ as *const _,
+                        self as *mut Window as *mut _,
+                        Some(handle_destroy_resource),
+                    );
+                    
+                    // Send window to manager
+                    ffi::wl_resource_post_event(wm_v1, 0, res); // river_window_manager_v1.window
+
+                    wl_list_remove(&mut self.node.link as *mut ffi::wl_list as *mut WlList);
+                    let rendering_list = &mut (*self.server).wm.rendering_requested.list as *mut ffi::wl_list as *mut WlList;
+                    wl_list_insert((*rendering_list).prev, &mut self.node.link as *mut ffi::wl_list as *mut WlList);
+
+                    if self.foreign_toplevel_handle.is_null() {
+                        let list = (*self.server).foreign_toplevel_list;
+                        let title = self.get_title();
+                        let app_id = self.get_app_id();
+                        let state = ffi::wlr_ext_foreign_toplevel_handle_v1_state {
+                            title,
+                            app_id,
+                        };
+                        let handle = ffi::wlr_ext_foreign_toplevel_handle_v1_create(list, &state);
+                        if !handle.is_null() {
+                            self.foreign_toplevel_handle = handle;
+                            (*handle).data = self as *mut Window as *mut _;
+                        }
+                    }
+
+                    if self.wlr_toplevel_handle.is_null() {
+                        let manager = (*self.server).wlr_foreign_toplevel_manager;
+                        let handle = ffi::wlr_foreign_toplevel_handle_v1_create(manager);
+                        if !handle.is_null() {
+                            self.wlr_toplevel_handle = handle;
+                            let title = self.get_title();
+                            if !title.is_null() {
+                                ffi::wlr_foreign_toplevel_handle_v1_set_title(handle, title);
+                            }
+                            let app_id = self.get_app_id();
+                            if !app_id.is_null() {
+                                ffi::wlr_foreign_toplevel_handle_v1_set_app_id(handle, app_id);
+                            }
+                        }
+                    }
+
+                    res
+                } else {
+                    self.object
+                };
+
+                if new_resource {
+                    let version = ffi::wl_resource_get_version(window_v1);
+                    if version >= 2 {
+                        ffi::wl_resource_post_event(window_v1, 14, self.unreliable_pid()); // sendUnreliablePid
+                    }
+                    if version >= 4 {
+                        if !self.foreign_toplevel_handle.is_null() {
+                            let identifier = (*self.foreign_toplevel_handle).identifier;
+                            ffi::wl_resource_post_event(window_v1, 16, identifier);
+                        }
+                    }
+                }
+
+                if new_resource || self.wm_scheduled.dimensions_hint != self.wm_sent.dimensions_hint {
+                    ffi::wl_resource_post_event(
+                        window_v1,
+                        2, // sendDimensionsHint
+                        self.wm_scheduled.dimensions_hint.min_width as i32,
+                        self.wm_scheduled.dimensions_hint.min_height as i32,
+                        self.wm_scheduled.dimensions_hint.max_width as i32,
+                        self.wm_scheduled.dimensions_hint.max_height as i32,
+                    );
+                    self.wm_sent.dimensions_hint = self.wm_scheduled.dimensions_hint;
+                }
+
+                if new_resource || self.wm_scheduled.decoration_hint != self.wm_sent.decoration_hint {
+                    ffi::wl_resource_post_event(window_v1, 1, self.wm_scheduled.decoration_hint); // sendDecorationHint
+                    self.wm_sent.decoration_hint = self.wm_scheduled.decoration_hint;
+                }
+
+                if let Some(ref offset) = self.wm_scheduled.show_window_menu_requested {
+                    ffi::wl_resource_post_event(window_v1, 3, offset.x, offset.y); // sendShowWindowMenuRequested
+                    self.wm_scheduled.show_window_menu_requested = None;
+                }
+
+                match self.wm_scheduled.fullscreen_requested {
+                    FullscreenRequest::NoRequest => {}
+                    FullscreenRequest::Fullscreen(output) => {
+                        let out_resource = if output.is_null() { std::ptr::null_mut() } else { (*output).object };
+                        ffi::wl_resource_post_event(window_v1, 4, out_resource); // sendFullscreenRequested
+                    }
+                    FullscreenRequest::Exit => {
+                        ffi::wl_resource_post_event(window_v1, 5); // sendExitFullscreenRequested
+                    }
+                }
+                self.wm_scheduled.fullscreen_requested = FullscreenRequest::NoRequest;
+
+                match self.wm_scheduled.maximize_requested {
+                    MaximizeRequest::NoRequest => {}
+                    MaximizeRequest::Maximize => {
+                        ffi::wl_resource_post_event(window_v1, 6); // sendMaximizeRequested
+                    }
+                    MaximizeRequest::Unmaximize => {
+                        ffi::wl_resource_post_event(window_v1, 7); // sendUnmaximizeRequested
+                    }
+                }
+                self.wm_scheduled.maximize_requested = MaximizeRequest::NoRequest;
+
+                if self.wm_scheduled.minimize_requested {
+                    ffi::wl_resource_post_event(window_v1, 8); // sendMinimizeRequested
+                }
+                self.wm_scheduled.minimize_requested = false;
+
+                let parent = self.get_parent();
+                if !parent.is_null() {
+                    let parent_ref = Some((*parent).ref_key);
+                    if self.wm_sent.parent.is_none() || self.wm_sent.parent != parent_ref {
+                        let parent_obj = (*parent).object;
+                        ffi::wl_resource_post_event(window_v1, 11, parent_obj); // sendParent
+                        self.wm_sent.parent = parent_ref;
+                    }
+                } else if self.wm_sent.parent.is_some() {
+                    ffi::wl_resource_post_event(window_v1, 11, std::ptr::null_mut::<ffi::wl_resource>()); // sendParent
+                    self.wm_sent.parent = None;
+                }
+
+                if new_resource || self.wm_scheduled.dirty_app_id {
+                    let app_id = self.get_app_id();
+                    ffi::wl_resource_post_event(window_v1, 13, app_id); // sendAppId
+                    self.wm_scheduled.dirty_app_id = false;
+                }
+
+                if new_resource || self.wm_scheduled.dirty_title {
+                    let title = self.get_title();
+                    ffi::wl_resource_post_event(window_v1, 12, title); // sendTitle
+                    self.wm_scheduled.dirty_title = false;
+                }
+
+                if let Some(seat) = self.wm_scheduled.pointer_move_requested.as_mut() {
+                    if !seat.object.is_null() {
+                        ffi::wl_resource_post_event(window_v1, 9, seat.object); // sendPointerMoveRequested
+                    }
+                }
+                self.wm_scheduled.pointer_move_requested = std::ptr::null_mut();
+
+                if let Some(ref data) = self.wm_scheduled.pointer_resize_requested {
+                    if !(*data.seat).object.is_null() {
+                        ffi::wl_resource_post_event(window_v1, 10, (*data.seat).object, data.edges); // sendPointerResizeRequested
+                    }
+                }
+                self.wm_scheduled.pointer_resize_requested = None;
+            }
+        }
+    }
+
+    pub unsafe fn make_inert(&mut self) {
+        if !self.object.is_null() {
+            ffi::wl_resource_post_event(self.object, 15); // sendClosed
+            ffi::wl_resource_set_implementation(
+                self.object,
+                &INERT_WINDOW_INTERFACE as *const _ as *const _,
+                std::ptr::null_mut(),
+                None,
+            );
+            self.object = std::ptr::null_mut();
+            (*self.server).wm.dirty_windowing();
+            self.node.make_inert();
+
+            for decorations in [&mut self.decorations_above as *mut ffi::wl_list, &mut self.decorations_below as *mut ffi::wl_list] {
+                let list_head = decorations as *mut WlList;
+                let mut curr = (*list_head).next;
+                while curr != list_head {
+                    let next = (*curr).next;
+                    let dec = crate::container_of!(curr, Decoration, link);
+                    (*dec).make_inert();
+                    curr = next;
+                }
+            }
+
+            let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+            let mut curr = (*seats).next;
+            while curr != seats {
+                let next = (*curr).next;
+                let seat = crate::container_of!(curr, crate::seat::Seat, link);
+                if let crate::seat::Focus::Window(w) = (*seat).focused {
+                    if w == self as *mut Window {
+                        (*seat).focus(crate::seat::Focus::None);
+                    }
+                }
+                curr = next;
+            }
+        }
+    }
+
+    pub unsafe fn manage_finish(&mut self) -> bool {
+        if matches!(self.impl_type, WindowImpl::Destroying) {
+            assert_eq!(self.state, WindowState::Closing);
+            return false;
+        }
+
+        match self.state {
+            WindowState::Init => unreachable!(),
+            WindowState::Ready => {
+                if self.wm_requested.dimensions.is_none() && self.wm_requested.fullscreen.is_null() {
+                    return false;
+                }
+                self.state = WindowState::Initialized;
+            }
+            WindowState::Initialized | WindowState::Mapped => {}
+            WindowState::Closing => return false,
+        }
+
+        if self.wm_requested.close {
+            self.close();
+            self.wm_requested.close = false;
+        }
+
+        let mut activated = false;
+        let seats = &mut (*self.server).wm.sent.seats as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*seats).next;
+        while curr != seats {
+            let next = (*curr).next;
+            let seat = crate::container_of!(curr, crate::seat::Seat, link_sent);
+            if let crate::seat::Focus::Window(w) = (*seat).focused {
+                if w == self as *mut Window {
+                    activated = true;
+                    break;
+                }
+            }
+            curr = next;
+        }
+
+        if !self.wlr_toplevel_handle.is_null() {
+            ffi::wlr_foreign_toplevel_handle_v1_set_activated(self.wlr_toplevel_handle, activated);
+        }
+
+        let (width, height) = if !self.wm_requested.fullscreen.is_null() {
+            let output = self.wm_requested.fullscreen;
+            let (w, h) = (*output).sent.dimensions();
+            if self.configure_sent.width != Some(w as u32) || self.configure_sent.height != Some(h as u32) {
+                self.configure_scheduled.width = Some(w as u32);
+                self.configure_scheduled.height = Some(h as u32);
+                self.rendering_scheduled.resend_dimensions = true;
+                (Some(w as u32), Some(h as u32))
+            } else {
+                (None, None)
+            }
+        } else if let Some(dimensions) = self.wm_requested.dimensions {
+            self.rendering_scheduled.resend_dimensions = true;
+            (Some(dimensions.width), Some(dimensions.height))
+        } else {
+            (None, None)
+        };
+        self.wm_requested.dimensions = None;
+
+        self.configure_scheduled = Configure {
+            width,
+            height,
+            bounds: self.wm_requested.bounds,
+            activated,
+            ssd: self.wm_requested.ssd,
+            tiled: self.wm_requested.tiled,
+            capabilities: self.wm_requested.capabilities,
+            maximized: self.wm_requested.maximized,
+            inform_fullscreen: self.wm_requested.inform_fullscreen,
+            resizing: self.wm_requested.resizing,
+        };
+
+        let track_configure = match self.impl_type {
+            WindowImpl::Toplevel(toplevel) => {
+                if toplevel.is_null() {
+                    false
+                } else {
+                    (*toplevel).configure()
+                }
+            }
+            WindowImpl::Xwayland(xwindow) => {
+                if xwindow.is_null() {
+                    false
+                } else {
+                    (*xwindow).configure()
+                }
+            }
+            WindowImpl::Destroying => unreachable!(),
+        };
+
+        if track_configure && matches!(self.state, WindowState::Mapped) {
+            self.surfaces.save();
+            self.send_frame_done();
+        }
+
+        track_configure
+    }
+
+    pub unsafe fn render_start(&mut self) {
+        match self.impl_type {
+            WindowImpl::Toplevel(toplevel) => {
+                if !toplevel.is_null() {
+                    match (*toplevel).configure_state {
+                        ConfigureState::Inflight(serial) => {
+                            (*toplevel).configure_state = ConfigureState::TimedOut(serial);
+                        }
+                        ConfigureState::Acked => {
+                            (*toplevel).configure_state = ConfigureState::TimedOutAcked;
+                        }
+                        ConfigureState::Committed => {
+                            (*toplevel).configure_state = ConfigureState::Idle;
+                        }
+                        _ => {}
+                    }
+                    self.rendering_scheduled.width = (*toplevel).geometry.width as u32;
+                    self.rendering_scheduled.height = (*toplevel).geometry.height as u32;
+                }
+            }
+            WindowImpl::Xwayland(xwindow) => {
+                if !xwindow.is_null() {
+                    self.rendering_scheduled.width = (*(*xwindow).xsurface).width as u32;
+                    self.rendering_scheduled.height = (*(*xwindow).xsurface).height as u32;
+                }
+            }
+            WindowImpl::Destroying => {}
+        }
+
+        let presentation_hint = self.presentation_hint();
+        let sent = &mut self.rendering_sent;
+        let scheduled = &mut self.rendering_scheduled;
+
+        if matches!(self.state, WindowState::Mapped) &&
+           (scheduled.resend_dimensions ||
+            scheduled.width != sent.width || scheduled.height != sent.height) {
+            if !self.object.is_null() {
+                ffi::wl_resource_post_event(self.object, 0, scheduled.width as i32, scheduled.height as i32); // sendDimensions
+                scheduled.resend_dimensions = false;
+            }
+        }
+        sent.width = scheduled.width;
+        sent.height = scheduled.height;
+        if sent.presentation_hint != presentation_hint {
+            if !self.object.is_null() {
+                let version = ffi::wl_resource_get_version(self.object);
+                if version >= 4 {
+                    ffi::wl_resource_post_event(self.object, 17, presentation_hint); // sendPresentationHint
+                }
+            }
+            sent.presentation_hint = presentation_hint;
+        }
+    }
+
+    pub unsafe fn presentation_hint(&self) -> ffi::river_output_v1_presentation_mode {
+        let root = self.root_surface();
+        if root.is_null() {
+            return ffi::river_output_v1_presentation_mode_RIVER_OUTPUT_V1_PRESENTATION_MODE_VSYNC;
+        }
+        
+        // tearing control check stub:
+        // switch (server.tearing_control_manager.hintFromSurface(root)) {
+        //     .async => .async,
+        //     .vsync => .vsync,
+        // }
+        // For now, return VSYNC by default.
+        ffi::river_output_v1_presentation_mode_RIVER_OUTPUT_V1_PRESENTATION_MODE_VSYNC
+    }
+
+    pub unsafe fn notify_title(&mut self) {
+        self.wm_scheduled.dirty_title = true;
+        (*self.server).wm.dirty_windowing();
+
+        if !self.foreign_toplevel_handle.is_null() {
+            let title = self.get_title();
+            let app_id = self.get_app_id();
+            let state = ffi::wlr_ext_foreign_toplevel_handle_v1_state {
+                title,
+                app_id,
+            };
+            ffi::wlr_ext_foreign_toplevel_handle_v1_update_state(self.foreign_toplevel_handle, &state);
+        }
+
+        if !self.wlr_toplevel_handle.is_null() {
+            let title = self.get_title();
+            if !title.is_null() {
+                ffi::wlr_foreign_toplevel_handle_v1_set_title(self.wlr_toplevel_handle, title);
+            }
+        }
+    }
+
+    pub unsafe fn notify_app_id(&mut self) {
+        self.wm_scheduled.dirty_app_id = true;
+        (*self.server).wm.dirty_windowing();
+
+        if !self.foreign_toplevel_handle.is_null() {
+            let title = self.get_title();
+            let app_id = self.get_app_id();
+            let state = ffi::wlr_ext_foreign_toplevel_handle_v1_state {
+                title,
+                app_id,
+            };
+            ffi::wlr_ext_foreign_toplevel_handle_v1_update_state(self.foreign_toplevel_handle, &state);
+        }
+
+        if !self.wlr_toplevel_handle.is_null() {
+            let app_id = self.get_app_id();
+            if !app_id.is_null() {
+                ffi::wlr_foreign_toplevel_handle_v1_set_app_id(self.wlr_toplevel_handle, app_id);
+            }
+        }
+    }
+
+    pub unsafe fn render_finish(&mut self) {
+        let requested = &self.rendering_requested;
+        let enabled = !requested.hidden && (matches!(self.state, WindowState::Mapped) || matches!(self.state, WindowState::Closing));
+
+        ffi::wlr_scene_node_set_enabled(self.tree as *mut ffi::wlr_scene_node, enabled);
+        ffi::wlr_scene_node_set_enabled(self.popup_tree as *mut ffi::wlr_scene_node, enabled);
+
+        self.box_geom.width = self.rendering_sent.width as i32;
+        self.box_geom.height = self.rendering_sent.height as i32;
+
+        let mut clip = requested.clip;
+        let mut content_clip = requested.content_clip;
+
+        if !self.wm_requested.fullscreen.is_null() {
+            let output = self.wm_requested.fullscreen;
+            self.box_geom.x = (*output).sent.x;
+            self.box_geom.y = (*output).sent.y;
+            ffi::wlr_scene_node_set_enabled(self.fullscreen_background as *mut ffi::wlr_scene_node, true);
+            let (width, height) = (*output).sent.dimensions();
+            ffi::wlr_scene_rect_set_size(self.fullscreen_background, width as i32, height as i32);
+            clip = ffi::wlr_box { x: 0, y: 0, width: width as i32, height: height as i32 };
+            content_clip = ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 };
+
+            ffi::wlr_scene_node_set_enabled(self.border.left as *mut ffi::wlr_scene_node, false);
+            ffi::wlr_scene_node_set_enabled(self.border.right as *mut ffi::wlr_scene_node, false);
+            ffi::wlr_scene_node_set_enabled(self.border.top as *mut ffi::wlr_scene_node, false);
+            ffi::wlr_scene_node_set_enabled(self.border.bottom as *mut ffi::wlr_scene_node, false);
+        } else {
+            self.box_geom.x = requested.x;
+            self.box_geom.y = requested.y;
+            ffi::wlr_scene_node_set_enabled(self.fullscreen_background as *mut ffi::wlr_scene_node, false);
+            self.draw_borders();
+        }
+
+        ffi::wlr_scene_node_set_position(self.tree as *mut ffi::wlr_scene_node, self.box_geom.x, self.box_geom.y);
+        ffi::wlr_scene_node_set_position(self.popup_tree as *mut ffi::wlr_scene_node, self.box_geom.x, self.box_geom.y);
+
+        self.apply_surface_clip(&clip, &content_clip);
+
+        for decorations in [&mut self.decorations_above as *mut ffi::wl_list, &mut self.decorations_below as *mut ffi::wl_list] {
+            let list_head = decorations as *mut WlList;
+            let mut curr = (*list_head).next;
+            while curr != list_head {
+                let next = (*curr).next;
+                let dec = crate::container_of!(curr, Decoration, link);
+                (*dec).render_finish(&clip);
+                curr = next;
+            }
+        }
+
+        match self.impl_type {
+            WindowImpl::Xwayland(xwindow) => {
+                if !xwindow.is_null() {
+                    (*xwindow).configure();
+                }
+            }
+            _ => {}
+        }
+    }
+
+    pub unsafe fn draw_borders(&mut self) {
+        let requested = &self.rendering_requested;
+        let content = ffi::wlr_box {
+            x: 0,
+            y: 0,
+            width: self.box_geom.width,
+            height: self.box_geom.height,
+        };
+
+        let mut intersect = std::mem::zeroed();
+        let clip_empty = requested.content_clip.width == 0 && requested.content_clip.height == 0;
+        if clip_empty || ffi::wlr_box_intersection(&mut intersect, &content, &requested.content_clip) {
+            let border = &requested.border;
+            let color: [f32; 4] = [
+                (border.r as f64 / u32::MAX as f64) as f32,
+                (border.g as f64 / u32::MAX as f64) as f32,
+                (border.b as f64 / u32::MAX as f64) as f32,
+                (border.a as f64 / u32::MAX as f64) as f32,
+            ];
+
+            let mut left = ffi::wlr_box {
+                x: -(border.width as i32),
+                y: 0,
+                width: border.width as i32,
+                height: content.height,
+            };
+            let mut right = ffi::wlr_box {
+                x: content.width,
+                y: 0,
+                width: border.width as i32,
+                height: content.height,
+            };
+            let mut top = ffi::wlr_box {
+                x: 0,
+                y: -(border.width as i32),
+                width: content.width,
+                height: border.width as i32,
+            };
+            let mut bottom = ffi::wlr_box {
+                x: 0,
+                y: content.height,
+                width: content.width,
+                height: border.width as i32,
+            };
+
+            if border.edges.top {
+                left.y -= border.width as i32;
+                left.height += border.width as i32;
+                right.y -= border.width as i32;
+                right.height += border.width as i32;
+            }
+            if border.edges.bottom {
+                left.height += border.width as i32;
+                right.height += border.width as i32;
+            }
+
+            let mut edges = [
+                ("left", &mut left, self.border.left, border.edges.left),
+                ("right", &mut right, self.border.right, border.edges.right),
+                ("top", &mut top, self.border.top, border.edges.top),
+                ("bottom", &mut bottom, self.border.bottom, border.edges.bottom),
+            ];
+
+            for (_, edge_box, rect, enabled) in &mut edges {
+                if requested.clip.width != 0 || requested.clip.height != 0 {
+                    let mut clip_intersect = std::mem::zeroed();
+                    ffi::wlr_box_intersection(&mut clip_intersect, *edge_box, &requested.clip);
+                    **edge_box = clip_intersect;
+                }
+                ffi::wlr_scene_node_set_enabled(*rect as *mut ffi::wlr_scene_node, *enabled);
+                ffi::wlr_scene_node_set_position(*rect as *mut ffi::wlr_scene_node, (*edge_box).x, (*edge_box).y);
+                ffi::wlr_scene_rect_set_size(*rect, (*edge_box).width, (*edge_box).height);
+                ffi::wlr_scene_rect_set_color(*rect, color.as_ptr());
+            }
+        }
+    }
+
+    pub unsafe fn apply_surface_clip(&mut self, a: *const ffi::wlr_box, b: *const ffi::wlr_box) {
+        let mut surface_clip = std::mem::zeroed::<ffi::wlr_box>();
+        let a_empty = (*a).width == 0 && (*a).height == 0;
+        let b_empty = (*b).width == 0 && (*b).height == 0;
+
+        if !a_empty && !b_empty {
+            if !ffi::wlr_box_intersection(&mut surface_clip, a, b) {
+                self.surfaces.set_enabled(false);
+                return;
+            }
+        } else if !a_empty {
+            surface_clip = *a;
+        } else {
+            surface_clip = *b;
+        }
+
+        self.surfaces.set_enabled(true);
+        match self.impl_type {
+            WindowImpl::Toplevel(toplevel) => {
+                if !toplevel.is_null() {
+                    surface_clip.x += (*toplevel).geometry.x;
+                    surface_clip.y += (*toplevel).geometry.y;
+                }
+            }
+            _ => {}
+        }
+
+        let children_head = ffi::river_scene_tree_get_children(self.surfaces.tree) as *mut WlList;
+        if (*children_head).next != children_head {
+            ffi::wlr_scene_subsurface_tree_set_clip(self.surfaces.tree as *mut ffi::wlr_scene_node, &surface_clip);
+        }
+    }
+}
+
+unsafe fn clock_gettime(clk_id: libc::clockid_t, tp: &mut libc::timespec) -> libc::c_int {
+    libc::clock_gettime(clk_id, tp)
+}
+
+unsafe extern "C" fn window_destroy(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn window_close(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    (*window).wm_requested.close = true;
+}
+
+unsafe extern "C" fn window_get_node(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    id: u32,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    if !(*window).node.object.is_null() {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_window_v1_error_RIVER_WINDOW_V1_ERROR_NODE_EXISTS,
+            b"window already has a node object\0".as_ptr() as *const _,
+        );
+        return;
+    }
+    (*window).node.create_object(client, ffi::wl_resource_get_version(resource) as u32, id);
+}
+
+unsafe extern "C" fn window_propose_dimensions(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    width: i32,
+    height: i32,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    if width < 0 || height < 0 {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_window_v1_error_RIVER_WINDOW_V1_ERROR_INVALID_DIMENSIONS,
+            b"dimensions must be greater than or equal to 0\0".as_ptr() as *const _,
+        );
+        return;
+    }
+    (*window).wm_requested.dimensions = Some(Dimensions {
+        width: width as u32,
+        height: height as u32,
+    });
+}
+
+unsafe extern "C" fn window_hide(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_rendering() {
+        return;
+    }
+    (*window).rendering_requested.hidden = true;
+}
+
+unsafe extern "C" fn window_show(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_rendering() {
+        return;
+    }
+    (*window).rendering_requested.hidden = false;
+}
+
+unsafe extern "C" fn window_use_csd(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    (*window).wm_requested.ssd = false;
+}
+
+unsafe extern "C" fn window_use_ssd(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    (*window).wm_requested.ssd = true;
+}
+
+unsafe extern "C" fn window_set_borders(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    edges: u32,
+    width: i32,
+    r: u32,
+    g: u32,
+    b: u32,
+    a: u32,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_rendering() {
+        return;
+    }
+    if width < 0 {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_window_v1_error_RIVER_WINDOW_V1_ERROR_INVALID_BORDER,
+            b"border width must be greater than or equal to 0\0".as_ptr() as *const _,
+        );
+        return;
+    }
+    (*window).rendering_requested.border = Border {
+        edges: Edges::from_u32(edges),
+        width: width as u32,
+        r,
+        g,
+        b,
+        a,
+    };
+}
+
+unsafe extern "C" fn window_set_tiled(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    edges: u32,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    (*window).wm_requested.tiled = edges;
+}
+
+unsafe extern "C" fn window_get_decoration_above(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    id: u32,
+    wl_surface: *mut ffi::wl_resource,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let wlr_surface = ffi::wlr_surface_from_resource(wl_surface);
+    let decoration = match Decoration::create(
+        client,
+        ffi::wl_resource_get_version(resource) as u32,
+        id,
+        wlr_surface,
+        (*window).decorations_above_tree,
+        window,
+    ) {
+        Ok(d) => d,
+        Err(e) => {
+            log::error!("Failed to create decoration: {}", e);
+            ffi::wl_client_post_no_memory(client);
+            return;
+        }
+    };
+    let list_head = &mut (*window).decorations_above as *mut ffi::wl_list as *mut WlList;
+    wl_list_insert((*list_head).prev, &mut (*decoration).link as *mut ffi::wl_list as *mut WlList);
+}
+
+unsafe extern "C" fn window_get_decoration_below(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    id: u32,
+    wl_surface: *mut ffi::wl_resource,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let wlr_surface = ffi::wlr_surface_from_resource(wl_surface);
+    let decoration = match Decoration::create(
+        client,
+        ffi::wl_resource_get_version(resource) as u32,
+        id,
+        wlr_surface,
+        (*window).decorations_below_tree,
+        window,
+    ) {
+        Ok(d) => d,
+        Err(e) => {
+            log::error!("Failed to create decoration: {}", e);
+            ffi::wl_client_post_no_memory(client);
+            return;
+        }
+    };
+    let list_head = &mut (*window).decorations_below as *mut ffi::wl_list as *mut WlList;
+    wl_list_insert((*list_head).prev, &mut (*decoration).link as *mut ffi::wl_list as *mut WlList);
+}
+
+unsafe extern "C" fn window_inform_resize_start(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    (*window).wm_requested.resizing = true;
+}
+
+unsafe extern "C" fn window_inform_resize_end(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    (*window).wm_requested.resizing = false;
+}
+
+unsafe extern "C" fn window_set_capabilities(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    caps: u32,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    (*window).wm_requested.capabilities = caps;
+}
+
+unsafe extern "C" fn window_inform_maximized(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    (*window).wm_requested.maximized = true;
+}
+
+unsafe extern "C" fn window_inform_unmaximized(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    (*window).wm_requested.maximized = false;
+}
+
+unsafe extern "C" fn window_inform_fullscreen(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    (*window).wm_requested.inform_fullscreen = true;
+}
+
+unsafe extern "C" fn window_inform_not_fullscreen(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    (*window).wm_requested.inform_fullscreen = false;
+}
+
+unsafe extern "C" fn window_fullscreen(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    output: *mut ffi::wl_resource,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    let out = if output.is_null() {
+        std::ptr::null_mut()
+    } else {
+        let wlr_output = ffi::wlr_output_from_resource(output);
+        if wlr_output.is_null() {
+            std::ptr::null_mut()
+        } else {
+            ffi::river_wlr_output_get_data(wlr_output) as *mut crate::output::Output
+        }
+    };
+    (*window).wm_requested.fullscreen = out;
+}
+
+unsafe extern "C" fn window_exit_fullscreen(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    (*window).wm_requested.fullscreen = std::ptr::null_mut();
+}
+
+unsafe extern "C" fn window_set_clip_box(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    x: i32,
+    y: i32,
+    width: i32,
+    height: i32,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_rendering() {
+        return;
+    }
+    if width < 0 || height < 0 {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_window_v1_error_RIVER_WINDOW_V1_ERROR_INVALID_CLIP_BOX,
+            b"width/height must be greater than or equal to 0\0".as_ptr() as *const _,
+        );
+        return;
+    }
+    (*window).rendering_requested.clip = ffi::wlr_box {
+        x,
+        y,
+        width,
+        height,
+    };
+}
+
+unsafe extern "C" fn window_set_content_clip_box(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    x: i32,
+    y: i32,
+    width: i32,
+    height: i32,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_rendering() {
+        return;
+    }
+    if width < 0 || height < 0 {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_window_v1_error_RIVER_WINDOW_V1_ERROR_INVALID_CLIP_BOX,
+            b"width/height must be greater than or equal to 0\0".as_ptr() as *const _,
+        );
+        return;
+    }
+    (*window).rendering_requested.content_clip = ffi::wlr_box {
+        x,
+        y,
+        width,
+        height,
+    };
+}
+
+unsafe extern "C" fn window_set_dimension_bounds(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    max_width: i32,
+    max_height: i32,
+) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if window.is_null() {
+        return;
+    }
+    let server = (*window).server;
+    if !(*server).wm.ensure_windowing() {
+        return;
+    }
+    if max_width < 0 || max_height < 0 {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_window_v1_error_RIVER_WINDOW_V1_ERROR_INVALID_DIMENSIONS,
+            b"dimensions must be greater than or equal to 0\0".as_ptr() as *const _,
+        );
+        return;
+    }
+    (*window).wm_requested.bounds = Dimensions {
+        width: max_width as u32,
+        height: max_height as u32,
+    };
+}
+
+// river_window_v1 implementation
+static WINDOW_INTERFACE: ffi::river_window_v1_interface = ffi::river_window_v1_interface {
+    destroy: Some(window_destroy),
+    close: Some(window_close),
+    get_node: Some(window_get_node),
+    propose_dimensions: Some(window_propose_dimensions),
+    hide: Some(window_hide),
+    show: Some(window_show),
+    use_csd: Some(window_use_csd),
+    use_ssd: Some(window_use_ssd),
+    set_borders: Some(window_set_borders),
+    set_tiled: Some(window_set_tiled),
+    get_decoration_above: Some(window_get_decoration_above),
+    get_decoration_below: Some(window_get_decoration_below),
+    inform_resize_start: Some(window_inform_resize_start),
+    inform_resize_end: Some(window_inform_resize_end),
+    set_capabilities: Some(window_set_capabilities),
+    inform_maximized: Some(window_inform_maximized),
+    inform_unmaximized: Some(window_inform_unmaximized),
+    inform_fullscreen: Some(window_inform_fullscreen),
+    inform_not_fullscreen: Some(window_inform_not_fullscreen),
+    fullscreen: Some(window_fullscreen),
+    exit_fullscreen: Some(window_exit_fullscreen),
+    set_clip_box: Some(window_set_clip_box),
+    set_content_clip_box: Some(window_set_content_clip_box),
+    set_dimension_bounds: Some(window_set_dimension_bounds),
+};
+
+static INERT_WINDOW_INTERFACE: ffi::river_window_v1_interface = ffi::river_window_v1_interface {
+    destroy: Some(window_destroy),
+    close: None,
+    get_node: None,
+    propose_dimensions: None,
+    hide: None,
+    show: None,
+    use_csd: None,
+    use_ssd: None,
+    set_borders: None,
+    set_tiled: None,
+    get_decoration_above: None,
+    get_decoration_below: None,
+    inform_resize_start: None,
+    inform_resize_end: None,
+    set_capabilities: None,
+    inform_maximized: None,
+    inform_unmaximized: None,
+    inform_fullscreen: None,
+    inform_not_fullscreen: None,
+    fullscreen: None,
+    exit_fullscreen: None,
+    set_clip_box: None,
+    set_content_clip_box: None,
+    set_dimension_bounds: None,
+};
+
+unsafe extern "C" fn handle_destroy_resource(resource: *mut ffi::wl_resource) {
+    let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+    if !window.is_null() {
+        (*window).object = std::ptr::null_mut();
+        (*window).node.make_inert();
+        
+        for decorations in [&mut (*window).decorations_above as *mut ffi::wl_list, &mut (*window).decorations_below as *mut ffi::wl_list] {
+            let list_head = decorations as *mut WlList;
+            let mut curr = (*list_head).next;
+            while curr != list_head {
+                let next = (*curr).next;
+                let dec = crate::container_of!(curr, Decoration, link);
+                (*dec).make_inert();
+                curr = next;
+            }
+        }
+    }
+}
+
+// river_decoration_v1 implementation
+pub struct DecorationRenderingRequested {
+    pub offset_x: i32,
+    pub offset_y: i32,
+    pub sync_next_commit: bool,
+}
+
+pub struct Decoration {
+    pub object: *mut ffi::wl_resource, // river_decoration_v1
+    pub surface: *mut ffi::wlr_surface,
+    pub tree: *mut ffi::wlr_scene_tree,
+    pub surfaces: crate::scene::SaveableSurfaces,
+    pub link: ffi::wl_list,
+    pub window: *mut Window,
+    pub rendering_requested: DecorationRenderingRequested,
+}
+
+impl Decoration {
+    pub unsafe fn create(
+        client: *mut ffi::wl_client,
+        version: u32,
+        id: u32,
+        surface: *mut ffi::wlr_surface,
+        parent: *mut ffi::wlr_scene_tree,
+        window: *mut Window,
+    ) -> Result<*mut Self, &'static str> {
+        let decoration_v1 = ffi::wl_resource_create(client, &ffi::river_decoration_v1_interface, version as i32, id);
+        if decoration_v1.is_null() {
+            ffi::wl_client_post_no_memory(client);
+            return Err("wl_resource_create failed");
+        }
+
+        if !ffi::wlr_surface_set_role(
+            surface,
+            &DECORATION_ROLE,
+            decoration_v1,
+            ffi::river_window_manager_v1_error_RIVER_WINDOW_MANAGER_V1_ERROR_ROLE,
+        ) {
+            return Err("wlr_surface_set_role failed");
+        }
+        ffi::river_wlr_surface_set_role_object(surface, decoration_v1);
+
+        let tree = ffi::wlr_scene_tree_create(parent);
+        if tree.is_null() {
+            return Err("wlr_scene_tree_create failed");
+        }
+
+        let surfaces = crate::scene::SaveableSurfaces::init(tree)?;
+        let subsurface_tree = ffi::wlr_scene_subsurface_tree_create(surfaces.tree, surface);
+        if subsurface_tree.is_null() {
+            ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
+            return Err("wlr_scene_subsurface_tree_create failed");
+        }
+
+        let dec = Box::new(Decoration {
+            object: decoration_v1,
+            surface,
+            tree,
+            surfaces,
+            link: std::mem::zeroed(),
+            window,
+            rendering_requested: DecorationRenderingRequested {
+                offset_x: 0,
+                offset_y: 0,
+                sync_next_commit: false,
+            },
+        });
+        let raw = Box::into_raw(dec);
+
+        ffi::wl_resource_set_implementation(
+            decoration_v1,
+            &DECORATION_INTERFACE as *const _ as *const _,
+            raw as *mut _,
+            Some(handle_dec_destroy_resource),
+        );
+
+        Ok(raw)
+    }
+
+    pub unsafe fn destroy(&mut self) {
+        assert!(self.object.is_null());
+        ffi::wlr_scene_node_destroy(self.tree as *mut ffi::wlr_scene_node);
+        wl_list_remove(&mut self.link as *mut ffi::wl_list as *mut WlList);
+        let _ = Box::from_raw(self);
+    }
+
+    pub unsafe fn make_inert(&mut self) {
+        if !self.object.is_null() {
+            ffi::wl_resource_set_implementation(
+                self.object,
+                &INERT_DECORATION_INTERFACE as *const _ as *const _,
+                std::ptr::null_mut(),
+                None,
+            );
+            self.object = std::ptr::null_mut();
+        }
+        self.surfaces.save();
+    }
+
+    pub unsafe fn render_finish(&mut self, window_clip: *const ffi::wlr_box) {
+        if self.rendering_requested.sync_next_commit {
+            self.rendering_requested.sync_next_commit = false;
+
+            if !self.surfaces.saved {
+                if !self.object.is_null() {
+                    ffi::wl_resource_post_error(
+                        self.object,
+                        ffi::river_decoration_v1_error_RIVER_DECORATION_V1_ERROR_NO_COMMIT,
+                        b"no wl_surface.commit after sync_next_commit and before update_rendering_finish\0".as_ptr() as *const _,
+                    );
+                }
+            }
+        }
+
+        self.surfaces.drop_saved();
+
+        ffi::wlr_scene_node_set_position(self.tree as *mut ffi::wlr_scene_node, self.rendering_requested.offset_x, self.rendering_requested.offset_y);
+
+        let children_head = ffi::river_scene_tree_get_children(self.surfaces.tree) as *mut WlList;
+        if (*children_head).next != children_head {
+            let mut clip = *window_clip;
+            clip.x -= self.rendering_requested.offset_x;
+            clip.y -= self.rendering_requested.offset_y;
+            ffi::wlr_scene_subsurface_tree_set_clip(self.surfaces.tree as *mut ffi::wlr_scene_node, &clip);
+        }
+    }
+}
+
+pub unsafe fn decoration_from_wlr_surface(surface: *mut ffi::wlr_surface) -> *mut Decoration {
+    if surface.is_null() {
+        return std::ptr::null_mut();
+    }
+    let role_ptr = ffi::river_wlr_surface_get_role(surface);
+    if role_ptr != &DECORATION_ROLE as *const _ {
+        return std::ptr::null_mut();
+    }
+    let resource = ffi::river_wlr_surface_get_role_resource(surface);
+    if resource.is_null() {
+        return std::ptr::null_mut();
+    }
+    ffi::wl_resource_get_user_data(resource) as *mut Decoration
+}
+
+unsafe extern "C" fn dec_client_commit(surface: *mut ffi::wlr_surface) {
+    let dec = decoration_from_wlr_surface(surface);
+    if dec.is_null() {
+        return;
+    }
+    if (*dec).rendering_requested.sync_next_commit {
+        (*dec).surfaces.save();
+    }
+}
+
+unsafe extern "C" fn dec_commit(surface: *mut ffi::wlr_surface) {
+    if ffi::wlr_surface_has_buffer(surface) {
+        ffi::wlr_surface_map(surface);
+    }
+}
+
+unsafe extern "C" fn dec_destroy(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn dec_set_offset(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    x: i32,
+    y: i32,
+) {
+    let dec = ffi::wl_resource_get_user_data(resource) as *mut Decoration;
+    if dec.is_null() {
+        return;
+    }
+    let server = (*(*dec).window).server;
+    if !(*server).wm.ensure_rendering() {
+        return;
+    }
+    (*dec).rendering_requested.offset_x = x;
+    (*dec).rendering_requested.offset_y = y;
+}
+
+unsafe extern "C" fn dec_sync_next_commit(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let dec = ffi::wl_resource_get_user_data(resource) as *mut Decoration;
+    if dec.is_null() {
+        return;
+    }
+    let server = (*(*dec).window).server;
+    if !(*server).wm.ensure_rendering() {
+        return;
+    }
+    (*dec).rendering_requested.sync_next_commit = true;
+}
+
+static DECORATION_INTERFACE: ffi::river_decoration_v1_interface = ffi::river_decoration_v1_interface {
+    destroy: Some(dec_destroy),
+    set_offset: Some(dec_set_offset),
+    sync_next_commit: Some(dec_sync_next_commit),
+};
+
+static INERT_DECORATION_INTERFACE: ffi::river_decoration_v1_interface = ffi::river_decoration_v1_interface {
+    destroy: Some(dec_destroy),
+    set_offset: None,
+    sync_next_commit: None,
+};
+
+unsafe extern "C" fn handle_dec_destroy_resource(resource: *mut ffi::wl_resource) {
+    let dec = ffi::wl_resource_get_user_data(resource) as *mut Decoration;
+    if !dec.is_null() {
+        (*dec).object = std::ptr::null_mut();
+        (*dec).destroy();
+    }
+}
+
+#[no_mangle]
+pub static mut DECORATION_ROLE: ffi::wlr_surface_role = ffi::wlr_surface_role {
+    name: b"river_decoration_v1\0".as_ptr() as *const _,
+    no_object: false,
+    client_commit: Some(dec_client_commit),
+    commit: Some(dec_commit),
+    map: None,
+    unmap: None,
+    destroy: None,
+};
diff --git a/src/window_manager.rs b/src/window_manager.rs
new file mode 100644
index 0000000..1d08bbc
--- /dev/null
+++ b/src/window_manager.rs
@@ -0,0 +1,727 @@
+// SPDX-FileCopyrightText: © 2024 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::server::{Server, WlListener, WlList, wl_list_insert, wl_list_remove, wl_listener_remove};
+use crate::slotmap::{SlotMap, Key};
+use std::hash::{Hash, Hasher};
+
+pub use crate::window::Window;
+pub use crate::shell_surface::ShellSurface;
+
+pub use crate::xwayland_override_redirect::XwaylandOverrideRedirect;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum WindowManagerState {
+    Idle,
+    Manage,
+    InflightConfigures(u32),
+    Render,
+}
+
+pub struct WindowManagerScheduled {
+    pub dirty: bool,
+    pub dirty_lazy: bool,
+    pub output_config: *mut ffi::wlr_output_configuration_v1,
+}
+
+pub struct WindowManagerSent {
+    pub session_locked: bool,
+    pub outputs: ffi::wl_list,
+    pub output_config: *mut ffi::wlr_output_configuration_v1,
+    pub seats: ffi::wl_list,
+}
+
+pub struct WindowManagerRenderingScheduled {
+    pub dirty: bool,
+}
+
+pub struct WindowManagerRenderingRequested {
+    pub list: ffi::wl_list,
+    pub order_hash: u64,
+}
+
+pub struct WindowManager {
+    pub server: *mut Server,
+    pub global: *mut ffi::wl_global,
+    pub server_destroy: ffi::wl_listener,
+    pub object: *mut ffi::wl_resource,
+    pub state: WindowManagerState,
+    pub windows: SlotMap<*mut Window>,
+    pub scheduled: WindowManagerScheduled,
+    pub sent: WindowManagerSent,
+    pub rendering_scheduled: WindowManagerRenderingScheduled,
+    pub rendering_requested: WindowManagerRenderingRequested,
+    pub dirty_idle: *mut ffi::wl_event_source,
+    pub timeout: *mut ffi::wl_event_source,
+}
+
+impl WindowManager {
+    pub unsafe fn init(&mut self) -> Result<(), ()> {
+        // This is a stub for the 0-arg struct instantiation.
+        // We will call the real initialization with the server parameter.
+        ffi::wl_list_init(&mut self.sent.outputs);
+        self.scheduled.output_config = std::ptr::null_mut();
+        self.sent.output_config = std::ptr::null_mut();
+        Ok(())
+    }
+
+    pub unsafe fn init_with_server(&mut self, server: *mut Server) -> Result<(), &'static str> {
+        self.server = server;
+        self.global = std::ptr::null_mut();
+        self.object = std::ptr::null_mut();
+        self.state = WindowManagerState::Idle;
+        self.windows = SlotMap::new();
+        self.scheduled = WindowManagerScheduled {
+            dirty: false,
+            dirty_lazy: false,
+            output_config: std::ptr::null_mut(),
+        };
+        self.sent = WindowManagerSent {
+            session_locked: false,
+            outputs: std::mem::zeroed(),
+            output_config: std::ptr::null_mut(),
+            seats: std::mem::zeroed(),
+        };
+        self.rendering_scheduled = WindowManagerRenderingScheduled {
+            dirty: false,
+        };
+        self.rendering_requested = WindowManagerRenderingRequested {
+            list: std::mem::zeroed(),
+            order_hash: 0,
+        };
+        self.dirty_idle = std::ptr::null_mut();
+
+        ffi::wl_list_init(&mut self.sent.outputs);
+        ffi::wl_list_init(&mut self.sent.seats);
+        ffi::wl_list_init(&mut self.rendering_requested.list);
+
+        let event_loop = ffi::wl_display_get_event_loop((*server).wl_server);
+        self.timeout = ffi::wl_event_loop_add_timer(event_loop, Some(handle_timeout), self as *mut WindowManager as *mut _);
+        if self.timeout.is_null() {
+            return Err("Failed to create timer event source");
+        }
+
+        self.global = ffi::wl_global_create(
+            (*server).wl_server,
+            &ffi::river_window_manager_v1_interface,
+            4,
+            self as *mut WindowManager as *mut _,
+            Some(bind),
+        );
+        if self.global.is_null() {
+            ffi::wl_event_source_remove(self.timeout);
+            return Err("Failed to create river_window_manager_v1 global");
+        }
+
+        let server_destroy_ptr = &mut self.server_destroy as *mut ffi::wl_listener as *mut WlListener;
+        (*server_destroy_ptr).notify = Some(handle_server_destroy);
+        ffi::wl_display_add_destroy_listener((*server).wl_server, &mut self.server_destroy);
+
+        Ok(())
+    }
+
+    pub unsafe fn deinit(&mut self) {
+        if !self.global.is_null() {
+            ffi::wl_global_destroy(self.global);
+            self.global = std::ptr::null_mut();
+        }
+        if !self.timeout.is_null() {
+            ffi::wl_event_source_remove(self.timeout);
+            self.timeout = std::ptr::null_mut();
+        }
+        wl_listener_remove(&mut self.server_destroy);
+    }
+
+    pub unsafe fn ensure_windowing(&self) -> bool {
+        match self.state {
+            WindowManagerState::Manage => true,
+            _ => {
+                if !self.object.is_null() {
+                    ffi::wl_resource_post_error(
+                        self.object,
+                        ffi::river_window_manager_v1_error_RIVER_WINDOW_MANAGER_V1_ERROR_SEQUENCE_ORDER,
+                        b"invalid modification of window management state\0".as_ptr() as *const _,
+                    );
+                }
+                false
+            }
+        }
+    }
+
+    pub unsafe fn ensure_rendering(&self) -> bool {
+        match self.state {
+            WindowManagerState::Manage | WindowManagerState::InflightConfigures(_) | WindowManagerState::Render => true,
+            WindowManagerState::Idle => {
+                if !self.object.is_null() {
+                    ffi::wl_resource_post_error(
+                        self.object,
+                        ffi::river_window_manager_v1_error_RIVER_WINDOW_MANAGER_V1_ERROR_SEQUENCE_ORDER,
+                        b"invalid modification of rendering state\0".as_ptr() as *const _,
+                    );
+                }
+                false
+            }
+        }
+    }
+
+    pub unsafe fn dirty_windowing(&mut self) {
+        self.scheduled.dirty = true;
+        self.add_dirty_idle();
+    }
+
+    pub unsafe fn dirty_windowing_lazy(&mut self) {
+        self.scheduled.dirty_lazy = true;
+        self.add_dirty_idle();
+    }
+
+    pub unsafe fn clean_windowing(&mut self) {
+        self.scheduled.dirty = false;
+        self.remove_dirty_idle();
+    }
+
+    pub unsafe fn dirty_rendering(&mut self) {
+        self.rendering_scheduled.dirty = true;
+        self.add_dirty_idle();
+    }
+
+    pub unsafe fn clean_rendering(&mut self) {
+        self.rendering_scheduled.dirty = false;
+        self.remove_dirty_idle();
+    }
+
+    unsafe fn add_dirty_idle(&mut self) {
+        if self.scheduled.dirty || self.scheduled.dirty_lazy || self.rendering_scheduled.dirty {
+            if self.dirty_idle.is_null() {
+                let event_loop = ffi::wl_display_get_event_loop((*self.server).wl_server);
+                self.dirty_idle = ffi::wl_event_loop_add_idle(
+                    event_loop,
+                    Some(dirty_idle_callback),
+                    self as *mut WindowManager as *mut _,
+                );
+            }
+        }
+    }
+
+    unsafe fn remove_dirty_idle(&mut self) {
+        if !self.scheduled.dirty && !self.rendering_scheduled.dirty {
+            if !self.dirty_idle.is_null() {
+                ffi::wl_event_source_remove(self.dirty_idle);
+                self.dirty_idle = std::ptr::null_mut();
+            }
+        }
+    }
+
+    pub unsafe fn manage_start(&mut self) {
+        assert!(matches!(self.state, WindowManagerState::Idle));
+        assert!(self.scheduled.dirty);
+        self.clean_windowing();
+        self.state = WindowManagerState::Manage;
+
+        log::debug!("manage sequence start");
+
+        let session_locked = (*self.server).lock_manager.state == crate::lock_manager::LockState::Locked;
+        if session_locked != self.sent.session_locked {
+            if !self.object.is_null() {
+                if session_locked {
+                    ffi::wl_resource_post_event(self.object, 4); // river_window_manager_v1.session_locked
+                } else {
+                    ffi::wl_resource_post_event(self.object, 5); // river_window_manager_v1.session_unlocked
+                }
+            }
+            self.sent.session_locked = session_locked;
+        }
+
+        (*self.server).om.auto_layout();
+
+        let outputs = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*outputs).next;
+        while curr != outputs {
+            let next = (*curr).next;
+            let output = crate::container_of!(curr, crate::output::Output, link);
+            (*output).layer_shell.manage_start(output);
+            curr = next;
+        }
+
+        assert!(self.sent.output_config.is_null());
+        self.sent.output_config = self.scheduled.output_config;
+        self.scheduled.output_config = std::ptr::null_mut();
+
+        for &win_ptr in self.windows.iter() {
+            (*win_ptr).manage_start();
+        }
+
+        let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*seats).next;
+        while curr != seats {
+            let next = (*curr).next;
+            let seat = crate::container_of!(curr, crate::seat::Seat, link);
+            (*seat).manage_start();
+            curr = next;
+        }
+
+        if !self.object.is_null() {
+            ffi::wl_resource_post_event(self.object, 2); // river_window_manager_v1.manage_start
+            self.start_timeout_timer(3000);
+        } else {
+            self.manage_finish();
+        }
+    }
+
+    pub unsafe fn manage_finish(&mut self) {
+        assert!(matches!(self.state, WindowManagerState::Manage));
+        self.cancel_timeout_timer();
+
+        log::debug!("manage sequence finish");
+
+        let seats = &mut self.sent.seats as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*seats).next;
+        while curr != seats {
+            let next = (*curr).next;
+            let seat = crate::container_of!(curr, crate::seat::Seat, link_sent);
+            (*seat).manage_finish();
+            curr = next;
+        }
+
+        self.state = WindowManagerState::InflightConfigures(0);
+
+        let render_list = &mut self.rendering_requested.list as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*render_list).next;
+        while curr != render_list {
+            let next = (*curr).next;
+            let node = crate::container_of!(curr, crate::wm_node::WmNode, link);
+            match (*node).get() {
+                crate::wm_node::WmNodeType::Window(window) => {
+                    if (*window).manage_finish() {
+                        if let WindowManagerState::InflightConfigures(ref mut count) = self.state {
+                            *count += 1;
+                        }
+                    }
+                }
+                _ => {}
+            }
+            curr = next;
+        }
+
+        if let WindowManagerState::InflightConfigures(count) = self.state {
+            log::debug!("sent {} tracked configure(s)", count);
+            if count > 0 {
+                self.start_timeout_timer(100);
+            } else {
+                self.render_start();
+            }
+        }
+    }
+
+    unsafe fn start_timeout_timer(&mut self, ms: u32) {
+        if !self.timeout.is_null() {
+            ffi::wl_event_source_timer_update(self.timeout, ms as i32);
+        }
+    }
+
+    unsafe fn cancel_timeout_timer(&mut self) {
+        if !self.timeout.is_null() {
+            ffi::wl_event_source_timer_update(self.timeout, 0);
+        }
+    }
+
+    pub unsafe fn notify_configured(&mut self) {
+        if let WindowManagerState::InflightConfigures(ref mut count) = self.state {
+            *count -= 1;
+            if *count == 0 {
+                self.cancel_timeout_timer();
+                self.render_start();
+            }
+        }
+    }
+
+    pub unsafe fn render_start(&mut self) {
+        assert!(matches!(self.state, WindowManagerState::InflightConfigures(0)) ||
+                (matches!(self.state, WindowManagerState::Idle) && self.rendering_scheduled.dirty));
+        self.state = WindowManagerState::Render;
+        self.clean_rendering();
+
+        log::debug!("render sequence start");
+
+        let render_list = &mut self.rendering_requested.list as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*render_list).next;
+        while curr != render_list {
+            let next = (*curr).next;
+            let node = crate::container_of!(curr, crate::wm_node::WmNode, link);
+            match (*node).get() {
+                crate::wm_node::WmNodeType::Window(window) => {
+                    (*window).render_start();
+                }
+                _ => {}
+            }
+            curr = next;
+        }
+
+        if !self.object.is_null() {
+            ffi::wl_resource_post_event(self.object, 3); // river_window_manager_v1.render_start
+            self.start_timeout_timer(3000);
+        } else {
+            self.render_finish();
+        }
+    }
+
+    pub unsafe fn render_finish(&mut self) {
+        assert!(matches!(self.state, WindowManagerState::Render));
+        self.state = WindowManagerState::Idle;
+        self.cancel_timeout_timer();
+
+        log::debug!("render sequence finish");
+
+        for &window in self.windows.iter() {
+            if !matches!((*window).state, crate::window::WindowState::Closing) {
+                (*window).surfaces.drop_saved();
+            }
+            if matches!((*window).state, crate::window::WindowState::Init) {
+                ffi::wlr_scene_node_reparent((*window).tree as *mut ffi::wlr_scene_node, (*self.server).scene.hidden_tree);
+            }
+            if let crate::window::WindowImpl::Destroying = (*window).impl_type {
+                Window::destroy(window);
+            }
+        }
+
+        let mut hasher = std::collections::hash_map::DefaultHasher::new();
+        let render_list = &mut self.rendering_requested.list as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*render_list).next;
+        while curr != render_list {
+            let next = (*curr).next;
+            let node = crate::container_of!(curr, crate::wm_node::WmNode, link);
+            match (*node).get() {
+                crate::wm_node::WmNodeType::Window(window) => {
+                    (*window).ref_key.hash(&mut hasher);
+                    rendered_fullscreen(window).hash(&mut hasher);
+                }
+                crate::wm_node::WmNodeType::ShellSurface(shell_surface) => {
+                    (shell_surface as usize).hash(&mut hasher);
+                }
+            }
+            curr = next;
+        }
+        let new_order_hash = hasher.finish();
+        let reorder = self.rendering_requested.order_hash != new_order_hash;
+        self.rendering_requested.order_hash = new_order_hash;
+
+        let mut found_fullscreen = false;
+        curr = (*render_list).next;
+        while curr != render_list {
+            let next = (*curr).next;
+            let node = crate::container_of!(curr, crate::wm_node::WmNode, link);
+            match (*node).get() {
+                crate::wm_node::WmNodeType::Window(window) => {
+                    (*window).render_finish();
+                    if reorder {
+                        ffi::wlr_scene_node_reparent((*window).popup_tree as *mut _, (*self.server).scene.layers.popups);
+                        if rendered_fullscreen(window) {
+                            ffi::wlr_scene_node_reparent((*window).tree as *mut _, (*self.server).scene.layers.fullscreen);
+                            ffi::wlr_scene_node_raise_to_top((*window).tree as *mut _);
+                            found_fullscreen = true;
+                        } else {
+                            ffi::wlr_scene_node_reparent((*window).tree as *mut _, (*self.server).scene.layers.wm);
+                            ffi::wlr_scene_node_raise_to_top((*window).tree as *mut _);
+                        }
+                    }
+                }
+                crate::wm_node::WmNodeType::ShellSurface(shell_surface) => {
+                    (*shell_surface).render_finish();
+                    if reorder {
+                        ffi::wlr_scene_node_reparent((*shell_surface).popup_tree as *mut _, (*self.server).scene.layers.popups);
+                        if found_fullscreen {
+                            ffi::wlr_scene_node_reparent((*shell_surface).tree as *mut _, (*self.server).scene.layers.fullscreen);
+                        } else {
+                            ffi::wlr_scene_node_reparent((*shell_surface).tree as *mut _, (*self.server).scene.layers.wm);
+                        }
+                        ffi::wlr_scene_node_raise_to_top((*shell_surface).tree as *mut _);
+                    }
+                }
+            }
+            curr = next;
+        }
+
+        (*self.server).om.commit_output_state(self.server);
+
+        let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+        curr = (*seats).next;
+        while curr != seats {
+            let next = (*curr).next;
+            let seat = crate::container_of!(curr, crate::seat::Seat, link);
+            (*seat).cursor.update_hovered();
+            curr = next;
+        }
+
+        (*self.server).idle_inhibit_manager.check_active();
+
+        log::debug!("finished committing transaction");
+
+        if self.scheduled.dirty || self.scheduled.dirty_lazy || self.rendering_scheduled.dirty {
+            self.add_dirty_idle();
+        }
+    }
+}
+
+// Deprecated sent_outputs that is part of structural layout compatibility
+pub struct WindowManagerScheduledCompat {
+    pub output_config: *mut ffi::wlr_output_configuration_v1,
+}
+pub struct WindowManagerSentCompat {
+    pub outputs: ffi::wl_list,
+    pub output_config: *mut ffi::wlr_output_configuration_v1,
+}
+impl WindowManager {
+    // Add legacy fields so structural offsets are preserved if layout-based code is compiled
+    pub fn sent_outputs_compat(&self) {}
+}
+
+unsafe fn rendered_fullscreen(window: *mut Window) -> bool {
+    !(*window).wm_requested.fullscreen.is_null() && !(*window).rendering_requested.hidden
+}
+
+unsafe extern "C" fn dirty_idle_callback(data: *mut std::ffi::c_void) {
+    let wm = data as *mut WindowManager;
+    if wm.is_null() {
+        return;
+    }
+    (*wm).dirty_idle = std::ptr::null_mut();
+    match (*wm).state {
+        WindowManagerState::Idle => {
+            if (*wm).rendering_scheduled.dirty {
+                (*wm).render_start();
+            } else {
+                assert!((*wm).scheduled.dirty || (*wm).scheduled.dirty_lazy);
+                (*wm).scheduled.dirty = true;
+                (*wm).scheduled.dirty_lazy = false;
+                (*wm).manage_start();
+            }
+        }
+        _ => {}
+    }
+}
+
+unsafe extern "C" fn handle_timeout(data: *mut std::ffi::c_void) -> std::os::raw::c_int {
+    let wm = data as *mut WindowManager;
+    if wm.is_null() {
+        return 0;
+    }
+
+    match (*wm).state {
+        WindowManagerState::InflightConfigures(count) => {
+            log::error!("timeout occurred, some imperfect frames may be shown");
+            assert!(count > 0);
+            (*wm).state = WindowManagerState::InflightConfigures(0);
+            (*wm).render_start();
+        }
+        WindowManagerState::Manage | WindowManagerState::Render => {
+            if !(*wm).object.is_null() {
+                log::error!("window manager unresponsive for more than 3 seconds, disconnecting");
+                ffi::wl_resource_post_error(
+                    (*wm).object,
+                    ffi::river_window_manager_v1_error_RIVER_WINDOW_MANAGER_V1_ERROR_UNRESPONSIVE,
+                    b"unresponsive for more than 3 seconds\0".as_ptr() as *const _,
+                );
+                let client = ffi::wl_resource_get_client((*wm).object);
+                ffi::wl_client_destroy(client);
+            }
+        }
+        WindowManagerState::Idle => {}
+    }
+    0
+}
+
+unsafe extern "C" fn handle_server_destroy(
+    listener: *mut ffi::wl_listener,
+    _data: *mut std::ffi::c_void,
+) {
+    let wm = crate::container_of!(listener, WindowManager, server_destroy);
+    (*wm).deinit();
+}
+
+// WM request handlers
+unsafe extern "C" fn wm_stop(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    let wm = ffi::wl_resource_get_user_data(resource) as *mut WindowManager;
+    if !wm.is_null() {
+        (*wm).object = std::ptr::null_mut();
+        ffi::wl_resource_post_event(resource, 1); // river_window_manager_v1.finished (opcode 1)
+        ffi::wl_resource_set_implementation(
+            resource,
+            &INERT_WM_INTERFACE as *const _ as *const _,
+            std::ptr::null_mut(),
+            None,
+        );
+    }
+}
+
+unsafe extern "C" fn wm_destroy(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn wm_manage_finish(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    let wm = ffi::wl_resource_get_user_data(resource) as *mut WindowManager;
+    if wm.is_null() {
+        return;
+    }
+    if !matches!((*wm).state, WindowManagerState::Manage) {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_window_manager_v1_error_RIVER_WINDOW_MANAGER_V1_ERROR_SEQUENCE_ORDER,
+            b"manage_finish request does not match manage_start\0".as_ptr() as *const _,
+        );
+        return;
+    }
+    (*wm).manage_finish();
+}
+
+unsafe extern "C" fn wm_manage_dirty(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    let wm = ffi::wl_resource_get_user_data(resource) as *mut WindowManager;
+    if wm.is_null() {
+        return;
+    }
+    (*wm).scheduled.dirty_lazy = true;
+    (*wm).add_dirty_idle();
+}
+
+unsafe extern "C" fn wm_render_finish(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    let wm = ffi::wl_resource_get_user_data(resource) as *mut WindowManager;
+    if wm.is_null() {
+        return;
+    }
+    if !matches!((*wm).state, WindowManagerState::Render) {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_window_manager_v1_error_RIVER_WINDOW_MANAGER_V1_ERROR_SEQUENCE_ORDER,
+            b"render_finish request does not match render_start\0".as_ptr() as *const _,
+        );
+        return;
+    }
+    (*wm).render_finish();
+}
+
+unsafe extern "C" fn wm_get_shell_surface(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    id: u32,
+    surface_resource: *mut ffi::wl_resource,
+) {
+    let wm = ffi::wl_resource_get_user_data(resource) as *mut WindowManager;
+    if wm.is_null() {
+        return;
+    }
+    let surface = ffi::wlr_surface_from_resource(surface_resource);
+    let version = ffi::wl_resource_get_version(resource) as u32;
+    if let Err(e) = crate::shell_surface::ShellSurface::create(client, version, id, surface, (*wm).server) {
+        log::error!("Failed to create shell surface: {}", e);
+        ffi::wl_client_post_no_memory(client);
+    }
+}
+
+unsafe extern "C" fn wm_exit_session(client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    let wm = ffi::wl_resource_get_user_data(resource) as *mut WindowManager;
+    if wm.is_null() {
+        return;
+    }
+    log::info!("window manager requested to exit session");
+    ffi::wl_display_terminate((*(*wm).server).wl_server);
+}
+
+static WM_INTERFACE: ffi::river_window_manager_v1_interface = ffi::river_window_manager_v1_interface {
+    stop: Some(wm_stop),
+    destroy: Some(wm_destroy),
+    manage_finish: Some(wm_manage_finish),
+    manage_dirty: Some(wm_manage_dirty),
+    render_finish: Some(wm_render_finish),
+    get_shell_surface: Some(wm_get_shell_surface),
+    exit_session: Some(wm_exit_session),
+};
+
+static INERT_WM_INTERFACE: ffi::river_window_manager_v1_interface = ffi::river_window_manager_v1_interface {
+    stop: None,
+    destroy: Some(wm_destroy),
+    manage_finish: None,
+    manage_dirty: None,
+    render_finish: None,
+    get_shell_surface: None,
+    exit_session: None,
+};
+
+unsafe extern "C" fn bind(
+    client: *mut ffi::wl_client,
+    data: *mut std::ffi::c_void,
+    version: u32,
+    id: u32,
+) {
+    let wm = data as *mut WindowManager;
+    if wm.is_null() {
+        return;
+    }
+
+    let resource = ffi::wl_resource_create(client, &ffi::river_window_manager_v1_interface, version as i32, id);
+    if resource.is_null() {
+        ffi::wl_client_post_no_memory(client);
+        log::error!("out of memory binding river_window_manager_v1");
+        return;
+    }
+
+    if !(*wm).object.is_null() {
+        ffi::wl_resource_post_event(resource, 0); // river_window_manager_v1.unavailable
+        ffi::wl_resource_set_implementation(
+            resource,
+            &INERT_WM_INTERFACE as *const _ as *const _,
+            std::ptr::null_mut(),
+            None,
+        );
+        return;
+    }
+
+    (*wm).object = resource;
+    ffi::wl_resource_set_implementation(
+        resource,
+        &WM_INTERFACE as *const _ as *const _,
+        wm as *mut _,
+        Some(handle_destroy_wm_resource),
+    );
+    (*wm).dirty_windowing();
+}
+
+unsafe extern "C" fn handle_destroy_wm_resource(resource: *mut ffi::wl_resource) {
+    let wm = ffi::wl_resource_get_user_data(resource) as *mut WindowManager;
+    if wm.is_null() {
+        return;
+    }
+    log::debug!("active river_window_manager_v1 destroyed");
+    (*wm).object = std::ptr::null_mut();
+
+    let server = (*wm).server;
+
+    // Iterate over outputs and make inert
+    let outputs_list = &mut (*server).om.outputs as *mut ffi::wl_list as *mut WlList;
+    let mut curr = (*outputs_list).next;
+    while curr != outputs_list {
+        let next = (*curr).next;
+        let output = crate::container_of!(curr, crate::output::Output, link);
+        (*output).make_inert();
+        curr = next;
+    }
+
+    // Iterate over seats and make inert
+    let seats_list = &mut (*server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+    curr = (*seats_list).next;
+    while curr != seats_list {
+        let next = (*curr).next;
+        let seat = crate::container_of!(curr, crate::seat::Seat, link);
+        (*seat).make_inert();
+        curr = next;
+    }
+
+    // Iterate over windows and make inert
+    for &window in (*wm).windows.iter() {
+        (*window).make_inert();
+    }
+
+    match (*wm).state {
+        WindowManagerState::Idle | WindowManagerState::InflightConfigures(_) => {}
+        WindowManagerState::Manage => (*wm).manage_finish(),
+        WindowManagerState::Render => (*wm).render_finish(),
+    }
+}
diff --git a/src/wlroots_log_wrapper.c b/src/wlroots_log_wrapper.c
new file mode 100644
index 0000000..0e83f5a
--- /dev/null
+++ b/src/wlroots_log_wrapper.c
@@ -0,0 +1,650 @@
+// SPDX-FileCopyrightText: © 2021 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+#define _POSIX_C_SOURCE 199309L
+#include <assert.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include <wlr/util/log.h>
+
+#define BUFFER_SIZE 1024
+
+void river_wlroots_log_callback(enum wlr_log_importance importance, const char *ptr, size_t len);
+
+static void callback(enum wlr_log_importance importance, const char *fmt, va_list args) {
+	char buffer[BUFFER_SIZE];
+
+	// Need to make a copy of the args in case our buffer isn't big
+	// enough and we need to use them again.
+	va_list args_copy;
+	va_copy(args_copy, args);
+
+	const int length = vsnprintf(buffer, BUFFER_SIZE, fmt, args);
+	// Need to add one for the terminating 0 byte
+	if (length + 1 <= BUFFER_SIZE) {
+		// The formatted string fit within our buffer, pass it on to river
+		river_wlroots_log_callback(importance, buffer, length);
+	} else {
+		// The formatted string did not fit in our buffer, we need
+		// to allocate enough memory to hold it.
+		char *allocated_buffer = malloc(length + 1);
+		if (allocated_buffer != NULL) {
+			const int length2 = vsnprintf(allocated_buffer, length + 1, fmt, args_copy);
+			assert(length2 == length);
+			river_wlroots_log_callback(importance, allocated_buffer, length);
+			free(allocated_buffer);
+		}
+	}
+
+	va_end(args_copy);
+}
+
+void river_init_wlroots_log(enum wlr_log_importance importance) {
+	wlr_log_init(importance, callback);
+}
+
+#include <time.h>
+#include <wlr/types/wlr_scene.h>
+#include <wlr/types/wlr_output.h>
+#include <wlr/types/wlr_compositor.h>
+#include <wlr/types/wlr_input_device.h>
+#include <wlr/types/wlr_keyboard.h>
+#include <wlr/interfaces/wlr_keyboard.h>
+#include <wlr/types/wlr_cursor.h>
+#include <wlr/types/wlr_seat.h>
+#include <wlr/types/wlr_pointer.h>
+#include <wlr/types/wlr_tablet_v2.h>
+#include <wlr/backend.h>
+#include <wlr/types/wlr_xdg_shell.h>
+#include <wlr/types/wlr_input_method_v2.h>
+#include <wlr/types/wlr_data_device.h>
+
+struct wlr_surface *river_scene_node_get_surface(struct wlr_scene_node *node) {
+	if (node->type == WLR_SCENE_NODE_BUFFER) {
+		struct wlr_scene_buffer *scene_buffer = wlr_scene_buffer_from_node(node);
+		struct wlr_scene_surface *scene_surface = wlr_scene_surface_try_from_buffer(scene_buffer);
+		if (scene_surface) {
+			return scene_surface->surface;
+		}
+	}
+	return NULL;
+}
+
+enum wlr_scene_node_type river_scene_node_get_type(struct wlr_scene_node *node) {
+	return node->type;
+}
+
+struct wlr_scene_tree *river_scene_node_get_parent(struct wlr_scene_node *node) {
+	return node->parent;
+}
+
+int river_scene_node_get_x(struct wlr_scene_node *node) {
+	return node->x;
+}
+
+int river_scene_node_get_y(struct wlr_scene_node *node) {
+	return node->y;
+}
+
+void *river_scene_node_get_data(struct wlr_scene_node *node) {
+	return node->data;
+}
+
+void river_scene_node_set_data(struct wlr_scene_node *node, void *data) {
+	node->data = data;
+}
+
+struct wl_signal *river_scene_node_get_destroy_signal(struct wlr_scene_node *node) {
+	return &node->events.destroy;
+}
+
+static void save_surface_tree_iter(struct wlr_scene_buffer *buffer, int sx, int sy, void *user_data) {
+	struct wlr_scene_tree *saved_tree = user_data;
+	struct wlr_scene_buffer *scene_buffer = wlr_scene_buffer_create(saved_tree, buffer->buffer);
+	if (!scene_buffer) {
+		return;
+	}
+	wlr_scene_node_set_position(&scene_buffer->node, sx, sy);
+	wlr_scene_buffer_set_dest_size(scene_buffer, buffer->dst_width, buffer->dst_height);
+	wlr_scene_buffer_set_source_box(scene_buffer, &buffer->src_box);
+	wlr_scene_buffer_set_transform(scene_buffer, buffer->transform);
+}
+
+void river_scene_tree_save_buffers(struct wlr_scene_tree *tree, struct wlr_scene_tree *saved_tree) {
+	wlr_scene_node_for_each_buffer(&tree->node, save_surface_tree_iter, saved_tree);
+}
+
+void river_scene_tree_clear_children(struct wlr_scene_tree *tree) {
+	struct wlr_scene_node *child, *tmp;
+	wl_list_for_each_safe(child, tmp, &tree->children, link) {
+		wlr_scene_node_destroy(child);
+	}
+}
+
+struct wlr_scene_node *river_scene_node_from_children_link(struct wl_list *link) {
+	return wl_container_of(link, (struct wlr_scene_node *)NULL, link);
+}
+
+struct wl_signal *river_wlr_output_get_destroy_signal(struct wlr_output *output) {
+	return &output->events.destroy;
+}
+
+struct wl_signal *river_wlr_output_get_request_state_signal(struct wlr_output *output) {
+	return &output->events.request_state;
+}
+
+struct wl_signal *river_wlr_output_get_frame_signal(struct wlr_output *output) {
+	return &output->events.frame;
+}
+
+struct wl_signal *river_wlr_output_get_present_signal(struct wlr_output *output) {
+	return &output->events.present;
+}
+
+void *river_wlr_output_get_data(struct wlr_output *output) {
+	return output->data;
+}
+
+void river_wlr_output_set_data(struct wlr_output *output, void *data) {
+	output->data = data;
+}
+
+const char *river_wlr_output_get_name(struct wlr_output *output) {
+	return output->name;
+}
+
+enum wlr_output_adaptive_sync_status river_wlr_output_get_adaptive_sync_status(struct wlr_output *output) {
+	return output->adaptive_sync_status;
+}
+
+bool river_wlr_output_get_enabled(struct wlr_output *output) {
+	return output->enabled;
+}
+
+struct wlr_output_mode *river_wlr_output_get_current_mode(struct wlr_output *output) {
+	return output->current_mode;
+}
+
+int32_t river_wlr_output_get_width(struct wlr_output *output) {
+	return output->width;
+}
+
+int32_t river_wlr_output_get_height(struct wlr_output *output) {
+	return output->height;
+}
+
+int32_t river_wlr_output_get_refresh(struct wlr_output *output) {
+	return output->refresh;
+}
+
+struct wl_global *river_wlr_output_get_global(struct wlr_output *output) {
+	return output->global;
+}
+
+void *river_wlr_surface_get_data(struct wlr_surface *surface) {
+	return surface->data;
+}
+
+void river_wlr_surface_set_data(struct wlr_surface *surface, void *data) {
+	surface->data = data;
+}
+
+struct wl_signal *river_wlr_surface_get_commit_signal(struct wlr_surface *surface) {
+	return &surface->events.commit;
+}
+
+enum wlr_input_device_type river_wlr_input_device_get_type(struct wlr_input_device *dev) {
+	return dev->type;
+}
+
+const char *river_wlr_input_device_get_name(struct wlr_input_device *dev) {
+	return dev->name;
+}
+
+struct wl_signal *river_wlr_input_device_get_destroy_signal(struct wlr_input_device *dev) {
+	return &dev->events.destroy;
+}
+
+void *river_wlr_input_device_get_data(struct wlr_input_device *dev) {
+	return dev->data;
+}
+
+void river_wlr_input_device_set_data(struct wlr_input_device *dev, void *data) {
+	dev->data = data;
+}
+
+struct wl_signal *river_wlr_keyboard_get_key_signal(struct wlr_keyboard *kbd) {
+	return &kbd->events.key;
+}
+
+struct wl_signal *river_wlr_keyboard_get_modifiers_signal(struct wlr_keyboard *kbd) {
+	return &kbd->events.modifiers;
+}
+
+struct wl_signal *river_wlr_keyboard_get_keymap_signal(struct wlr_keyboard *kbd) {
+	return &kbd->events.keymap;
+}
+
+struct xkb_keymap *river_wlr_keyboard_get_keymap(struct wlr_keyboard *kbd) {
+	return kbd->keymap;
+}
+
+struct wlr_keyboard_modifiers *river_wlr_keyboard_get_modifiers(struct wlr_keyboard *kbd) {
+	return &kbd->modifiers;
+}
+
+void *river_wlr_keyboard_get_data(struct wlr_keyboard *kbd) {
+	return kbd->data;
+}
+
+void river_wlr_keyboard_set_data(struct wlr_keyboard *kbd, void *data) {
+	kbd->data = data;
+}
+
+double river_wlr_cursor_get_x(struct wlr_cursor *cursor) {
+	return cursor->x;
+}
+
+double river_wlr_cursor_get_y(struct wlr_cursor *cursor) {
+	return cursor->y;
+}
+
+struct wl_signal *river_wlr_cursor_get_motion_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.motion;
+}
+
+struct wl_signal *river_wlr_cursor_get_motion_absolute_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.motion_absolute;
+}
+
+struct wl_signal *river_wlr_cursor_get_button_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.button;
+}
+
+struct wl_signal *river_wlr_cursor_get_axis_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.axis;
+}
+
+struct wl_signal *river_wlr_cursor_get_frame_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.frame;
+}
+
+struct wl_signal *river_wlr_cursor_get_swipe_begin_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.swipe_begin;
+}
+
+struct wl_signal *river_wlr_cursor_get_swipe_update_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.swipe_update;
+}
+
+struct wl_signal *river_wlr_cursor_get_swipe_end_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.swipe_end;
+}
+
+struct wl_signal *river_wlr_cursor_get_pinch_begin_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.pinch_begin;
+}
+
+struct wl_signal *river_wlr_cursor_get_pinch_update_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.pinch_update;
+}
+
+struct wl_signal *river_wlr_cursor_get_pinch_end_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.pinch_end;
+}
+
+struct wl_signal *river_wlr_cursor_get_hold_begin_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.hold_begin;
+}
+
+struct wl_signal *river_wlr_cursor_get_hold_end_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.hold_end;
+}
+
+struct wl_signal *river_wlr_cursor_get_touch_down_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.touch_down;
+}
+
+struct wl_signal *river_wlr_cursor_get_touch_motion_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.touch_motion;
+}
+
+struct wl_signal *river_wlr_cursor_get_touch_up_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.touch_up;
+}
+
+struct wl_signal *river_wlr_cursor_get_touch_cancel_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.touch_cancel;
+}
+
+struct wl_signal *river_wlr_cursor_get_touch_frame_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.touch_frame;
+}
+
+struct wl_signal *river_wlr_cursor_get_tablet_tool_axis_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.tablet_tool_axis;
+}
+
+struct wl_signal *river_wlr_cursor_get_tablet_tool_proximity_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.tablet_tool_proximity;
+}
+
+struct wl_signal *river_wlr_cursor_get_tablet_tool_tip_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.tablet_tool_tip;
+}
+
+struct wl_signal *river_wlr_cursor_get_tablet_tool_button_signal(struct wlr_cursor *cursor) {
+	return &cursor->events.tablet_tool_button;
+}
+
+const char *river_wlr_seat_get_name(struct wlr_seat *seat) {
+	return seat->name;
+}
+
+void *river_wlr_seat_get_data(struct wlr_seat *seat) {
+	return seat->data;
+}
+
+void river_wlr_seat_set_data(struct wlr_seat *seat, void *data) {
+	seat->data = data;
+}
+
+struct wl_signal *river_wlr_seat_get_request_set_cursor_signal(struct wlr_seat *seat) {
+	return &seat->events.request_set_cursor;
+}
+
+struct wl_signal *river_wlr_seat_get_request_set_selection_signal(struct wlr_seat *seat) {
+	return &seat->events.request_set_selection;
+}
+
+struct wl_signal *river_wlr_seat_get_request_start_drag_signal(struct wlr_seat *seat) {
+	return &seat->events.request_start_drag;
+}
+
+struct wl_signal *river_wlr_seat_get_start_drag_signal(struct wlr_seat *seat) {
+	return &seat->events.start_drag;
+}
+
+struct wl_signal *river_wlr_seat_get_request_set_primary_selection_signal(struct wlr_seat *seat) {
+	return &seat->events.request_set_primary_selection;
+}
+
+struct wlr_seat_client *river_wlr_seat_get_pointer_focused_client(struct wlr_seat *seat) {
+	return seat->pointer_state.focused_client;
+}
+
+struct wl_client *river_wlr_seat_client_get_client(struct wlr_seat_client *client) {
+	return client->client;
+}
+
+struct wlr_keyboard *river_wlr_seat_get_keyboard(struct wlr_seat *seat) {
+	return seat->keyboard_state.keyboard;
+}
+
+struct wl_global *river_wlr_seat_get_global(struct wlr_seat *seat) {
+	return seat->global;
+}
+
+struct wl_signal *river_wlr_backend_get_new_input_signal(struct wlr_backend *backend) {
+	return &backend->events.new_input;
+}
+
+const struct wlr_surface_role *river_wlr_surface_get_role(struct wlr_surface *surface) {
+	return surface->role;
+}
+
+struct wl_resource *river_wlr_surface_get_role_resource(struct wlr_surface *surface) {
+	return surface->role_resource;
+}
+
+void river_wlr_surface_set_role_object(struct wlr_surface *surface, struct wl_resource *role_resource) {
+	surface->role_resource = role_resource;
+}
+
+struct wlr_surface *river_wlr_xdg_surface_get_surface(struct wlr_xdg_surface *xdg_surface) {
+	return xdg_surface->surface;
+}
+
+struct wl_signal *river_wlr_xdg_surface_get_ack_configure_signal(struct wlr_xdg_surface *xdg_surface) {
+	return &xdg_surface->events.ack_configure;
+}
+
+struct wl_signal *river_wlr_xdg_surface_get_new_popup_signal(struct wlr_xdg_surface *xdg_surface) {
+	return &xdg_surface->events.new_popup;
+}
+
+void river_wlr_xdg_surface_get_geometry(struct wlr_xdg_surface *xdg_surface, struct wlr_box *box) {
+	*box = xdg_surface->geometry;
+}
+
+bool river_wlr_xdg_surface_get_initial_commit(struct wlr_xdg_surface *xdg_surface) {
+	return xdg_surface->initial_commit;
+}
+
+bool river_wlr_xdg_surface_get_initialized(struct wlr_xdg_surface *xdg_surface) {
+	return xdg_surface->initialized;
+}
+
+struct wlr_xdg_surface *river_wlr_xdg_toplevel_get_base(struct wlr_xdg_toplevel *toplevel) {
+	return toplevel->base;
+}
+
+struct wl_signal *river_wlr_xdg_toplevel_get_destroy_signal(struct wlr_xdg_toplevel *toplevel) {
+	return &toplevel->events.destroy;
+}
+
+struct wl_signal *river_wlr_xdg_toplevel_get_request_show_window_menu_signal(struct wlr_xdg_toplevel *toplevel) {
+	return &toplevel->events.request_show_window_menu;
+}
+
+struct wl_signal *river_wlr_xdg_toplevel_get_request_fullscreen_signal(struct wlr_xdg_toplevel *toplevel) {
+	return &toplevel->events.request_fullscreen;
+}
+
+struct wl_signal *river_wlr_xdg_toplevel_get_request_maximize_signal(struct wlr_xdg_toplevel *toplevel) {
+	return &toplevel->events.request_maximize;
+}
+
+struct wl_signal *river_wlr_xdg_toplevel_get_request_minimize_signal(struct wlr_xdg_toplevel *toplevel) {
+	return &toplevel->events.request_minimize;
+}
+
+struct wl_signal *river_wlr_xdg_toplevel_get_request_move_signal(struct wlr_xdg_toplevel *toplevel) {
+	return &toplevel->events.request_move;
+}
+
+struct wl_signal *river_wlr_xdg_toplevel_get_request_resize_signal(struct wlr_xdg_toplevel *toplevel) {
+	return &toplevel->events.request_resize;
+}
+
+struct wl_signal *river_wlr_xdg_toplevel_get_set_parent_signal(struct wlr_xdg_toplevel *toplevel) {
+	return &toplevel->events.set_parent;
+}
+
+struct wl_signal *river_wlr_xdg_toplevel_get_set_title_signal(struct wlr_xdg_toplevel *toplevel) {
+	return &toplevel->events.set_title;
+}
+
+struct wl_signal *river_wlr_xdg_toplevel_get_set_app_id_signal(struct wlr_xdg_toplevel *toplevel) {
+	return &toplevel->events.set_app_id;
+}
+
+const char *river_wlr_xdg_toplevel_get_title(struct wlr_xdg_toplevel *toplevel) {
+	return toplevel->title;
+}
+
+const char *river_wlr_xdg_toplevel_get_app_id(struct wlr_xdg_toplevel *toplevel) {
+	return toplevel->app_id;
+}
+
+struct wlr_xdg_toplevel *river_wlr_xdg_toplevel_get_parent(struct wlr_xdg_toplevel *toplevel) {
+	return toplevel->parent;
+}
+
+bool river_wlr_xdg_toplevel_get_requested_fullscreen(struct wlr_xdg_toplevel *toplevel) {
+	return toplevel->requested.fullscreen;
+}
+
+struct wlr_output *river_wlr_xdg_toplevel_get_requested_fullscreen_output(struct wlr_xdg_toplevel *toplevel) {
+	return toplevel->requested.fullscreen_output;
+}
+
+bool river_wlr_xdg_toplevel_get_requested_maximized(struct wlr_xdg_toplevel *toplevel) {
+	return toplevel->requested.maximized;
+}
+
+void river_wlr_xdg_toplevel_get_requested_min_max_size(struct wlr_xdg_toplevel *toplevel, int *min_w, int *min_h, int *max_w, int *max_h) {
+	*min_w = toplevel->current.min_width;
+	*min_h = toplevel->current.min_height;
+	*max_w = toplevel->current.max_width;
+	*max_h = toplevel->current.max_height;
+}
+
+struct wlr_xdg_surface *river_wlr_xdg_popup_get_base(struct wlr_xdg_popup *popup) {
+	return popup->base;
+}
+
+struct wl_signal *river_wlr_xdg_popup_get_destroy_signal(struct wlr_xdg_popup *popup) {
+	return &popup->events.destroy;
+}
+
+struct wl_signal *river_wlr_xdg_popup_get_reposition_signal(struct wlr_xdg_popup *popup) {
+	return &popup->events.reposition;
+}
+
+void river_wlr_xdg_popup_get_anchor_rect(struct wlr_xdg_popup *popup, struct wlr_box *box) {
+	*box = popup->scheduled.rules.anchor_rect;
+}
+
+void *river_wlr_xdg_surface_get_data(struct wlr_xdg_surface *xdg_surface) {
+	return xdg_surface->data;
+}
+
+void river_wlr_xdg_surface_set_data(struct wlr_xdg_surface *xdg_surface, void *data) {
+	xdg_surface->data = data;
+}
+
+struct wl_list *river_wlr_xdg_surface_get_popups(struct wlr_xdg_surface *xdg_surface) {
+	return &xdg_surface->popups;
+}
+
+struct wlr_scene_tree *river_wlr_scene_tree_get_parent(struct wlr_scene_tree *tree) {
+	return tree->node.parent;
+}
+
+struct wl_list *river_scene_tree_get_children(struct wlr_scene_tree *tree) {
+	return &tree->children;
+}
+
+struct wl_signal *river_wlr_surface_get_map_signal(struct wlr_surface *surface) {
+	return &surface->events.map;
+}
+
+struct wl_signal *river_wlr_surface_get_unmap_signal(struct wlr_surface *surface) {
+	return &surface->events.unmap;
+}
+
+struct wl_resource *river_wlr_surface_get_resource(struct wlr_surface *surface) {
+	return surface->resource;
+}
+
+bool river
diff truncated