Compare commits

..

28 Commits

Author SHA1 Message Date
Ivan Molodetskikh 72c8f569ac Bump version to 0.1.1 2024-02-03 10:00:06 +04:00
Ivan Molodetskikh 798d9c55df Support fullscreen for new windows 2024-02-03 09:45:26 +04:00
Ivan Molodetskikh 05613eed1e Verify that pending fullscreen matches column 2024-02-03 09:44:34 +04:00
Ivan Molodetskikh b23dd4b800 Respect natural-scroll for workspace switch gesture 2024-02-03 09:00:08 +04:00
Ivan Molodetskikh 1f72089a46 Place new workspace after current when moving
This feels more natural, also makes moving back and forth idempotent in
most cases.
2024-02-03 08:42:56 +04:00
Ivan Molodetskikh fbe9020915 Update dependencies 2024-02-02 17:04:17 +04:00
Ivan Molodetskikh 2036116f16 config: Premultiply alpha in Color when converting to f32
Smithay wants premultiplied alpha.
2024-02-01 18:53:45 +04:00
Ivan Molodetskikh 9afd728ae9 Add error messages to backend initialization 2024-02-01 16:55:46 +04:00
Andreas Stührk e51268a39e Add actions to move the active workspace to another monitor 2024-02-01 12:29:46 +04:00
Ivan Molodetskikh 0a715ce155 default-config: Improve wording for focus-ring/border comment
SSD or server-side decorations is never mentioned elsewhere.
2024-02-01 12:06:13 +04:00
Ivan Molodetskikh 89ac958670 default-config: Document how focus ring and border draw behind
Related: https://github.com/YaLTeR/niri/issues/150
2024-02-01 10:08:15 +04:00
Ivan Molodetskikh 2e50f8dee0 Hardcode winit transform for now 2024-01-31 23:02:38 +04:00
Ivan Molodetskikh 7052f0129e Stop screencasts on size changes 2024-01-31 23:02:38 +04:00
axtloss 962e159db6 Add option to rotate outputs 2024-01-31 23:02:38 +04:00
Ivan Molodetskikh 11bff3a2f1 Update Smithay (rotation fix) 2024-01-31 23:02:38 +04:00
Ivan Molodetskikh 15606304f2 README: Bring AUR link back 2024-01-30 22:36:30 -08:00
Christian Meissl 85eac9d9d0 chore: bump smithay
includes fixes for wrong direct scan-out transform
and damage artifacts on output transform changes.
also includes a fix for a race in popup surface re-use.
2024-01-30 15:30:31 +04:00
Ivan Molodetskikh d3f4583c90 foreign_toplevel: Use OutputHandler to send output_enter on demand 2024-01-30 12:30:57 +04:00
Ivan Molodetskikh fefb1cccd6 foreign_toplevel: Update the focused window last 2024-01-30 12:30:57 +04:00
Ivan Molodetskikh deef52519a foreign_toplevel: Change activated to mean keyboard focus 2024-01-30 12:30:57 +04:00
Ivan Molodetskikh 59ff331597 Implement wlr-foreign-toplevel-management
The parent event isn't sent but whatever.
2024-01-30 12:30:57 +04:00
Christian Meissl b813f99abd tty: reset surface state after changing monitor state
changing the "ACTIVE" property of a surface requires
to re-evaluate the surface state.
2024-01-30 08:03:21 +04:00
Ivan Molodetskikh d9b9cec8b8 README: Remove AUR link for now
It doesn't work properly yet apparently.
2024-01-29 12:29:32 -08:00
Christian Meissl 597ea62d17 input: update keyboard led state 2024-01-28 23:43:08 +04:00
Ivan Molodetskikh 51243a0a50 Show notification about creating a default config 2024-01-28 17:15:47 +04:00
Ivan Molodetskikh 0ebcc3e0d6 Create default config file if missing 2024-01-28 17:15:33 +04:00
Ivan Molodetskikh 64c85d865e winit: Don't remove output on CloseRequested
More winit events can process after CloseRequested, which will cause a
panic if trying to access the now-removed output.
2024-01-28 16:30:29 +04:00
Ivan Molodetskikh 367e4955ea Mark Msg as pub
Seems to break the build on 1.72.0 otherwise.
2024-01-28 09:34:42 +04:00
19 changed files with 1224 additions and 184 deletions
Generated
+94 -78
View File
@@ -38,9 +38,9 @@ checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
[[package]]
name = "android-activity"
version = "0.5.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39b801912a977c3fd52d80511fe1c0c8480c6f957f21ae2ce1b92ffe970cf4b9"
checksum = "ee91c0c2905bae44f84bfa4e044536541df26b7703fd0888deeb9060fcc44289"
dependencies = [
"android-properties",
"bitflags 2.4.2",
@@ -79,9 +79,9 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
checksum = "2faccea4cc4ab4a667ce676a30e8ec13922a692c99bb8f5b11f1502c72e04220"
[[package]]
name = "anstyle-parse"
@@ -215,9 +215,9 @@ dependencies = [
[[package]]
name = "async-io"
version = "2.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb41eb19024a91746eba0773aa5e16036045bbf45733766661099e182ea6a744"
checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65"
dependencies = [
"async-lock 3.3.0",
"cfg-if",
@@ -226,7 +226,7 @@ dependencies = [
"futures-lite 2.2.0",
"parking",
"polling 3.3.2",
"rustix 0.38.30",
"rustix 0.38.31",
"slab",
"tracing",
"windows-sys 0.52.0",
@@ -265,7 +265,7 @@ dependencies = [
"cfg-if",
"event-listener 3.1.0",
"futures-lite 1.13.0",
"rustix 0.38.30",
"rustix 0.38.31",
"windows-sys 0.48.0",
]
@@ -286,13 +286,13 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5"
dependencies = [
"async-io 2.3.0",
"async-io 2.3.1",
"async-lock 2.8.0",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix 0.38.30",
"rustix 0.38.31",
"signal-hook-registry",
"slab",
"windows-sys 0.48.0",
@@ -438,9 +438,9 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "bytemuck"
version = "1.14.0"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
checksum = "ed2490600f404f2b94c167e31d3ed1d5f3c225a0f3b80230053b3e0b7b962bd9"
dependencies = [
"bytemuck_derive",
]
@@ -504,7 +504,7 @@ dependencies = [
"futures-io",
"log",
"polling 3.3.2",
"rustix 0.38.30",
"rustix 0.38.31",
"slab",
"thiserror",
]
@@ -516,7 +516,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02"
dependencies = [
"calloop",
"rustix 0.38.30",
"rustix 0.38.31",
"wayland-backend",
"wayland-client",
]
@@ -861,7 +861,7 @@ dependencies = [
"bytemuck",
"drm-ffi",
"drm-fourcc",
"rustix 0.38.30",
"rustix 0.38.31",
]
[[package]]
@@ -871,7 +871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41334f8405792483e32ad05fbb9c5680ff4e84491883d2947a4757dc54cb2ac6"
dependencies = [
"drm-sys",
"rustix 0.38.30",
"rustix 0.38.31",
]
[[package]]
@@ -1308,7 +1308,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
dependencies = [
"heck",
"proc-macro-crate 2.0.1",
"proc-macro-crate 2.0.2",
"proc-macro-error",
"proc-macro2",
"quote",
@@ -1396,9 +1396,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.1.0"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520"
dependencies = [
"equivalent",
"hashbrown",
@@ -1551,9 +1551,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.152"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libloading"
@@ -1746,9 +1746,9 @@ dependencies = [
[[package]]
name = "memmap2"
version = "0.9.3"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45fd3a57831bf88bc63f8cebc0cf956116276e97fef3966103e96416209f7c92"
checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322"
dependencies = [
"libc",
]
@@ -1842,7 +1842,7 @@ dependencies = [
[[package]]
name = "niri"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"anyhow",
"arrayvec",
@@ -1883,7 +1883,7 @@ dependencies = [
[[package]]
name = "niri-config"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"bitflags 2.4.2",
"knuffel",
@@ -1895,7 +1895,7 @@ dependencies = [
[[package]]
name = "niri-ipc"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"serde",
]
@@ -1946,6 +1946,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.17"
@@ -1971,7 +1977,7 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro-crate 2.0.2",
"proc-macro2",
"quote",
"syn 2.0.48",
@@ -2253,7 +2259,7 @@ dependencies = [
"cfg-if",
"concurrent-queue",
"pin-project-lite",
"rustix 0.38.30",
"rustix 0.38.31",
"tracing",
"windows-sys 0.52.0",
]
@@ -2288,9 +2294,9 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a"
checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
dependencies = [
"toml_datetime",
"toml_edit 0.20.2",
@@ -2331,9 +2337,9 @@ dependencies = [
[[package]]
name = "profiling"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135ede8821cf6376eb7a64148901e1690b788c11ae94dc297ae917dbc91dc0e"
checksum = "0f0f7f43585c34e4fdd7497d746bc32e14458cf11c69341cc0587b1d825dde42"
dependencies = [
"profiling-procmacros",
"tracy-client",
@@ -2341,9 +2347,9 @@ dependencies = [
[[package]]
name = "profiling-procmacros"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b322d7d65c1ab449be3c890fcbd0db6e1092d0dd05d79dba2dd28032cebeb05"
checksum = "ce97fecd27bc49296e5e20518b5a1bb54a14f7d5fe6228bc9686ee2a74915cc8"
dependencies = [
"quote",
"syn 2.0.48",
@@ -2395,6 +2401,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.35"
@@ -2486,7 +2501,7 @@ checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.4",
"regex-automata 0.4.5",
"regex-syntax 0.8.2",
]
@@ -2501,9 +2516,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.4"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a"
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
dependencies = [
"aho-corasick",
"memchr",
@@ -2544,9 +2559,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.30"
version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca"
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [
"bitflags 2.4.2",
"errno",
@@ -2608,18 +2623,18 @@ checksum = "621e3680f3e07db4c9c2c3fb07c6223ab2fab2e54bd3c04c3ae037990f428c32"
[[package]]
name = "serde"
version = "1.0.195"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.195"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [
"proc-macro2",
"quote",
@@ -2628,9 +2643,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.111"
version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
dependencies = [
"itoa",
"ryu",
@@ -2716,7 +2731,7 @@ checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "smithay"
version = "0.3.0"
source = "git+https://github.com/Smithay/smithay.git#8854dee7c2f49e9077f10d484b0de9a8e81c587c"
source = "git+https://github.com/Smithay/smithay.git#0eac415ba2d9409cbc201955dc0fd306c116ae05"
dependencies = [
"appendlist",
"bitflags 2.4.2",
@@ -2742,7 +2757,7 @@ dependencies = [
"pkg-config",
"profiling",
"rand",
"rustix 0.38.30",
"rustix 0.38.31",
"scan_fmt",
"smallvec",
"tempfile",
@@ -2772,8 +2787,8 @@ dependencies = [
"cursor-icon",
"libc",
"log",
"memmap2 0.9.3",
"rustix 0.38.30",
"memmap2 0.9.4",
"rustix 0.38.31",
"thiserror",
"wayland-backend",
"wayland-client",
@@ -2788,7 +2803,7 @@ dependencies = [
[[package]]
name = "smithay-drm-extras"
version = "0.1.0"
source = "git+https://github.com/Smithay/smithay.git#8854dee7c2f49e9077f10d484b0de9a8e81c587c"
source = "git+https://github.com/Smithay/smithay.git#0eac415ba2d9409cbc201955dc0fd306c116ae05"
dependencies = [
"drm",
"edid-rs",
@@ -2872,7 +2887,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "006851c9ccefa3c38a7646b8cec804bb429def3da10497bfa977179869c3e8e2"
dependencies = [
"quick-xml",
"quick-xml 0.30.0",
"windows 0.51.1",
]
@@ -2885,7 +2900,7 @@ dependencies = [
"cfg-if",
"fastrand 2.0.1",
"redox_syscall 0.4.1",
"rustix 0.38.30",
"rustix 0.38.31",
"windows-sys 0.52.0",
]
@@ -2921,11 +2936,12 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e"
checksum = "fe80ced77cbfb4cb91a94bf72b378b4b6791a0d9b7f09d0be747d1bdff4e68bd"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde",
"time-core",
@@ -3071,9 +3087,9 @@ dependencies = [
[[package]]
name = "tracy-client-sys"
version = "0.22.1"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "078c7ed72141b0e4369671a7f7af0eecffe18d753bf0296adca9c7add7276c9d"
checksum = "9d104d610dfa9dd154535102cc9c6164ae1fa37842bc2d9e83f9ac82b0ae0882"
dependencies = [
"cc",
]
@@ -3280,13 +3296,13 @@ checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b"
[[package]]
name = "wayland-backend"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19152ddd73f45f024ed4534d9ca2594e0ef252c1847695255dae47f34df9fbe4"
checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40"
dependencies = [
"cc",
"downcast-rs",
"nix",
"rustix 0.38.31",
"scoped-tls",
"smallvec",
"wayland-sys",
@@ -3294,12 +3310,12 @@ dependencies = [
[[package]]
name = "wayland-client"
version = "0.31.1"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ca7d52347346f5473bf2f56705f360e8440873052e575e55890c4fa57843ed3"
checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f"
dependencies = [
"bitflags 2.4.2",
"nix",
"rustix 0.38.31",
"wayland-backend",
"wayland-scanner",
]
@@ -3317,11 +3333,11 @@ dependencies = [
[[package]]
name = "wayland-cursor"
version = "0.31.0"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44aa20ae986659d6c77d64d808a046996a932aa763913864dc40c359ef7ad5b"
checksum = "71ce5fa868dd13d11a0d04c5e2e65726d0897be8de247c0c5a65886e283231ba"
dependencies = [
"nix",
"rustix 0.38.31",
"wayland-client",
"xcursor",
]
@@ -3338,9 +3354,9 @@ dependencies = [
[[package]]
name = "wayland-protocols"
version = "0.31.0"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e253d7107ba913923dc253967f35e8561a3c65f914543e46843c88ddd729e21c"
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
dependencies = [
"bitflags 2.4.2",
"wayland-backend",
@@ -3391,25 +3407,25 @@ dependencies = [
[[package]]
name = "wayland-scanner"
version = "0.31.0"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb8e28403665c9f9513202b7e1ed71ec56fde5c107816843fb14057910b2c09c"
checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283"
dependencies = [
"proc-macro2",
"quick-xml",
"quick-xml 0.31.0",
"quote",
]
[[package]]
name = "wayland-server"
version = "0.31.0"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f3f0c52a445936ca1184c98f1a69cf4ad9c9130788884531ef04428468cb1ce"
checksum = "00e6e4d5c285bc24ba4ed2d5a4bd4febd5fd904451f465973225c8e99772fdb7"
dependencies = [
"bitflags 2.4.2",
"downcast-rs",
"io-lifetimes 2.0.3",
"nix",
"rustix 0.38.31",
"wayland-backend",
"wayland-scanner",
]
@@ -3725,7 +3741,7 @@ dependencies = [
"js-sys",
"libc",
"log",
"memmap2 0.9.3",
"memmap2 0.9.4",
"ndk",
"ndk-sys",
"objc2",
@@ -3734,7 +3750,7 @@ dependencies = [
"percent-encoding",
"raw-window-handle",
"redox_syscall 0.3.5",
"rustix 0.38.30",
"rustix 0.38.31",
"smithay-client-toolkit",
"smol_str",
"unicode-segmentation",
@@ -3754,9 +3770,9 @@ dependencies = [
[[package]]
name = "winnow"
version = "0.5.34"
version = "0.5.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16"
checksum = "818ce546a11a9986bc24f93d0cdf38a8a1a400f1473ea8c82e59f6e0ffab9249"
dependencies = [
"memchr",
]
@@ -3783,7 +3799,7 @@ dependencies = [
"libc",
"libloading",
"once_cell",
"rustix 0.38.30",
"rustix 0.38.31",
"x11rb-protocol",
]
+8 -8
View File
@@ -1,5 +1,5 @@
[workspace.package]
version = "0.1.0"
version = "0.1.1"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -9,7 +9,7 @@ repository = "https://github.com/YaLTeR/niri"
[workspace.dependencies]
bitflags = "2.4.2"
directories = "5.0.1"
serde = { version = "1.0.195", features = ["derive"] }
serde = { version = "1.0.196", features = ["derive"] }
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
tracy-client = { version = "0.16.5", default-features = false }
@@ -46,20 +46,20 @@ directories = "5.0.1"
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.152"
libc = "0.2.153"
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
logind-zbus = { version = "3.1.2", optional = true }
niri-config = { version = "0.1.0", path = "niri-config" }
niri-ipc = { version = "0.1.0", path = "niri-ipc" }
niri-config = { version = "0.1.1", path = "niri-config" }
niri-ipc = { version = "0.1.1", path = "niri-ipc" }
notify-rust = { version = "4.10.0", optional = true }
pangocairo = "0.18.0"
pipewire = { version = "0.7.2", optional = true }
png = "0.17.11"
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
profiling = "1.0.13"
profiling = "1.0.14"
sd-notify = "0.4.1"
serde.workspace = true
serde_json = "1.0.111"
serde_json = "1.0.113"
smithay-drm-extras.workspace = true
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing.workspace = true
@@ -109,7 +109,7 @@ lto = "thin"
debug = false
[package.metadata.generate-rpm]
version = "0.1.0"
version = "0.1.1"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+60 -1
View File
@@ -187,6 +187,8 @@ pub struct Output {
pub name: String,
#[knuffel(child, unwrap(argument), default = 1.)]
pub scale: f64,
#[knuffel(child, unwrap(argument, str), default = Transform::Normal)]
pub transform: Transform,
#[knuffel(child)]
pub position: Option<Position>,
#[knuffel(child, unwrap(argument, str))]
@@ -199,12 +201,62 @@ impl Default for Output {
off: false,
name: String::new(),
scale: 1.,
transform: Transform::Normal,
position: None,
mode: None,
}
}
}
/// Output transform, which goes counter-clockwise.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Transform {
Normal,
_90,
_180,
_270,
Flipped,
Flipped90,
Flipped180,
Flipped270,
}
impl FromStr for Transform {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"normal" => Ok(Self::Normal),
"90" => Ok(Self::_90),
"180" => Ok(Self::_180),
"270" => Ok(Self::_270),
"flipped" => Ok(Self::Flipped),
"flipped-90" => Ok(Self::Flipped90),
"flipped-180" => Ok(Self::Flipped180),
"flipped-270" => Ok(Self::Flipped270),
_ => Err(miette!(concat!(
r#"invalid transform, can be "90", "180", "270", "#,
r#""flipped", "flipped-90", "flipped-180" or "flipped-270""#
))),
}
}
}
impl From<Transform> for smithay::utils::Transform {
fn from(value: Transform) -> Self {
match value {
Transform::Normal => Self::Normal,
Transform::_90 => Self::_90,
Transform::_180 => Self::_180,
Transform::_270 => Self::_270,
Transform::Flipped => Self::Flipped,
Transform::Flipped90 => Self::Flipped90,
Transform::Flipped180 => Self::Flipped180,
Transform::Flipped270 => Self::Flipped270,
}
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Position {
#[knuffel(property)]
@@ -296,7 +348,8 @@ impl Color {
impl From<Color> for [f32; 4] {
fn from(c: Color) -> Self {
[c.r, c.g, c.b, c.a].map(|x| x as f32 / 255.)
let [r, g, b, a] = [c.r, c.g, c.b, c.a].map(|x| x as f32 / 255.);
[r * a, g * a, b * a, a]
}
}
@@ -438,6 +491,10 @@ pub enum Action {
SetColumnWidth(#[knuffel(argument, str)] SizeChange),
SwitchLayout(#[knuffel(argument)] LayoutAction),
ShowHotkeyOverlay,
MoveWorkspaceToMonitorLeft,
MoveWorkspaceToMonitorRight,
MoveWorkspaceToMonitorDown,
MoveWorkspaceToMonitorUp,
}
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -726,6 +783,7 @@ mod tests {
output "eDP-1" {
scale 2.0
transform "flipped-90"
position x=10 y=20
mode "1920x1080@144"
}
@@ -826,6 +884,7 @@ mod tests {
off: false,
name: "eDP-1".to_owned(),
scale: 2.,
transform: Transform::Flipped90,
position: Some(Position { x: 10, y: 20 }),
mode: Some(Mode {
width: 1920,
+16
View File
@@ -65,6 +65,10 @@ input {
// Scale is a floating-point number, but at the moment only integer values work.
scale 2.0
// Transform allows to rotate the output counter-clockwise, valid values are:
// normal, 90, 180, 270, flipped, flipped-90, flipped-180 and flipped-270.
transform "normal"
// Resolution and, optionally, refresh rate of the output.
// The format is "<width>x<height>" or "<width>x<height>@<refresh rate>".
// If the refresh rate is omitted, niri will pick the highest refresh rate
@@ -86,6 +90,14 @@ input {
}
layout {
// By default focus ring and border are rendered as a solid background rectangle
// behind windows. That is, they will show up through semitransparent windows.
// This is because windows using client-side decoratins can have an arbitrary shape.
//
// If you don't like that, you should uncomment `prefer-no-csd` below.
// Niri will draw focus ring and border *around* windows that agree to omit their
// client-side decorations.
// You can change how the focus ring looks.
focus-ring {
// Uncomment this line to disable the focus ring.
@@ -263,6 +275,10 @@ binds {
// Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
// ...
// And you can also move a whole workspace to another monitor:
// Mod+Shift+Ctrl+Left { move-workspace-to-monitor-left; }
// ...
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+U { focus-workspace-down; }
+1 -1
View File
@@ -138,7 +138,7 @@ impl Backend {
}
}
pub fn set_monitors_active(&self, active: bool) {
pub fn set_monitors_active(&mut self, active: bool) {
match self {
Backend::Tty(tty) => tty.set_monitors_active(active),
Backend::Winit(_) => (),
+47 -20
View File
@@ -144,11 +144,19 @@ pub struct SurfaceDmabufFeedback {
}
impl Tty {
pub fn new(config: Rc<RefCell<Config>>, event_loop: LoopHandle<'static, State>) -> Self {
let (session, notifier) = LibSeatSession::new().unwrap();
pub fn new(
config: Rc<RefCell<Config>>,
event_loop: LoopHandle<'static, State>,
) -> anyhow::Result<Self> {
let (session, notifier) = LibSeatSession::new().context(
"Error creating a session. This might mean that you're trying to run niri on a TTY \
that is already busy, for example if you're running this inside tmux that had been \
originally started on a different TTY",
)?;
let seat_name = session.seat();
let udev_backend = UdevBackend::new(session.seat()).unwrap();
let udev_backend =
UdevBackend::new(session.seat()).context("error creating a udev backend")?;
let udev_dispatcher = Dispatcher::new(udev_backend, move |event, _, state: &mut State| {
state.backend.tty().on_udev_event(&mut state.niri, event);
});
@@ -157,7 +165,9 @@ impl Tty {
.unwrap();
let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
libinput.udev_assign_seat(&seat_name).unwrap();
libinput
.udev_assign_seat(&seat_name)
.map_err(|()| anyhow!("error assigning the seat to libinput"))?;
let input_backend = LibinputInputBackend::new(libinput.clone());
event_loop
@@ -192,18 +202,23 @@ impl Tty {
Ok(gles)
};
let api = GbmGlesBackend::with_factory(Box::new(create_renderer));
let gpu_manager = GpuManager::new(api).unwrap();
let gpu_manager = GpuManager::new(api).context("error creating the GPU manager")?;
let (primary_node, primary_render_node) = primary_node_from_config(&config.borrow())
.unwrap_or_else(|| {
let primary_gpu_path = udev::primary_gpu(&seat_name).unwrap().unwrap();
let primary_node = DrmNode::from_path(primary_gpu_path).unwrap();
.ok_or(())
.or_else(|()| {
let primary_gpu_path = udev::primary_gpu(&seat_name)
.context("error getting the primary GPU")?
.context("couldn't find a GPU")?;
let primary_node = DrmNode::from_path(primary_gpu_path)
.context("error opening the primary GPU DRM node")?;
let primary_render_node = primary_node
.node_with_type(NodeType::Render)
.unwrap()
.unwrap();
(primary_node, primary_render_node)
});
.context("error getting the render node for the primary GPU")?
.context("error getting the render node for the primary GPU")?;
Ok::<_, anyhow::Error>((primary_node, primary_render_node))
})?;
let mut node_path = String::new();
if let Some(path) = primary_render_node.dev_path() {
@@ -213,7 +228,7 @@ impl Tty {
}
info!("using as the render node: {}", node_path);
Self {
Ok(Self {
config,
session,
udev_dispatcher,
@@ -227,7 +242,7 @@ impl Tty {
update_output_config_on_resume: false,
ipc_outputs: Rc::new(RefCell::new(HashMap::new())),
enabled_outputs: Arc::new(Mutex::new(HashMap::new())),
}
})
}
pub fn init(&mut self, niri: &mut Niri) {
@@ -728,7 +743,7 @@ impl Tty {
// Power on all monitors if necessary and queue a redraw on the new one.
niri.event_loop.insert_idle(move |state| {
state.niri.activate_monitors(&state.backend);
state.niri.activate_monitors(&mut state.backend);
state.niri.queue_redraw(output);
});
@@ -1031,7 +1046,7 @@ impl Tty {
niri.send_dmabuf_feedbacks(output, dmabuf_feedback, &res.states);
}
if res.damage.is_some() {
if !res.is_empty {
let presentation_feedbacks =
niri.take_presentation_feedbacks(output, &res.states);
let data = (presentation_feedbacks, target_presentation_time);
@@ -1196,10 +1211,22 @@ impl Tty {
self.devices.get(&self.primary_node).map(|d| d.gbm.clone())
}
pub fn set_monitors_active(&self, active: bool) {
for device in self.devices.values() {
for crtc in device.surfaces.keys() {
set_crtc_active(&device.drm, *crtc, active);
pub fn set_monitors_active(&mut self, active: bool) {
// We only disable the CRTC here, this will also reset the
// surface state so that the next call to `render_frame` will
// always produce a new frame and `queue_frame` will change
// the CRTC to active. This makes sure we always enable a CRTC
// within an atomic operation.
if active {
return;
}
for device in self.devices.values_mut() {
for (crtc, surface) in device.surfaces.iter_mut() {
set_crtc_active(&device.drm, *crtc, false);
if let Err(err) = surface.compositor.reset_state() {
warn!("error resetting surface state: {err:?}");
}
}
}
}
+9 -10
View File
@@ -16,7 +16,6 @@ use smithay::reexports::calloop::LoopHandle;
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use smithay::reexports::winit::dpi::LogicalSize;
use smithay::reexports::winit::window::WindowBuilder;
use smithay::utils::Transform;
use super::RenderResult;
use crate::niri::{RedrawState, State};
@@ -33,12 +32,15 @@ pub struct Winit {
}
impl Winit {
pub fn new(config: Rc<RefCell<Config>>, event_loop: LoopHandle<State>) -> Self {
pub fn new(
config: Rc<RefCell<Config>>,
event_loop: LoopHandle<State>,
) -> Result<Self, winit::Error> {
let builder = WindowBuilder::new()
.with_inner_size(LogicalSize::new(1280.0, 800.0))
// .with_resizable(false)
.with_title("niri");
let (backend, winit) = winit::init_from_builder(builder).unwrap();
let (backend, winit) = winit::init_from_builder(builder)?;
let output = Output::new(
"winit".to_string(),
@@ -54,7 +56,7 @@ impl Winit {
size: backend.window_size(),
refresh: 60_000,
};
output.change_current_state(Some(mode), Some(Transform::Flipped180), None, None);
output.change_current_state(Some(mode), None, None, None);
output.set_preferred(mode);
let physical_properties = output.physical_properties();
@@ -107,21 +109,18 @@ impl Winit {
WinitEvent::Redraw => state
.niri
.queue_redraw(state.backend.winit().output.clone()),
WinitEvent::CloseRequested => {
state.niri.stop_signal.stop();
state.niri.remove_output(&state.backend.winit().output);
}
WinitEvent::CloseRequested => state.niri.stop_signal.stop(),
})
.unwrap();
Self {
Ok(Self {
config,
output,
backend,
damage_tracker,
ipc_outputs,
enabled_outputs,
}
})
}
pub fn init(&mut self, niri: &mut Niri) {
+46 -6
View File
@@ -1,5 +1,6 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::Duration;
use pangocairo::cairo::{self, ImageSurface};
@@ -26,6 +27,10 @@ const BORDER: i32 = 4;
pub struct ConfigErrorNotification {
state: State,
buffers: RefCell<HashMap<i32, Option<MemoryRenderBuffer>>>,
// If set, this is a "Created config at {path}" notification. If unset, this is a config error
// notification.
created_path: Option<PathBuf>,
}
enum State {
@@ -43,10 +48,25 @@ impl ConfigErrorNotification {
Self {
state: State::Hidden,
buffers: RefCell::new(HashMap::new()),
created_path: None,
}
}
pub fn show_created(&mut self, created_path: Option<PathBuf>) {
if self.created_path != created_path {
self.created_path = created_path;
self.buffers.borrow_mut().clear();
}
self.state = State::Showing(Animation::new(0., 1., Duration::from_millis(250)));
}
pub fn show(&mut self) {
if self.created_path.is_some() {
self.created_path = None;
self.buffers.borrow_mut().clear();
}
// Show from scratch even if already showing to bring attention.
self.state = State::Showing(Animation::new(0., 1., Duration::from_millis(250)));
}
@@ -65,7 +85,15 @@ impl ConfigErrorNotification {
State::Showing(anim) => {
anim.set_current_time(target_presentation_time);
if anim.is_done() {
self.state = State::Shown(target_presentation_time + Duration::from_secs(4));
let duration = if self.created_path.is_some() {
// Make this quite a bit longer because it comes with a monitor modeset
// (can take a while) and an important hotkeys popup diverting the
// attention.
Duration::from_secs(8)
} else {
Duration::from_secs(4)
};
self.state = State::Shown(target_presentation_time + duration);
}
}
State::Shown(deadline) => {
@@ -96,11 +124,12 @@ impl ConfigErrorNotification {
}
let scale = output.current_scale().integer_scale();
let path = self.created_path.as_deref();
let mut buffers = self.buffers.borrow_mut();
let buffer = buffers
.entry(scale)
.or_insert_with_key(move |&scale| render(scale).ok());
.or_insert_with_key(move |&scale| render(scale, path).ok());
let buffer = buffer.as_ref()?;
let elem = MemoryRenderBufferRenderElement::from_buffer(
@@ -138,11 +167,22 @@ impl ConfigErrorNotification {
}
}
fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
fn render(scale: i32, created_path: Option<&Path>) -> anyhow::Result<MemoryRenderBuffer> {
let _span = tracy_client::span!("config_error_notification::render");
let padding = PADDING * scale;
let mut text = String::from(TEXT);
let mut border_color = (1., 0.3, 0.3);
if let Some(path) = created_path {
text = format!(
"Created a default config file at \
<span face='monospace' bgcolor='#000000'>{:?}</span>",
path
);
border_color = (0.5, 1., 0.5);
};
let mut font = FontDescription::from_string(FONT);
font.set_absolute_size((font.size() * scale).into());
@@ -150,7 +190,7 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
let cr = cairo::Context::new(&surface)?;
let layout = pangocairo::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_markup(TEXT);
layout.set_markup(&text);
let (mut width, mut height) = layout.pixel_size();
width += padding * 2;
@@ -168,7 +208,7 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
cr.move_to(padding.into(), padding.into());
let layout = pangocairo::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_markup(TEXT);
layout.set_markup(&text);
cr.set_source_rgb(1., 1., 1.);
pangocairo::show_layout(&cr, &layout);
@@ -178,7 +218,7 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
cr.line_to(width.into(), height.into());
cr.line_to(0., height.into());
cr.line_to(0., 0.);
cr.set_source_rgb(1., 0.3, 0.3);
cr.set_source_rgb(border_color.0, border_color.1, border_color.2);
cr.set_line_width((BORDER * scale).into());
cr.stroke()?;
drop(cr);
+80 -1
View File
@@ -11,8 +11,10 @@ use std::thread;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::desktop::{PopupKind, PopupManager};
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
use smithay::input::{Seat, SeatHandler, SeatState};
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
use smithay::output::Output;
use smithay::reexports::input;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
@@ -21,6 +23,7 @@ use smithay::utils::{Logical, Rectangle, Size};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
use smithay::wayland::output::OutputHandler;
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
use smithay::wayland::security_context::{
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
@@ -45,7 +48,11 @@ use smithay::{
delegate_tablet_manager, delegate_text_input_manager, delegate_virtual_keyboard_manager,
};
use crate::delegate_foreign_toplevel;
use crate::niri::{ClientState, State};
use crate::protocols::foreign_toplevel::{
self, ForeignToplevelHandler, ForeignToplevelManagerState,
};
use crate::utils::output_size;
impl SeatHandler for State {
@@ -73,6 +80,19 @@ impl SeatHandler for State {
set_data_device_focus(dh, seat, client.clone());
set_primary_focus(dh, seat, client);
}
fn led_state_changed(&mut self, _seat: &Seat<Self>, led_state: keyboard::LedState) {
let keyboards = self
.niri
.devices
.iter()
.filter(|device| device.has_capability(input::DeviceCapability::Keyboard))
.cloned();
for mut keyboard in keyboards {
keyboard.led_update(led_state.into());
}
}
}
delegate_seat!(State);
delegate_cursor_shape!(State);
@@ -189,6 +209,11 @@ impl DataControlHandler for State {
delegate_data_control!(State);
impl OutputHandler for State {
fn output_bound(&mut self, output: Output, wl_output: WlOutput) {
foreign_toplevel::on_output_bound(self, &output, &wl_output);
}
}
delegate_output!(State);
delegate_presentation!(State);
@@ -277,3 +302,57 @@ impl SecurityContextHandler for State {
}
}
delegate_security_context!(State);
impl ForeignToplevelHandler for State {
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState {
&mut self.niri.foreign_toplevel_state
}
fn activate(&mut self, wl_surface: WlSurface) {
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
let window = window.clone();
self.niri.layout.activate_window(&window);
self.niri.queue_redraw_all();
}
}
fn close(&mut self, wl_surface: WlSurface) {
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
window.toplevel().send_close();
}
}
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>) {
if let Some((window, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
{
if !window
.toplevel()
.current_state()
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
{
return;
}
let window = window.clone();
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(window.clone(), &requested_output);
}
}
self.niri.layout.set_fullscreen(&window, true);
}
}
fn unset_fullscreen(&mut self, wl_surface: WlSurface) {
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
let window = window.clone();
self.niri.layout.set_fullscreen(&window, false);
}
}
}
delegate_foreign_toplevel!(State);
+14
View File
@@ -230,6 +230,13 @@ impl XdgShellHandler for State {
}
self.niri.layout.set_fullscreen(&window, true);
} else if let Some(window) = self.niri.unmapped_windows.get(surface.wl_surface()) {
if let Some(ws) = self.niri.layout.active_workspace() {
window.toplevel().with_pending_state(|state| {
state.size = Some(ws.view_size());
state.states.set(xdg_toplevel::State::Fullscreen);
});
}
}
}
@@ -246,6 +253,13 @@ impl XdgShellHandler for State {
{
let window = window.clone();
self.niri.layout.set_fullscreen(&window, false);
} else if let Some(window) = self.niri.unmapped_windows.get(surface.wl_surface()) {
if let Some(ws) = self.niri.layout.active_workspace() {
window.toplevel().with_pending_state(|state| {
state.size = Some(ws.new_window_size());
state.states.unset(xdg_toplevel::State::Fullscreen);
});
}
}
}
+51 -7
View File
@@ -51,7 +51,7 @@ impl State {
// Power on monitors if they were off.
if should_activate_monitors(&event) {
self.niri.activate_monitors(&self.backend);
self.niri.activate_monitors(&mut self.backend);
}
let hide_hotkey_overlay =
@@ -127,6 +127,17 @@ impl State {
}
}
if device.has_capability(input::DeviceCapability::Keyboard) {
if let Some(led_state) = self
.niri
.seat
.get_keyboard()
.map(|keyboard| keyboard.led_state())
{
device.led_update(led_state.into());
}
}
apply_libinput_settings(&self.niri.config.borrow().input, device);
}
InputEvent::DeviceRemoved { device } => {
@@ -273,7 +284,7 @@ impl State {
self.niri.suppressed_keys.clear();
}
Action::PowerOffMonitors => {
self.niri.deactivate_monitors(&self.backend);
self.niri.deactivate_monitors(&mut self.backend);
}
Action::ToggleDebugTint => {
self.backend.toggle_debug_tint();
@@ -598,6 +609,30 @@ impl State {
self.niri.queue_redraw_all();
}
}
Action::MoveWorkspaceToMonitorLeft => {
if let Some(output) = self.niri.output_left() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveWorkspaceToMonitorRight => {
if let Some(output) = self.niri.output_right() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveWorkspaceToMonitorDown => {
if let Some(output) = self.niri.output_down() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveWorkspaceToMonitorUp => {
if let Some(output) = self.niri.output_up() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
}
}
}
}
@@ -1121,11 +1156,20 @@ impl State {
);
}
fn on_gesture_swipe_update<I: InputBackend>(&mut self, event: I::GestureSwipeUpdateEvent) {
let res = self
.niri
.layout
.workspace_switch_gesture_update(event.delta_y());
fn on_gesture_swipe_update<I: InputBackend>(&mut self, event: I::GestureSwipeUpdateEvent)
where
I::Device: 'static,
{
let mut delta_y = event.delta_y();
let device = event.device();
if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
if device.config_scroll_natural_scroll_enabled() {
delta_y = -delta_y;
}
}
let res = self.niri.layout.workspace_switch_gesture_update(delta_y);
if let Some(output) = res {
if let Some(output) = output {
self.niri.queue_redraw(output);
+151 -2
View File
@@ -29,6 +29,7 @@
//! compromise we only keep the first workspace there, and move the rest to the primary output,
//! making the primary output their original output.
use std::cmp::min;
use std::mem;
use std::rc::Rc;
use std::time::Duration;
@@ -103,6 +104,11 @@ pub trait LayoutElement: PartialEq {
///
/// This will *not* switch immediately after a [`LayoutElement::request_fullscreen()`] call.
fn is_fullscreen(&self) -> bool;
/// Whether we're requesting the element to be fullscreen.
///
/// This *will* switch immediately after a [`LayoutElement::request_fullscreen()`] call.
fn is_pending_fullscreen(&self) -> bool;
}
#[derive(Debug)]
@@ -289,6 +295,11 @@ impl LayoutElement for Window {
.states
.contains(xdg_toplevel::State::Fullscreen)
}
fn is_pending_fullscreen(&self) -> bool {
self.toplevel()
.with_pending_state(|state| state.states.contains(xdg_toplevel::State::Fullscreen))
}
}
impl<W: LayoutElement> Layout<W> {
@@ -776,6 +787,27 @@ impl<W: LayoutElement> Layout<W> {
mon.workspaces.iter().flat_map(|ws| ws.windows())
}
pub fn with_windows(&self, mut f: impl FnMut(&W, Option<&Output>)) {
match &self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mon.workspaces {
for win in ws.windows() {
f(win, Some(&mon.output));
}
}
}
}
MonitorSet::NoOutputs { workspaces } => {
for ws in workspaces {
for win in ws.windows() {
f(win, None);
}
}
}
}
}
fn active_monitor(&mut self) -> Option<&mut Monitor<W>> {
let MonitorSet::Normal {
monitors,
@@ -1314,6 +1346,47 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn move_workspace_to_output(&mut self, output: &Output) {
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
else {
return;
};
let current = &mut monitors[*active_monitor_idx];
if current.active_workspace_idx == current.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(current.output.clone(), current.options.clone());
current.workspaces.push(ws);
}
let mut ws = current.workspaces.remove(current.active_workspace_idx);
current.active_workspace_idx = current.active_workspace_idx.saturating_sub(1);
current.workspace_switch = None;
current.clean_up_workspaces();
ws.set_output(Some(output.clone()));
ws.original_output = OutputId::new(output);
let target_idx = monitors
.iter()
.position(|mon| &mon.output == output)
.unwrap();
let target = &mut monitors[target_idx];
// Insert the workspace after the currently active one. Unless the currently active one is
// the last empty workspace, then insert before.
let target_ws_idx = min(target.active_workspace_idx + 1, target.workspaces.len() - 1);
target.workspaces.insert(target_ws_idx, ws);
target.active_workspace_idx = target_ws_idx;
target.workspace_switch = None;
target.clean_up_workspaces();
*active_monitor_idx = target_idx;
}
pub fn set_fullscreen(&mut self, window: &W, is_fullscreen: bool) {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
@@ -1399,7 +1472,7 @@ impl<W: LayoutElement> Layout<W> {
for monitor in monitors {
if let Some(WorkspaceSwitch::Gesture(gesture)) = &mut monitor.workspace_switch {
// Normalize like GNOME Shell's workspace switching.
let delta_y = -delta_y / 400.;
let delta_y = delta_y / 400.;
let min = gesture.center_idx.saturating_sub(1) as f64;
let max = (gesture.center_idx + 1).min(monitor.workspaces.len() - 1) as f64;
@@ -1523,6 +1596,7 @@ mod tests {
requested_size: Cell<Option<Size<i32, Logical>>>,
min_size: Size<i32, Logical>,
max_size: Size<i32, Logical>,
pending_fullscreen: Cell<bool>,
}
#[derive(Debug, Clone)]
@@ -1542,6 +1616,7 @@ mod tests {
requested_size: Cell::new(None),
min_size,
max_size,
pending_fullscreen: Cell::new(false),
}))
}
@@ -1598,9 +1673,12 @@ mod tests {
fn request_size(&self, size: Size<i32, Logical>) {
self.0.requested_size.set(Some(size));
self.0.pending_fullscreen.set(false);
}
fn request_fullscreen(&self, _size: Size<i32, Logical>) {}
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
self.0.pending_fullscreen.set(true);
}
fn min_size(&self) -> Size<i32, Logical> {
self.0.min_size
@@ -1627,6 +1705,10 @@ mod tests {
fn is_fullscreen(&self) -> bool {
false
}
fn is_pending_fullscreen(&self) -> bool {
self.0.pending_fullscreen.get()
}
}
fn arbitrary_bbox() -> impl Strategy<Value = Rectangle<i32, Logical>> {
@@ -1716,6 +1798,7 @@ mod tests {
SetColumnWidth(#[proptest(strategy = "arbitrary_size_change()")] SizeChange),
SetWindowHeight(#[proptest(strategy = "arbitrary_size_change()")] SizeChange),
Communicate(#[proptest(strategy = "1..=5usize")] usize),
MoveWorkspaceToOutput(#[proptest(strategy = "1..=5u8")] u8),
}
impl Op {
@@ -1889,6 +1972,14 @@ mod tests {
layout.update_window(&win);
}
}
Op::MoveWorkspaceToOutput(id) => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
layout.move_workspace_to_output(&output);
}
}
}
}
@@ -1945,6 +2036,9 @@ mod tests {
Op::CloseWindow(0),
Op::CloseWindow(1),
Op::CloseWindow(2),
Op::FullscreenWindow(1),
Op::FullscreenWindow(2),
Op::FullscreenWindow(3),
Op::FocusColumnLeft,
Op::FocusColumnRight,
Op::FocusWindowUp,
@@ -1975,6 +2069,7 @@ mod tests {
Op::MoveWindowDownOrToWorkspaceDown,
Op::MoveWindowUp,
Op::MoveWindowUpOrToWorkspaceUp,
Op::MoveWorkspaceToOutput(1),
];
for third in every_op {
@@ -2072,6 +2167,9 @@ mod tests {
Op::CloseWindow(0),
Op::CloseWindow(1),
Op::CloseWindow(2),
Op::FullscreenWindow(1),
Op::FullscreenWindow(2),
Op::FullscreenWindow(3),
Op::FocusColumnLeft,
Op::FocusColumnRight,
Op::FocusWindowUp,
@@ -2389,6 +2487,57 @@ mod tests {
check_ops(&ops);
}
#[test]
fn move_workspace_to_output() {
let ops = [
Op::AddOutput(1),
Op::AddOutput(2),
Op::FocusOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::MoveWorkspaceToOutput(2),
];
let mut layout = Layout::default();
for op in ops {
op.apply(&mut layout);
}
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = layout.monitor_set
else {
unreachable!()
};
assert_eq!(active_monitor_idx, 1);
assert_eq!(monitors[0].workspaces.len(), 1);
assert!(!monitors[0].workspaces[0].has_windows());
assert_eq!(monitors[1].active_workspace_idx, 0);
assert_eq!(monitors[1].workspaces.len(), 2);
assert!(monitors[1].workspaces[0].has_windows());
}
#[test]
fn fullscreen() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
Op::FullscreenWindow(1),
];
check_ops(&ops);
}
fn arbitrary_spacing() -> impl Strategy<Value = u16> {
// Give equal weight to:
// - 0: the element is disabled
+19 -2
View File
@@ -330,6 +330,10 @@ impl<W: LayoutElement> Workspace<W> {
}
}
pub fn view_size(&self) -> Size<i32, Logical> {
self.view_size
}
pub fn update_output_scale_transform(&mut self) {
let Some(output) = self.output.as_ref() else {
return;
@@ -351,7 +355,7 @@ impl<W: LayoutElement> Workspace<W> {
))
}
pub fn configure_new_window(&self, window: &Window) {
pub fn new_window_size(&self) -> Size<i32, Logical> {
let width = if let Some(width) = self.options.default_width {
let mut width = width.resolve(&self.options, self.working_area.size.w);
if !self.options.border.off {
@@ -367,8 +371,11 @@ impl<W: LayoutElement> Workspace<W> {
height -= self.options.border.width as i32 * 2;
}
let size = Size::from((width, max(height, 1)));
Size::from((width, max(height, 1)))
}
pub fn configure_new_window(&self, window: &Window) {
let size = self.new_window_size();
let bounds = self.toplevel_bounds();
if let Some(output) = self.output.as_ref() {
@@ -1151,8 +1158,14 @@ impl<W: LayoutElement> Column<W> {
options,
};
let is_pending_fullscreen = window.is_pending_fullscreen();
rv.add_window(window);
if is_pending_fullscreen {
rv.set_fullscreen(true);
}
rv
}
@@ -1441,6 +1454,10 @@ impl<W: LayoutElement> Column<W> {
if self.is_fullscreen {
assert_eq!(self.tiles.len(), 1);
}
for tile in &self.tiles {
assert_eq!(self.is_fullscreen, tile.window().is_pending_fullscreen());
}
}
fn toggle_width(&mut self) {
+47 -4
View File
@@ -15,6 +15,7 @@ mod input;
mod ipc;
mod layout;
mod niri;
mod protocols;
mod render_helpers;
mod screenshot_ui;
mod utils;
@@ -26,6 +27,8 @@ mod dummy_pw_utils;
mod pw_utils;
use std::ffi::OsString;
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::Command;
use std::{env, mem};
@@ -86,7 +89,7 @@ enum Sub {
}
#[derive(Subcommand)]
enum Msg {
pub enum Msg {
/// List connected outputs.
Outputs,
}
@@ -154,7 +157,44 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
info!("starting version {}", &version());
// Load the config.
let path = cli.config.or_else(default_config_path);
let mut config_created = false;
let path = cli.config.or_else(|| {
let default_path = default_config_path()?;
let default_parent = default_path.parent().unwrap();
if let Err(err) = fs::create_dir_all(default_parent) {
warn!(
"error creating config directories {:?}: {err:?}",
default_parent
);
return Some(default_path);
}
// Create the config and fill it with the default config if it doesn't exist.
let new_file = File::options()
.read(true)
.write(true)
.create_new(true)
.open(&default_path);
match new_file {
Ok(mut new_file) => {
let default = include_bytes!("../resources/default-config.kdl");
match new_file.write_all(default) {
Ok(()) => {
config_created = true;
info!("wrote default config to {:?}", &default_path);
}
Err(err) => {
warn!("error writing config file at {:?}: {err:?}", &default_path)
}
}
}
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
Err(err) => warn!("error creating config file at {:?}: {err:?}", &default_path),
}
Some(default_path)
});
let mut config_errored = false;
let mut config = path
@@ -180,7 +220,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
event_loop.handle(),
event_loop.get_signal(),
display,
);
)
.unwrap();
// Set WAYLAND_DISPLAY for children.
let socket_name = &state.niri.socket_name;
@@ -218,7 +259,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
};
// Set up config file watcher.
let _watcher = if let Some(path) = path {
let _watcher = if let Some(path) = path.clone() {
let (tx, rx) = calloop::channel::sync_channel(1);
let watcher = Watcher::new(path.clone(), tx);
event_loop
@@ -243,6 +284,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Show the config error notification right away if needed.
if config_errored {
state.niri.config_error_notification.show();
} else if config_created {
state.niri.config_error_notification.show_created(path);
}
// Run the compositor.
+102 -40
View File
@@ -103,6 +103,7 @@ use crate::hotkey_overlay::HotkeyOverlay;
use crate::input::{apply_libinput_settings, TabletData};
use crate::ipc::server::IpcServer;
use crate::layout::{Layout, MonitorRenderElement};
use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState};
use crate::pw_utils::{Cast, PipeWire};
use crate::render_helpers::NiriRenderer;
use crate::screenshot_ui::{ScreenshotUi, ScreenshotUiRenderElement};
@@ -151,6 +152,7 @@ pub struct Niri {
pub kde_decoration_state: KdeDecorationState,
pub layer_shell_state: WlrLayerShellState,
pub session_lock_state: SessionLockManagerState,
pub foreign_toplevel_state: ForeignToplevelManagerState,
pub shm_state: ShmState,
pub output_manager_state: OutputManagerState,
pub dmabuf_state: DmabufState,
@@ -296,7 +298,7 @@ impl State {
event_loop: LoopHandle<'static, State>,
stop_signal: LoopSignal,
display: Display<State>,
) -> Self {
) -> Result<Self, Box<dyn std::error::Error>> {
let _span = tracy_client::span!("State::new");
let config = Rc::new(RefCell::new(config));
@@ -305,15 +307,18 @@ impl State {
env::var_os("WAYLAND_DISPLAY").is_some() || env::var_os("DISPLAY").is_some();
let mut backend = if has_display {
Backend::Winit(Winit::new(config.clone(), event_loop.clone()))
let winit = Winit::new(config.clone(), event_loop.clone())?;
Backend::Winit(winit)
} else {
Backend::Tty(Tty::new(config.clone(), event_loop.clone()))
let tty = Tty::new(config.clone(), event_loop.clone())
.context("error initializing the TTY backend")?;
Backend::Tty(tty)
};
let mut niri = Niri::new(config.clone(), event_loop, stop_signal, display, &backend);
backend.init(&mut niri);
Self { backend, niri }
Ok(Self { backend, niri })
}
pub fn refresh_and_flush_clients(&mut self) {
@@ -327,6 +332,7 @@ impl State {
self.refresh_popup_grab();
self.update_keyboard_focus();
self.refresh_pointer_focus();
foreign_toplevel::refresh(self);
{
let _span = tracy_client::span!("flush_clients");
@@ -645,20 +651,26 @@ impl State {
let mut resized_outputs = vec![];
for output in self.niri.global_space.outputs() {
let name = output.name();
let scale = self
.niri
.config
.borrow()
.outputs
.iter()
.find(|o| o.name == name)
.map(|c| c.scale)
.unwrap_or(1.);
let config = self.niri.config.borrow_mut();
let config = config.outputs.iter().find(|o| o.name == name);
let scale = config.map(|c| c.scale).unwrap_or(1.);
let scale = scale.clamp(1., 10.).ceil() as i32;
if output.current_scale().integer_scale() != scale {
let mut transform = config
.map(|c| c.transform.into())
.unwrap_or(Transform::Normal);
// FIXME: fix winit damage on other transforms.
if name == "winit" {
transform = Transform::Flipped180;
}
if output.current_scale().integer_scale() != scale
|| output.current_transform() != transform
{
output.change_current_state(
None,
None,
Some(transform),
Some(output::Scale::Integer(scale)),
None,
);
@@ -855,6 +867,11 @@ impl Niri {
!client.get_data::<ClientState>().unwrap().restricted
});
let foreign_toplevel_state =
ForeignToplevelManagerState::new::<State, _>(&display_handle, |client| {
!client.get_data::<ClientState>().unwrap().restricted
});
let mut seat: Seat<State> = seat_state.new_wl_seat(&display_handle, backend.seat_name());
seat.add_keyboard(
config_.input.keyboard.xkb.to_xkb_config(),
@@ -972,6 +989,7 @@ impl Niri {
kde_decoration_state,
layer_shell_state,
session_lock_state,
foreign_toplevel_state,
text_input_state,
input_method_state,
virtual_keyboard_state,
@@ -1163,18 +1181,25 @@ impl Niri {
let global = output.create_global::<State>(&self.display_handle);
let name = output.name();
let scale = self
.config
.borrow()
.outputs
.iter()
.find(|o| o.name == name)
.map(|c| c.scale)
.unwrap_or(1.);
let scale = scale.clamp(1., 10.).ceil() as i32;
// Set scale before adding to the layout since that will read the output size.
output.change_current_state(None, None, Some(output::Scale::Integer(scale)), None);
let config = self.config.borrow();
let c = config.outputs.iter().find(|o| o.name == name);
let scale = c.map(|c| c.scale).unwrap_or(1.);
let scale = scale.clamp(1., 10.).ceil() as i32;
let mut transform = c.map(|c| c.transform.into()).unwrap_or(Transform::Normal);
// FIXME: fix winit damage on other transforms.
if name == "winit" {
transform = Transform::Flipped180;
}
drop(config);
// Set scale and transform before adding to the layout since that will read the output size.
output.change_current_state(
None,
Some(transform),
Some(output::Scale::Integer(scale)),
None,
);
self.layout.add_output(output.clone());
@@ -1291,14 +1316,16 @@ impl Niri {
}
// If the output size changed with an open screenshot UI, close the screenshot UI.
if let Some((old_size, old_scale)) = self.screenshot_ui.output_size(&output) {
let output_transform = output.current_transform();
if let Some((old_size, old_scale, old_transform)) = self.screenshot_ui.output_size(&output)
{
let transform = output.current_transform();
let output_mode = output.current_mode().unwrap();
let size = output_transform.transform_size(output_mode.size);
let size = transform.transform_size(output_mode.size);
let scale = output.current_scale().integer_scale();
// FIXME: scale changes shouldn't matter but they currently do since I haven't quite
// figured out how to draw the screenshot textures in physical coordinates.
if old_size != size || old_scale != scale {
// FIXME: scale changes and transform flips shouldn't matter but they currently do since
// I haven't quite figured out how to draw the screenshot textures in
// physical coordinates.
if old_size != size || old_scale != scale || old_transform != transform {
self.screenshot_ui.close();
self.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
@@ -1310,7 +1337,7 @@ impl Niri {
self.queue_redraw(output);
}
pub fn deactivate_monitors(&mut self, backend: &Backend) {
pub fn deactivate_monitors(&mut self, backend: &mut Backend) {
if !self.monitors_active {
return;
}
@@ -1319,7 +1346,7 @@ impl Niri {
backend.set_monitors_active(false);
}
pub fn activate_monitors(&mut self, backend: &Backend) {
pub fn activate_monitors(&mut self, backend: &mut Backend) {
if self.monitors_active {
return;
}
@@ -1738,7 +1765,9 @@ impl Niri {
// FIXME we basically need to pick the largest scale factor across the overlapping
// outputs, this is how it's usually done in clients as well.
let mut cursor_scale = 1;
let mut cursor_transform = Transform::Normal;
let mut dnd_scale = 1;
let mut dnd_transform = Transform::Normal;
for output in self.global_space.outputs() {
let geo = self.global_space.output_geometry(output).unwrap();
@@ -1746,6 +1775,9 @@ impl Niri {
if let Some(mut overlap) = geo.intersection(bbox) {
overlap.loc -= surface_pos;
cursor_scale = cursor_scale.max(output.current_scale().integer_scale());
// FIXME: using the largest overlapping or "primary" output transform would
// make more sense here.
cursor_transform = output.current_transform();
output_update(output, Some(overlap), surface);
} else {
output_update(output, None, surface);
@@ -1756,6 +1788,9 @@ impl Niri {
if let Some(mut overlap) = geo.intersection(bbox) {
overlap.loc -= surface_pos;
dnd_scale = dnd_scale.max(output.current_scale().integer_scale());
// FIXME: using the largest overlapping or "primary" output transform
// would make more sense here.
dnd_transform = output.current_transform();
output_update(output, Some(overlap), surface);
} else {
output_update(output, None, surface);
@@ -1764,11 +1799,11 @@ impl Niri {
}
with_states(surface, |data| {
send_surface_state(surface, data, cursor_scale, Transform::Normal);
send_surface_state(surface, data, cursor_scale, cursor_transform);
});
if let Some((surface, _)) = dnd {
with_states(surface, |data| {
send_surface_state(surface, data, dnd_scale, Transform::Normal);
send_surface_state(surface, data, dnd_scale, dnd_transform);
});
}
}
@@ -1785,6 +1820,7 @@ impl Niri {
};
let mut dnd_scale = 1;
let mut dnd_transform = Transform::Normal;
for output in self.global_space.outputs() {
let geo = self.global_space.output_geometry(output).unwrap();
@@ -1806,15 +1842,18 @@ impl Niri {
if let Some(mut overlap) = geo.intersection(bbox) {
overlap.loc -= surface_pos;
dnd_scale = dnd_scale.max(output.current_scale().integer_scale());
// FIXME: using the largest overlapping or "primary" output transform would
// make more sense here.
dnd_transform = output.current_transform();
output_update(output, Some(overlap), surface);
} else {
output_update(output, None, surface);
}
with_states(surface, |data| {
send_surface_state(surface, data, dnd_scale, Transform::Normal);
});
}
with_states(surface, |data| {
send_surface_state(surface, data, dnd_scale, dnd_transform);
});
}
}
}
@@ -2400,9 +2439,13 @@ impl Niri {
let _span = tracy_client::span!("Niri::render_for_screen_cast");
let size = output.current_mode().unwrap().size;
let transform = output.current_transform();
let size = transform.transform_size(size);
let scale = Scale::from(output.current_scale().fractional_scale());
let mut elements = None;
let mut casts_to_stop = vec![];
let mut casts = mem::take(&mut self.casts);
for cast in &mut casts {
@@ -2414,6 +2457,12 @@ impl Niri {
continue;
}
if cast.size != size {
debug!("stopping screencast due to output size change");
casts_to_stop.push(cast.session_id);
continue;
}
let last = cast.last_frame_time;
let min = cast.min_time_between_frames.get();
if last.is_zero() {
@@ -2468,6 +2517,10 @@ impl Niri {
cast.last_frame_time = target_presentation_time;
}
self.casts = casts;
for id in casts_to_stop {
self.stop_cast(id);
}
}
#[cfg(feature = "xdp-gnome-screencast")]
@@ -2522,6 +2575,9 @@ impl Niri {
.cloned()
.filter_map(|output| {
let size = output.current_mode().unwrap().size;
let transform = output.current_transform();
let size = transform.transform_size(size);
let scale = Scale::from(output.current_scale().fractional_scale());
let elements = self.render::<GlesRenderer>(renderer, &output, true);
@@ -2549,6 +2605,9 @@ impl Niri {
let _span = tracy_client::span!("Niri::screenshot");
let size = output.current_mode().unwrap().size;
let transform = output.current_transform();
let size = transform.transform_size(size);
let scale = Scale::from(output.current_scale().fractional_scale());
let elements = self.render::<GlesRenderer>(renderer, output, true);
let pixels = render_to_vec(renderer, size, scale, Fourcc::Abgr8888, &elements)?;
@@ -2670,6 +2729,9 @@ impl Niri {
let geom = geom.to_physical(output_scale);
let size = geom.size;
let transform = output.current_transform();
let size = transform.transform_size(size);
let elements = self.render::<GlesRenderer>(renderer, &output, include_pointer);
let pixels = render_to_vec(
renderer,
+466
View File
@@ -0,0 +1,466 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use arrayvec::ArrayVec;
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_protocols_wlr;
use smithay::reexports::wayland_server::backend::ClientId;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::{
ToplevelStateSet, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
};
use wayland_protocols_wlr::foreign_toplevel::v1::server::{
zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
};
use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
use crate::niri::State;
const VERSION: u32 = 3;
pub struct ForeignToplevelManagerState {
display: DisplayHandle,
instances: Vec<ZwlrForeignToplevelManagerV1>,
toplevels: HashMap<WlSurface, ToplevelData>,
}
pub trait ForeignToplevelHandler {
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState;
fn activate(&mut self, wl_surface: WlSurface);
fn close(&mut self, wl_surface: WlSurface);
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>);
fn unset_fullscreen(&mut self, wl_surface: WlSurface);
}
struct ToplevelData {
title: Option<String>,
app_id: Option<String>,
states: ArrayVec<u32, 3>,
output: Option<Output>,
instances: HashMap<ZwlrForeignToplevelHandleV1, Vec<WlOutput>>,
// FIXME: parent.
}
pub struct ForeignToplevelGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
impl ForeignToplevelManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = ForeignToplevelGlobalData {
filter: Box::new(filter),
};
display.create_global::<D, ZwlrForeignToplevelManagerV1, _>(VERSION, global_data);
Self {
display: display.clone(),
instances: Vec::new(),
toplevels: HashMap::new(),
}
}
}
pub fn refresh(state: &mut State) {
let _span = tracy_client::span!("foreign_toplevel::refresh");
let protocol_state = &mut state.niri.foreign_toplevel_state;
// Handle closed windows.
protocol_state.toplevels.retain(|surface, data| {
if state.niri.layout.find_window_and_output(surface).is_some() {
return true;
}
for instance in data.instances.keys() {
instance.closed();
}
false
});
// Handle new and existing windows.
//
// Save the focused window for last, this way when the focus changes, we will first deactivate
// the previous window and only then activate the newly focused window.
let mut focused = None;
state.niri.layout.with_windows(|window, output| {
let wl_surface = window.toplevel().wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
if state.niri.keyboard_focus.as_ref() == Some(wl_surface) {
focused = Some((window.clone(), output.cloned()));
} else {
refresh_toplevel(protocol_state, wl_surface, &role, output, false);
}
});
});
// Finally, refresh the focused window.
if let Some((window, output)) = focused {
let wl_surface = window.toplevel().wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
refresh_toplevel(protocol_state, wl_surface, &role, output.as_ref(), true);
});
}
}
pub fn on_output_bound(state: &mut State, output: &Output, wl_output: &WlOutput) {
let _span = tracy_client::span!("foreign_toplevel::on_output_bound");
let Some(client) = wl_output.client() else {
return;
};
let protocol_state = &mut state.niri.foreign_toplevel_state;
for data in protocol_state.toplevels.values_mut() {
if data.output.as_ref() != Some(output) {
continue;
}
for (instance, outputs) in &mut data.instances {
if instance.client().as_ref() != Some(&client) {
continue;
}
instance.output_enter(wl_output);
instance.done();
outputs.push(wl_output.clone());
}
}
}
fn refresh_toplevel(
protocol_state: &mut ForeignToplevelManagerState,
wl_surface: &WlSurface,
role: &XdgToplevelSurfaceRoleAttributes,
output: Option<&Output>,
has_focus: bool,
) {
let states = to_state_vec(&role.current.states, has_focus);
match protocol_state.toplevels.entry(wl_surface.clone()) {
Entry::Occupied(entry) => {
// Existing window, check if anything changed.
let data = entry.into_mut();
let mut new_title = None;
if data.title != role.title {
data.title = role.title.clone();
new_title = role.title.as_deref();
if new_title.is_none() {
error!("toplevel title changed to None");
}
}
let mut new_app_id = None;
if data.app_id != role.app_id {
data.app_id = role.app_id.clone();
new_app_id = role.app_id.as_deref();
if new_app_id.is_none() {
error!("toplevel app_id changed to None");
}
}
let mut states_changed = false;
if data.states != states {
data.states = states;
states_changed = true;
}
let mut output_changed = false;
if data.output.as_ref() != output {
data.output = output.cloned();
output_changed = true;
}
let something_changed =
new_title.is_some() || new_app_id.is_some() || states_changed || output_changed;
if something_changed {
for (instance, outputs) in &mut data.instances {
if let Some(new_title) = new_title {
instance.title(new_title.to_owned());
}
if let Some(new_app_id) = new_app_id {
instance.app_id(new_app_id.to_owned());
}
if states_changed {
instance.state(data.states.iter().flat_map(|x| x.to_ne_bytes()).collect());
}
if output_changed {
for wl_output in outputs.drain(..) {
instance.output_leave(&wl_output);
}
if let Some(output) = &data.output {
if let Some(client) = instance.client() {
for wl_output in output.client_outputs(&client) {
instance.output_enter(&wl_output);
outputs.push(wl_output);
}
}
}
}
instance.done();
}
}
for outputs in data.instances.values_mut() {
// Clean up dead wl_outputs.
outputs.retain(|x| x.is_alive());
}
}
Entry::Vacant(entry) => {
// New window, start tracking it.
let mut data = ToplevelData {
title: role.title.clone(),
app_id: role.app_id.clone(),
states,
output: output.cloned(),
instances: HashMap::new(),
};
for manager in &protocol_state.instances {
if let Some(client) = manager.client() {
data.add_instance::<State>(&protocol_state.display, &client, manager);
}
}
entry.insert(data);
}
}
}
impl ToplevelData {
fn add_instance<D>(
&mut self,
handle: &DisplayHandle,
client: &Client,
manager: &ZwlrForeignToplevelManagerV1,
) where
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
D: 'static,
{
let toplevel = client
.create_resource::<ZwlrForeignToplevelHandleV1, _, D>(handle, manager.version(), ())
.unwrap();
manager.toplevel(&toplevel);
if let Some(title) = &self.title {
toplevel.title(title.clone());
}
if let Some(app_id) = &self.app_id {
toplevel.app_id(app_id.clone());
}
toplevel.state(self.states.iter().flat_map(|x| x.to_ne_bytes()).collect());
let mut outputs = Vec::new();
if let Some(output) = &self.output {
for wl_output in output.client_outputs(client) {
toplevel.output_enter(&wl_output);
outputs.push(wl_output);
}
}
toplevel.done();
self.instances.insert(toplevel, outputs);
}
}
impl<D> GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData, D>
for ForeignToplevelManagerState
where
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
D: ForeignToplevelHandler,
{
fn bind(
state: &mut D,
handle: &DisplayHandle,
client: &Client,
resource: New<ZwlrForeignToplevelManagerV1>,
_global_data: &ForeignToplevelGlobalData,
data_init: &mut DataInit<'_, D>,
) {
let manager = data_init.init(resource, ());
let state = state.foreign_toplevel_manager_state();
for data in state.toplevels.values_mut() {
data.add_instance::<D>(handle, client, &manager);
}
state.instances.push(manager);
}
fn can_view(client: Client, global_data: &ForeignToplevelGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<ZwlrForeignToplevelManagerV1, (), D> for ForeignToplevelManagerState
where
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
D: ForeignToplevelHandler,
{
fn request(
state: &mut D,
_client: &Client,
resource: &ZwlrForeignToplevelManagerV1,
request: <ZwlrForeignToplevelManagerV1 as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
match request {
zwlr_foreign_toplevel_manager_v1::Request::Stop => {
resource.finished();
let state = state.foreign_toplevel_manager_state();
state.instances.retain(|x| x != resource);
}
_ => unreachable!(),
}
}
fn destroyed(
state: &mut D,
_client: ClientId,
resource: &ZwlrForeignToplevelManagerV1,
_data: &(),
) {
let state = state.foreign_toplevel_manager_state();
state.instances.retain(|x| x != resource);
}
}
impl<D> Dispatch<ZwlrForeignToplevelHandleV1, (), D> for ForeignToplevelManagerState
where
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
D: ForeignToplevelHandler,
{
fn request(
state: &mut D,
_client: &Client,
resource: &ZwlrForeignToplevelHandleV1,
request: <ZwlrForeignToplevelHandleV1 as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
let protocol_state = state.foreign_toplevel_manager_state();
let Some((surface, _)) = protocol_state
.toplevels
.iter()
.find(|(_, data)| data.instances.contains_key(resource))
else {
return;
};
let surface = surface.clone();
match request {
zwlr_foreign_toplevel_handle_v1::Request::SetMaximized => (),
zwlr_foreign_toplevel_handle_v1::Request::UnsetMaximized => (),
zwlr_foreign_toplevel_handle_v1::Request::SetMinimized => (),
zwlr_foreign_toplevel_handle_v1::Request::UnsetMinimized => (),
zwlr_foreign_toplevel_handle_v1::Request::Activate { .. } => {
state.activate(surface);
}
zwlr_foreign_toplevel_handle_v1::Request::Close => {
state.close(surface);
}
zwlr_foreign_toplevel_handle_v1::Request::SetRectangle { .. } => (),
zwlr_foreign_toplevel_handle_v1::Request::Destroy => (),
zwlr_foreign_toplevel_handle_v1::Request::SetFullscreen { output } => {
state.set_fullscreen(surface, output);
}
zwlr_foreign_toplevel_handle_v1::Request::UnsetFullscreen => {
state.unset_fullscreen(surface);
}
_ => unreachable!(),
}
}
fn destroyed(
state: &mut D,
_client: ClientId,
resource: &ZwlrForeignToplevelHandleV1,
_data: &(),
) {
let state = state.foreign_toplevel_manager_state();
for data in state.toplevels.values_mut() {
data.instances.retain(|instance, _| instance != resource);
}
}
}
fn to_state_vec(states: &ToplevelStateSet, has_focus: bool) -> ArrayVec<u32, 3> {
let mut rv = ArrayVec::new();
if states.contains(xdg_toplevel::State::Maximized) {
rv.push(zwlr_foreign_toplevel_handle_v1::State::Maximized as u32);
}
if states.contains(xdg_toplevel::State::Fullscreen) {
rv.push(zwlr_foreign_toplevel_handle_v1::State::Fullscreen as u32);
}
// HACK: wlr-foreign-toplevel-management states:
//
// These have the same meaning as the states with the same names defined in xdg-toplevel
//
// However, clients such as sfwbar and fcitx seem to treat the activated state as keyboard
// focus, i.e. they don't expect multiple windows to have it set at once. Even Waybar which
// handles multiple activated windows correctly uses it in its design in such a way that
// keyboard focus would make more sense. Let's do what the clients expect.
if has_focus {
rv.push(zwlr_foreign_toplevel_handle_v1::State::Activated as u32);
}
rv
}
#[macro_export]
macro_rules! delegate_foreign_toplevel {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: $crate::protocols::foreign_toplevel::ForeignToplevelGlobalData
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: ()
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: ()
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
};
}
+1
View File
@@ -0,0 +1 @@
pub mod foreign_toplevel;
+5
View File
@@ -27,6 +27,7 @@ use smithay::output::Output;
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{self, Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::gbm::Modifier;
use smithay::utils::{Physical, Size};
use zbus::SignalContext;
use crate::dbus::mutter_screen_cast::{self, CursorMode, ScreenCastToNiri};
@@ -43,6 +44,7 @@ pub struct Cast {
_listener: StreamListener<()>,
pub is_active: Rc<Cell<bool>>,
pub output: Output,
pub size: Size<i32, Physical>,
pub cursor_mode: CursorMode,
pub last_frame_time: Duration,
pub min_time_between_frames: Rc<Cell<Duration>>,
@@ -112,6 +114,8 @@ impl PipeWire {
let mode = output.current_mode().unwrap();
let size = mode.size;
let transform = output.current_transform();
let size = transform.transform_size(size);
let refresh = mode.refresh;
let stream = Stream::new(&self.core, "niri-screen-cast-src", Properties::new())
@@ -383,6 +387,7 @@ impl PipeWire {
_listener: listener,
is_active,
output,
size,
cursor_mode,
last_frame_time: Duration::ZERO,
min_time_between_frames,
+7 -4
View File
@@ -42,6 +42,7 @@ pub enum ScreenshotUi {
pub struct OutputData {
size: Size<i32, Physical>,
scale: i32,
transform: Transform,
texture: GlesTexture,
texture_buffer: TextureBuffer<GlesTexture>,
buffers: [SolidColorBuffer; 8],
@@ -94,6 +95,7 @@ impl ScreenshotUi {
)
}
};
let scale = selection.0.current_scale().integer_scale();
let selection = (
selection.0,
@@ -104,9 +106,9 @@ impl ScreenshotUi {
let output_data = screenshots
.into_iter()
.map(|(output, texture)| {
let output_transform = output.current_transform();
let transform = output.current_transform();
let output_mode = output.current_mode().unwrap();
let size = output_transform.transform_size(output_mode.size);
let size = transform.transform_size(output_mode.size);
let scale = output.current_scale().integer_scale();
let texture_buffer = TextureBuffer::from_texture(
renderer,
@@ -129,6 +131,7 @@ impl ScreenshotUi {
let data = OutputData {
size,
scale,
transform,
texture,
texture_buffer,
buffers,
@@ -333,10 +336,10 @@ impl ScreenshotUi {
}
}
pub fn output_size(&self, output: &Output) -> Option<(Size<i32, Physical>, i32)> {
pub fn output_size(&self, output: &Output) -> Option<(Size<i32, Physical>, i32, Transform)> {
if let Self::Open { output_data, .. } = self {
let data = output_data.get(output)?;
Some((data.size, data.scale))
Some((data.size, data.scale, data.transform))
} else {
None
}