Compare commits

...

121 Commits

Author SHA1 Message Date
Ivan Molodetskikh ae89cb6017 Update README.md 2025-05-17 15:59:05 +03:00
Ivan Molodetskikh b6fc4d0455 wiki/Overview: Link the new video 2025-05-17 14:38:24 +03:00
Federico Ceratto d8265ad34e Stop including broken LFS files in source tarball 2025-05-17 04:29:46 -07:00
Ivan Molodetskikh 3b864dc104 Bump version to 25.05 2025-05-17 13:50:36 +03:00
Ivan Molodetskikh 15093221ed wiki/Overview: Update wording 2025-05-17 13:44:55 +03:00
Ivan Molodetskikh ac7b3fbf19 wiki: Link to issue in numlock
https://github.com/YaLTeR/niri/issues/1501
2025-05-17 07:58:52 +03:00
Ivan Molodetskikh bb8eb377c7 Update dependencies more carefully
No winit deadlock in this update.
2025-05-16 22:54:37 +03:00
Ivan Molodetskikh 6169c0312a Revert "Update dependencies"
Something is causing winit deadlock on nested niri exit.

This reverts commit 2ae99224ab.

This reverts commit 0d6843ea67.
2025-05-16 22:53:54 +03:00
Ivan Molodetskikh 2ae99224ab Update dependencies 2025-05-16 22:29:09 +03:00
Ivan Molodetskikh 4f63e13385 Deal with new Clippy warnings 2025-05-16 22:21:14 +03:00
Ivan Molodetskikh 46a8f81160 ipc/client: Make compositor version check for JSON parsing errors
These can happen when adding new fields to returned structs.
2025-05-15 09:08:53 +03:00
Ivan Molodetskikh 0d6843ea67 Update dependencies 2025-05-13 17:13:35 +03:00
Ivan Molodetskikh 6d083ea497 layout: Fix workspace swipe to same workspace forgetting previous id
This manifested much more prominently in the overview.
2025-05-13 08:17:15 +03:00
Ivan Molodetskikh 7a42140d6c dependabot: Change to weekly
Let's see if this fixes it missing from the GitHub UI.
2025-05-12 20:05:33 +03:00
Ivan Molodetskikh eeb411bef5 wiki: Add Since for touchpad drag 2025-05-12 20:05:10 +03:00
Ivan Molodetskikh defd4c5c4d Add center-visible-columns action 2025-05-12 14:13:51 +03:00
dependabot[bot] 7227e64149 build(deps): bump clap in the rust-dependencies group
Bumps the rust-dependencies group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.5.37 to 4.5.38
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.37...clap_complete-v4.5.38)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.38
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 02:58:56 -07:00
Ivan Molodetskikh c98537a2b0 Implement baba-is-float for layers 2025-05-12 09:10:59 +03:00
Ivan Molodetskikh 9c103f1f1d Add missing "to" in comment 2025-05-12 08:26:39 +03:00
Ivan Molodetskikh 2aff1ec71a ipc/socket: Support multiple requests 2025-05-11 21:51:26 -07:00
Jon Heinritz 3466fc0a66 ipc: document the new socket behavior 2025-05-11 21:51:26 -07:00
Jon Heinritz f917932b3e ipc: support long living sockets 2025-05-11 21:51:26 -07:00
Ivan Molodetskikh 89b7423ee5 Print urgent status in niri msg windows 2025-05-10 23:43:00 +03:00
Ivan Molodetskikh a2efaf2816 Add is-urgent window rule matcher 2025-05-10 22:49:55 +03:00
Ivan Molodetskikh 5816691460 Add urgent color support to tab indicators 2025-05-10 22:42:45 +03:00
Ivan Molodetskikh 4b5e9e6cb0 wiki: Document urgent-color 2025-05-10 22:42:45 +03:00
Duncan Overbruck a8259b4cea add WindowUrgencyChanged ipc event 2025-05-10 12:14:41 -07:00
Duncan Overbruck 9d3d7cb0e9 add {toggle,set,unset}-urgent cli actions 2025-05-10 12:14:41 -07:00
Duncan Overbruck 398bc78ea0 add urgent border color and gradient 2025-05-10 12:14:41 -07:00
Duncan Overbruck caa6189448 add workspace urgency ipc event 2025-05-10 12:14:41 -07:00
Duncan Overbruck 86f57c2ec7 add window urgency through xdg-activation-v1
urgency is done through activation requests without a serial from a
previous interaction.

https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/150
2025-05-10 12:14:41 -07:00
Charlie Le 3cc67897af Implement IPC for the overview state (#1526)
* Implement IPC for the overview state

* Update Overview IPC to maintain naming consistency, renamed OverviewToggled to be more clear, simplify overview state request on the server, consolidate ipc refresh

* Fix Overview is_open in IPC client

* Change opened to is_open

* Update niri-ipc/src/lib.rs

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>

* Update niri-ipc/src/state.rs

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>

* Update src/ipc/client.rs

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>

* Update src/ipc/client.rs

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>

* Add overview state to EventStreamStatePart replicate and apply

* Fix formatting

* Rename Overview to OverviewState

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-05-09 18:01:01 +03:00
dependabot[bot] a99489c6c0 build(deps): bump clap_complete in the rust-dependencies group
Bumps the rust-dependencies group with 1 update: [clap_complete](https://github.com/clap-rs/clap).


Updates `clap_complete` from 4.5.49 to 4.5.50
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.49...clap_complete-v4.5.50)

---
updated-dependencies:
- dependency-name: clap_complete
  dependency-version: 4.5.50
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-09 07:38:04 -07:00
Ivan Molodetskikh 0763c7e196 Add a clickable button to capture the screenshot
Allows tablet-, touch- and mouse-only confirmation.
2025-05-09 15:42:23 +03:00
Ivan Molodetskikh fb5c5204e8 Extract confirm_screenshot() 2025-05-09 15:41:57 +03:00
Ivan Molodetskikh d207cd385b screenshot_ui: Refactor mouse down + touch slot state 2025-05-09 15:10:00 +03:00
Ivan Molodetskikh 99bf2df2b4 Silence new zvariant De/SerializeDict deprecations
Questionable exercise converting to serde with much more boilerplate, and
breaking compat with older zvariant versions. Plus maybe this will be
undeprecated back.
2025-05-09 10:35:16 +03:00
Ivan Molodetskikh 09be90f4e6 Add touch selection support to the screenshot UI 2025-05-09 10:28:20 +03:00
Ivan Molodetskikh dfc42b9d82 Split ScreenshotUi::pointer_down() and up() 2025-05-09 10:28:20 +03:00
Ivan Molodetskikh e2b9838d89 Extract evt.slot() 2025-05-09 10:28:20 +03:00
Ivan Molodetskikh 816a0d479c Rename touch_location to pos 2025-05-09 10:28:20 +03:00
Ivan Molodetskikh 84323d10a4 Support tablet input for screenshot UI selection 2025-05-09 10:28:20 +03:00
Ivan Molodetskikh b956f2775c Use early return 2025-05-09 10:28:20 +03:00
Ivan Molodetskikh 9ff2f83db0 Simplify ScreenshotUi::pointer_button() 2025-05-09 10:28:20 +03:00
James Sully 7a10f71ee5 refactor(main): eliminate a mut from config load code in main
I think this makes for marginally better readability, since you don't
have to wonder whether config_errored is set anywhere else. It's also
slightly terser.
2025-05-09 00:25:54 -07:00
James Sully ea7add3563 fix: don't try to create a default config at path that exists
Currently this bug has no actual consequences, we just continue silently
on AlreadyExists in main()
(this line: https://github.com/YaLTeR/niri/blob/e9c6f08906143c3fec1ad1301d538bef4cbc1978/src/main.rs#L151).

This commit just eliminates the redundant attempt.
2025-05-08 21:52:39 -07:00
Ivan Molodetskikh e9c6f08906 Add a resize transaction client-server test 2025-05-07 22:59:57 +03:00
Ivan Molodetskikh 17343a6740 wiki: Fix Until note location 2025-05-06 17:42:14 +03:00
Ivan Molodetskikh 140d726cd3 wiki: Clarify that layers within backdrop ignore input 2025-05-06 17:40:52 +03:00
Ivan Molodetskikh c37d3b3442 wiki: Link to output backdrop-color from overview {} 2025-05-06 17:34:40 +03:00
Ivan Molodetskikh 497f186422 Add layout background-color setting 2025-05-06 17:34:40 +03:00
Ivan Molodetskikh 3e31c134a6 Implement place-within-backdrop layer rule 2025-05-06 17:34:40 +03:00
Ivan Molodetskikh fe682938db Simplify exclusive focus on layer check 2025-05-06 17:34:40 +03:00
Ivan Molodetskikh 6142922ca4 wiki: Mention Overview behavior on layer-shell page 2025-05-06 17:34:40 +03:00
Ivan Molodetskikh 4b44fba14c wiki: Clarify FAQ question about border with background 2025-05-06 17:34:40 +03:00
dependabot[bot] 57639ca84c build(deps): bump the rust-dependencies group across 1 directory with 3 updates
Bumps the rust-dependencies group with 3 updates in the / directory: [clap_complete](https://github.com/clap-rs/clap), [glam](https://github.com/bitshifter/glam-rs) and [zbus](https://github.com/dbus2/zbus).


Updates `clap_complete` from 4.5.48 to 4.5.49
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.48...clap_complete-v4.5.49)

Updates `glam` from 0.30.2 to 0.30.3
- [Changelog](https://github.com/bitshifter/glam-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitshifter/glam-rs/compare/0.30.2...0.30.3)

Updates `zbus` from 5.5.0 to 5.6.0
- [Release notes](https://github.com/dbus2/zbus/releases)
- [Commits](https://github.com/dbus2/zbus/compare/zbus-5.5.0...zbus-5.6.0)

---
updated-dependencies:
- dependency-name: clap_complete
  dependency-version: 4.5.49
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: glam
  dependency-version: 0.30.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zbus
  dependency-version: 5.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-06 01:38:03 -07:00
Ivan Molodetskikh ec88aae77d wiki: Add Since to numlock 2025-05-05 20:29:55 +03:00
Ivan Molodetskikh 6c9705dd4b layout/scrolling: Update view offset on config update
Fix always-centering not applied right away. No other changes intended.
2025-05-01 21:37:34 +03:00
Aberter Yan eb590c5346 Implement --focus for MoveColumnToWorkspace/Up/Down 2025-05-01 11:06:34 -07:00
Ivan Molodetskikh 02baad91ac wiki: Clarify how key bindings are resolved 2025-05-01 16:28:28 +03:00
Ivan Molodetskikh 68589cd5a1 wiki: Remove "experimental" from custom shaders
They've been around for a while.
2025-05-01 11:19:38 +03:00
Ivan Molodetskikh f2c690802b Adjust the workspace shadow defaults some more 2025-05-01 11:04:47 +03:00
Ivan Molodetskikh 9d6037b94c Normalize workspace shadows to 1080 px tall screen, adjust defaults
Workspace gaps are dependent on screen size, so it makes sense to make shadows
depend on the screen size to, to avoid them filling more or less of the gap.
2025-05-01 10:33:53 +03:00
Ivan Molodetskikh 7b4cf094ef Draw workspace shadows behind all workspaces 2025-05-01 10:10:11 +03:00
Ivan Molodetskikh 446bc155ce Add workspace-shadow {} config to overview {} 2025-05-01 09:45:38 +03:00
Ivan Molodetskikh 3289324ce4 wiki: Use subheadings for overview settings 2025-05-01 09:36:39 +03:00
Ivan Molodetskikh 9fb02b9571 layout: Fix DnD scroll not stopping when interactive moving unfullscreen to floating 2025-04-30 20:32:56 +03:00
erdii 0e9496b01e chore(wiki): document numlock setting
Co-Authored-By: Ivan Molodetskikh <yalterz@gmail.com>
Signed-off-by: erdii <me@erdii.engineering>
2025-04-30 09:54:19 -07:00
erdii 82dabc21f3 feat: implement support to enable numlock at startup
Signed-off-by: erdii <me@erdii.engineering>
2025-04-30 09:54:19 -07:00
erdii 39b3d62873 chore: bump smithay 2025-04-30 09:54:19 -07:00
dependabot[bot] af080a03cd build(deps): bump the rust-dependencies group with 3 updates
Bumps the rust-dependencies group with 3 updates: [wayland-backend](https://github.com/smithay/wayland-rs), [insta](https://github.com/mitsuhiko/insta) and [wayland-client](https://github.com/smithay/wayland-rs).


Updates `wayland-backend` from 0.3.9 to 0.3.10
- [Release notes](https://github.com/smithay/wayland-rs/releases)
- [Changelog](https://github.com/Smithay/wayland-rs/blob/master/historical_changelog.md)
- [Commits](https://github.com/smithay/wayland-rs/commits)

Updates `insta` from 1.43.0 to 1.43.1
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.43.0...1.43.1)

Updates `wayland-client` from 0.31.9 to 0.31.10
- [Release notes](https://github.com/smithay/wayland-rs/releases)
- [Changelog](https://github.com/Smithay/wayland-rs/blob/master/historical_changelog.md)
- [Commits](https://github.com/smithay/wayland-rs/commits)

---
updated-dependencies:
- dependency-name: wayland-backend
  dependency-version: 0.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: insta
  dependency-version: 1.43.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: wayland-client
  dependency-version: 0.31.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-30 01:46:05 -07:00
Ivan Molodetskikh 5f117c61dc animation/spring: Guard against numerical instability 2025-04-29 10:51:53 +03:00
Christian Meissl cb857e32e4 Bump Smithay and others
Presentation subsurface fix, popup unconstrain resize fix, cursor shape fix, refactors.
2025-04-29 08:53:25 +03:00
dependabot[bot] 199be26947 build(deps): bump the rust-dependencies group across 1 directory with 5 updates
Bumps the rust-dependencies group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.97` | `1.0.98` |
| [clap](https://github.com/clap-rs/clap) | `4.5.34` | `4.5.37` |
| [glam](https://github.com/bitshifter/glam-rs) | `0.30.1` | `0.30.2` |
| [libc](https://github.com/rust-lang/libc) | `0.2.171` | `0.2.172` |
| [insta](https://github.com/mitsuhiko/insta) | `1.42.2` | `1.43.0` |



Updates `anyhow` from 1.0.97 to 1.0.98
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.97...1.0.98)

Updates `clap` from 4.5.34 to 4.5.37
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.34...clap_complete-v4.5.37)

Updates `glam` from 0.30.1 to 0.30.2
- [Changelog](https://github.com/bitshifter/glam-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitshifter/glam-rs/compare/0.30.1...0.30.2)

Updates `libc` from 0.2.171 to 0.2.172
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.172/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.171...0.2.172)

Updates `insta` from 1.42.2 to 1.43.0
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.42.2...1.43.0)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.98
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: clap
  dependency-version: 4.5.37
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: glam
  dependency-version: 0.30.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libc
  dependency-version: 0.2.172
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: insta
  dependency-version: 1.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-28 22:35:11 -07:00
Ivan Molodetskikh d5c0c74d2c Fix hot corners preventing focus even when disabled 2025-04-29 08:24:45 +03:00
Ivan Molodetskikh 9bb292ec82 default-config: Set repeat=off for the Overview bind 2025-04-28 12:05:55 +03:00
Ivan Molodetskikh a1ba6bcaa0 wiki: Update backdrop-color examples 2025-04-28 09:32:44 +03:00
Ivan Molodetskikh fd389af6d8 Add backdrop-color setting to overview {} 2025-04-28 09:14:43 +03:00
Ivan Molodetskikh db09727b18 Replace Smithay's SolidColor elements with ours
Must've forgotten about these back when I replaced others.
2025-04-28 09:05:55 +03:00
Ivan Molodetskikh c9d6478c3c wiki: Rename Configuration: Overview page to Introduction 2025-04-28 07:54:02 +03:00
bogdanov 758cca5432 Fix pointer hiding so that it is no longer annoying (#1426)
* replace `pointer_hidden` with `pointer_visiblity`

* disable hidden pointer after content underneath has changed

* fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2025-04-27 06:25:36 +00:00
Ivan Molodetskikh 78e3daf5f8 overview: Activate window upon dropping from interactive move 2025-04-26 13:29:36 +03:00
Mitchel Stewart a99a0b2492 Steam Black Screen system-composer 2025-04-26 00:38:48 -07:00
Ivan Molodetskikh bfd42c74f4 layout/tab_indicator: Fix negative gap
Regressed in a recent commit that added max1.
2025-04-26 09:25:31 +03:00
Ivan Molodetskikh 501ea47128 wiki/Overview: Mention backdrop-color 2025-04-25 17:56:17 +03:00
Ivan Molodetskikh d2a1cf53b4 Fix panic when interactively moving to invisible workspace
Introduced in the interactive move between workspaces commit.
2025-04-25 16:55:36 +03:00
Ivan Molodetskikh 62d47d77d5 wiki: Document backdrop-color and overview-open-close animation 2025-04-25 15:29:42 +03:00
Ivan Molodetskikh 85cd64e830 Document the Overview and other new things 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 55c14eebf2 hotkey_overlay: Show the ToggleOverview bind 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 3fe67549b4 default-config: Bind Mod+O to toggle-overview 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 1835b532d9 Implement interactive move to a new workspace above/between 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh e6d82d3ee3 Implement top-left hot corner to toggle the Overview
Compared to third-party implementations such as waycorner:

- It works during interactive window move (no surfaces receive pointer
  focus in this case, so this cannot work through layer-shell).
- It works during drag-and-drop.
- It disables itself over fullscreen windows.
- It does not prevent direct scanout.
2025-04-25 02:00:18 -07:00
Ivan Molodetskikh fae3a27641 Implement DnD hold to activate window or workspace 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 31e76cf451 overview: Add DnD up/down scrolling 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh b8a9be542f overview: Add touchscreen gestures 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 59de6918b3 overview: Add two-finger touchpad scroll 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh bd3d554389 overview: Add hardcoded mouse scroll binds 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh af1fca35bb Implement an Overview 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 9571d149b2 Render workspaces separately with gaps between
This design makes more sense spatially, and is required for the
Overview. Gaps also make it clear how clipping windows to workspace
bounds works.

Background and bottom layer-shell surfaces get duplicated for each
workspace, while top and overlay stay "on top".
2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 99358e36b3 layout/monitor: Extract activate_workspace_with_anim_config() 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 8b878f355f Put interactively moved window on top of background and bottom layer popups 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 395b6d9a4f layout: Extract interactive_moved_window_under() and add output check
Fixes interactively moved window getting input on every output rather
than just its own.
2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 25f24f668c Extract mapped_hit_data() 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 929eaf0d69 Pass target workspace to view offset grab 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh ce3103949f layout/scrolling: Support view offset anim during gesture
Brings back moving the newly active window into focus upon interactive
move dragging out.
2025-04-25 02:00:18 -07:00
Ivan Molodetskikh ef60dd81d7 layout/monitor: Cache scale, view_size, working_area 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 7671a5d833 layout/monitor: Don't consider workspace switch in active_tile_visual_rectangle()
This only did something when in the middle of a touchpad gesture, and it
didn't really make sense for that edge case.
2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 3f09352067 layout/monitor: Extract add_workspace_at() 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 5059cce886 Add with_alpha() to shader and shadow element 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh b20dd226c0 layout: Move insert hint from ScrollingSpace to Monitor 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh acb69c3b4d layout: Return floating and scrolling elems separately from Workspace 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh dbe0a9e293 layout/tab_indicator: Use round_max1 where appropriate 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh d3a79faeec layout/monitor: Extract workspace_render_idx() 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 21630ddb5e layout/monitor: Extract workspaces_render_geo() 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 9e5e0c85bb Simplify condition 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 5cd8040d1a Extract is_layout_obscured_under() 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 86351938f2 Put the top layer above bottom and background layer popups
Makes it consistent with how window popups are below the top layer, also
will make more sense for the overview.
2025-04-25 02:00:18 -07:00
Ivan Molodetskikh ee4c5e23ab Reformat scroll factor computation 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh ffd6acc0aa layout/monitor: Extract WorkspaceSwitchGesture::min_max() 2025-04-25 02:00:18 -07:00
Ivan Molodetskikh cee11dc329 layout/monitor: Keep track of workspace switch gesture start idx
Fixes jump when "catching" an animation with a gesture.
2025-04-25 02:00:18 -07:00
Ivan Molodetskikh 59a42249a4 Remove cancellation from swipe gestures
It only worked for workspace switch, and even there it was more confusing than
helpful.
2025-04-25 02:00:18 -07:00
80 changed files with 6117 additions and 1545 deletions
+11
View File
@@ -1 +1,12 @@
# LFS configuration for images from the wiki
*.png filter=lfs diff=lfs merge=lfs -text
# Exclude LFS-tracked files from the tarball
/wiki/img/ export-ignore
# exclude .gitattributes itself from the tarball
.gitattributes export-ignore
# tip: can be tested using
# git archive --format=tar.gz --output=source.tar.gz HEAD && \
# tar tfvz source.tar.gz | grep -e '.png' -e '.gitattributes'
+2 -2
View File
@@ -3,7 +3,7 @@ updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
groups:
smithay:
patterns:
@@ -17,6 +17,6 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
ignore:
- dependency-name: "Andrew-Chen-Wang/github-wiki-action"
Generated
+254 -237
View File
File diff suppressed because it is too large Load Diff
+16 -16
View File
@@ -6,7 +6,7 @@ members = [
]
[workspace.package]
version = "25.2.0"
version = "25.5.0"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -15,10 +15,10 @@ repository = "https://github.com/YaLTeR/niri"
rust-version = "1.80.1"
[workspace.dependencies]
anyhow = "1.0.97"
bitflags = "2.9.0"
clap = { version = "4.5.34", features = ["derive"] }
insta = "1.42.2"
anyhow = "1.0.98"
bitflags = "2.9.1"
clap = { version = "4.5.38", features = ["derive"] }
insta = "1.43.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] }
@@ -56,26 +56,26 @@ async-channel = "2.3.1"
async-io = { version = "2.4.0", optional = true }
atomic = "0.6.0"
bitflags.workspace = true
bytemuck = { version = "1.22.0", features = ["derive"] }
bytemuck = { version = "1.23.0", features = ["derive"] }
calloop = { version = "0.14.2", features = ["executor", "futures-io"] }
clap = { workspace = true, features = ["string"] }
clap_complete = "4.5.47"
clap_complete = "4.5.50"
directories = "6.0.0"
drm-ffi = "0.9.0"
fastrand = "2.3.0"
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.30.1"
glam = "0.30.3"
input = { version = "0.9.1", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.171"
libc = "0.2.172"
libdisplay-info = "0.2.2"
log = { version = "0.4.27", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "25.2.0", path = "niri-config" }
niri-ipc = { version = "25.2.0", path = "niri-ipc", features = ["clap"] }
niri-config = { version = "25.5.0", path = "niri-config" }
niri-ipc = { version = "25.5.0", path = "niri-ipc", features = ["clap"] }
ordered-float = "5.0.0"
pango = { version = "0.20.9", features = ["v1_44"] }
pangocairo = "0.20.7"
pango = { version = "0.20.10", features = ["v1_44"] }
pangocairo = "0.20.10"
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
png = "0.17.16"
portable-atomic = { version = "1.11.0", default-features = false, features = ["float"] }
@@ -88,10 +88,10 @@ tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
url = { version = "2.5.4", optional = true }
wayland-backend = "0.3.8"
wayland-backend = "0.3.10"
wayland-scanner = "0.31.6"
xcursor = "0.3.8"
zbus = { version = "5.5.0", optional = true }
zbus = { version = "5.7.0", optional = true }
[dependencies.smithay]
workspace = true
@@ -118,7 +118,7 @@ insta.workspace = true
proptest = "1.6.0"
proptest-derive = { version = "0.5.1", features = ["boxed_union"] }
rayon = "1.10.0"
wayland-client = "0.31.8"
wayland-client = "0.31.10"
xshell = "0.2.7"
[features]
+7 -5
View File
@@ -10,7 +10,7 @@
<a href="https://github.com/YaLTeR/niri/wiki/Getting-Started">Getting Started</a> | <a href="https://github.com/YaLTeR/niri/wiki/Configuration:-Overview">Configuration</a> | <a href="https://github.com/YaLTeR/niri/discussions/325">Setup&nbsp;Showcase</a>
</p>
![niri with a few windows open](https://github.com/user-attachments/assets/d142e57d-a25d-4ddb-ab46-311417458211)
![niri with a few windows open](https://github.com/user-attachments/assets/535e6530-2f44-4b84-a883-1240a3eee6e9)
## About
@@ -30,9 +30,11 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
- Built from the ground up for scrollable tiling
- [Dynamic workspaces](https://github.com/YaLTeR/niri/wiki/Workspaces) like in GNOME
- An [Overview](https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995) that zooms out workspaces and windows
- Built-in screenshot UI
- Monitor and window screencasting through xdg-desktop-portal-gnome
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
- [Dynamic cast target](https://github.com/YaLTeR/niri/wiki/Screencasting#dynamic-screencast-target) that can change what it shows on the go
- [Touchpad](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/YaLTeR/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
- Group windows into [tabs](https://github.com/YaLTeR/niri/wiki/Tabs)
- Configurable layout: gaps, borders, struts, window sizes
@@ -72,7 +74,7 @@ I've seen someone use it fine on an Eee PC 900 from 2008, of all things.
- Discord and other Electron apps: work well through xwayland-satellite.
- Chromium and VSCode: work perfectly natively on Wayland with the right flags.
- X11 apps that want to position windows or bars at specific screen coordinates: won't work well; you can run them in a nested compositor like [labwc](https://github.com/YaLTeR/niri/wiki/Xwayland#using-the-labwc-wayland-compositor) or [rootful Xwayland](https://github.com/YaLTeR/niri/wiki/Xwayland#directly-running-xwayland-in-rootful-mode).
- Display scaling (integer or fractional) will make X11 apps look blurry; this needs to be supported in xwayland-satellite.
- Display scaling (integer or fractional) keeps X11 apps crisp, but you need the latest xwayland-satellite.
For games, you can run them in [gamescope] at native resolution, even with display scaling.
## Inspiration
@@ -88,8 +90,8 @@ Here are some other projects which implement a similar workflow:
- [PaperWM]: scrollable tiling on top of GNOME Shell.
- [karousel]: scrollable tiling on top of KDE.
- [papersway]: scrollable tiling on top of sway/i3.
- [hyprscroller] and [hyprslidr]: scrollable tiling on top of Hyprland.
- [scroll](https://github.com/dawsers/scroll) and [papersway]: scrollable tiling on top of sway/i3.
- [hyprscrolling] and [hyprslidr]: scrollable tiling on top of Hyprland.
- [PaperWM.spoon]: scrollable tiling on top of macOS.
## Media
@@ -108,7 +110,7 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
[fuzzel]: https://codeberg.org/dnkl/fuzzel
[karousel]: https://github.com/peterfajdiga/karousel
[papersway]: https://spwhitton.name/tech/code/papersway/
[hyprscroller]: https://github.com/dawsers/hyprscroller
[hyprscrolling]: https://github.com/hyprwm/hyprland-plugins/tree/main/hyprscrolling
[hyprslidr]: https://gitlab.com/magus/hyprslidr
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
[Matrix channel]: https://matrix.to/#/#niri:matrix.org
+1 -1
View File
@@ -12,7 +12,7 @@ bitflags.workspace = true
csscolorparser = "0.7.0"
knuffel = "3.2.0"
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
niri-ipc = { version = "25.2.0", path = "../niri-ipc" }
niri-ipc = { version = "25.5.0", path = "../niri-ipc" }
regex = "1.11.1"
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
+4
View File
@@ -15,6 +15,10 @@ pub struct LayerRule {
pub shadow: ShadowRule,
#[knuffel(child)]
pub geometry_corner_radius: Option<CornerRadius>,
#[knuffel(child, unwrap(argument))]
pub place_within_backdrop: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub baba_is_float: Option<bool>,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
+336 -17
View File
@@ -23,7 +23,8 @@ use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
use smithay::input::keyboard::{Keysym, XkbConfig};
use smithay::reexports::input;
pub const DEFAULT_BACKGROUND_COLOR: Color = Color::from_array_unpremul([0.2, 0.2, 0.2, 1.]);
pub const DEFAULT_BACKGROUND_COLOR: Color = Color::from_array_unpremul([0.25, 0.25, 0.25, 1.]);
pub const DEFAULT_BACKDROP_COLOR: Color = Color::from_array_unpremul([0.15, 0.15, 0.15, 1.]);
pub mod layer_rule;
@@ -61,6 +62,8 @@ pub struct Config {
#[knuffel(child, default)]
pub gestures: Gestures,
#[knuffel(child, default)]
pub overview: Overview,
#[knuffel(child, default)]
pub environment: Environment,
#[knuffel(children(name = "window-rule"))]
pub window_rules: Vec<WindowRule>,
@@ -117,6 +120,8 @@ pub struct Keyboard {
pub repeat_rate: u8,
#[knuffel(child, unwrap(argument), default)]
pub track_layout: TrackLayout,
#[knuffel(child)]
pub numlock: bool,
}
impl Default for Keyboard {
@@ -126,6 +131,7 @@ impl Default for Keyboard {
repeat_delay: 600,
repeat_rate: 25,
track_layout: Default::default(),
numlock: Default::default(),
}
}
}
@@ -442,8 +448,10 @@ pub struct Output {
pub variable_refresh_rate: Option<Vrr>,
#[knuffel(child)]
pub focus_at_startup: bool,
#[knuffel(child, default = DEFAULT_BACKGROUND_COLOR)]
pub background_color: Color,
#[knuffel(child)]
pub background_color: Option<Color>,
#[knuffel(child)]
pub backdrop_color: Option<Color>,
}
impl Output {
@@ -471,7 +479,8 @@ impl Default for Output {
position: None,
mode: None,
variable_refresh_rate: None,
background_color: DEFAULT_BACKGROUND_COLOR,
background_color: None,
backdrop_color: None,
}
}
}
@@ -532,6 +541,8 @@ pub struct Layout {
pub gaps: FloatOrInt<0, 65535>,
#[knuffel(child, default)]
pub struts: Struts,
#[knuffel(child, default = DEFAULT_BACKGROUND_COLOR)]
pub background_color: Color,
}
impl Default for Layout {
@@ -551,6 +562,7 @@ impl Default for Layout {
gaps: FloatOrInt(16.),
struts: Default::default(),
preset_window_heights: Default::default(),
background_color: DEFAULT_BACKGROUND_COLOR,
}
}
}
@@ -571,10 +583,14 @@ pub struct FocusRing {
pub active_color: Color,
#[knuffel(child, default = Self::default().inactive_color)]
pub inactive_color: Color,
#[knuffel(child, default = Self::default().urgent_color)]
pub urgent_color: Color,
#[knuffel(child)]
pub active_gradient: Option<Gradient>,
#[knuffel(child)]
pub inactive_gradient: Option<Gradient>,
#[knuffel(child)]
pub urgent_gradient: Option<Gradient>,
}
impl Default for FocusRing {
@@ -584,8 +600,10 @@ impl Default for FocusRing {
width: FloatOrInt(4.),
active_color: Color::from_rgba8_unpremul(127, 200, 255, 255),
inactive_color: Color::from_rgba8_unpremul(80, 80, 80, 255),
urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255),
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
}
}
}
@@ -657,10 +675,14 @@ pub struct Border {
pub active_color: Color,
#[knuffel(child, default = Self::default().inactive_color)]
pub inactive_color: Color,
#[knuffel(child, default = Self::default().urgent_color)]
pub urgent_color: Color,
#[knuffel(child)]
pub active_gradient: Option<Gradient>,
#[knuffel(child)]
pub inactive_gradient: Option<Gradient>,
#[knuffel(child)]
pub urgent_gradient: Option<Gradient>,
}
impl Default for Border {
@@ -670,8 +692,10 @@ impl Default for Border {
width: FloatOrInt(4.),
active_color: Color::from_rgba8_unpremul(255, 200, 127, 255),
inactive_color: Color::from_rgba8_unpremul(80, 80, 80, 255),
urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255),
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
}
}
}
@@ -683,8 +707,10 @@ impl From<Border> for FocusRing {
width: value.width,
active_color: value.active_color,
inactive_color: value.inactive_color,
urgent_color: value.urgent_color,
active_gradient: value.active_gradient,
inactive_gradient: value.inactive_gradient,
urgent_gradient: value.urgent_gradient,
}
}
}
@@ -696,8 +722,10 @@ impl From<FocusRing> for Border {
width: value.width,
active_color: value.active_color,
inactive_color: value.inactive_color,
urgent_color: value.urgent_color,
active_gradient: value.active_gradient,
inactive_gradient: value.inactive_gradient,
urgent_gradient: value.urgent_gradient,
}
}
}
@@ -745,6 +773,49 @@ pub struct ShadowOffset {
pub y: FloatOrInt<-65535, 65535>,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct WorkspaceShadow {
#[knuffel(child)]
pub off: bool,
#[knuffel(child, default = Self::default().offset)]
pub offset: ShadowOffset,
#[knuffel(child, unwrap(argument), default = Self::default().softness)]
pub softness: FloatOrInt<0, 1024>,
#[knuffel(child, unwrap(argument), default = Self::default().spread)]
pub spread: FloatOrInt<-1024, 1024>,
#[knuffel(child, default = Self::default().color)]
pub color: Color,
}
impl Default for WorkspaceShadow {
fn default() -> Self {
Self {
off: false,
offset: ShadowOffset {
x: FloatOrInt(0.),
y: FloatOrInt(10.),
},
softness: FloatOrInt(40.),
spread: FloatOrInt(10.),
color: Color::from_rgba8_unpremul(0, 0, 0, 0x50),
}
}
}
impl From<WorkspaceShadow> for Shadow {
fn from(value: WorkspaceShadow) -> Self {
Self {
on: !value.off,
offset: value.offset,
softness: value.softness,
spread: value.spread,
draw_behind_window: false,
color: value.color,
inactive_color: None,
}
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct TabIndicator {
#[knuffel(child)]
@@ -770,9 +841,13 @@ pub struct TabIndicator {
#[knuffel(child)]
pub inactive_color: Option<Color>,
#[knuffel(child)]
pub urgent_color: Option<Color>,
#[knuffel(child)]
pub active_gradient: Option<Gradient>,
#[knuffel(child)]
pub inactive_gradient: Option<Gradient>,
#[knuffel(child)]
pub urgent_gradient: Option<Gradient>,
}
impl Default for TabIndicator {
@@ -791,8 +866,10 @@ impl Default for TabIndicator {
corner_radius: FloatOrInt(0.),
active_color: None,
inactive_color: None,
urgent_color: None,
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
}
}
}
@@ -984,6 +1061,8 @@ pub struct Animations {
pub config_notification_open_close: ConfigNotificationOpenCloseAnim,
#[knuffel(child, default)]
pub screenshot_ui_open: ScreenshotUiOpenAnim,
#[knuffel(child, default)]
pub overview_open_close: OverviewOpenCloseAnim,
}
impl Default for Animations {
@@ -999,6 +1078,7 @@ impl Default for Animations {
window_resize: Default::default(),
config_notification_open_close: Default::default(),
screenshot_ui_open: Default::default(),
overview_open_close: Default::default(),
}
}
}
@@ -1146,6 +1226,22 @@ impl Default for ScreenshotUiOpenAnim {
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OverviewOpenCloseAnim(pub Animation);
impl Default for OverviewOpenCloseAnim {
fn default() -> Self {
Self(Animation {
off: false,
kind: AnimationKind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 800,
epsilon: 0.0001,
}),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Animation {
pub off: bool,
@@ -1183,6 +1279,10 @@ pub struct SpringParams {
pub struct Gestures {
#[knuffel(child, default)]
pub dnd_edge_view_scroll: DndEdgeViewScroll,
#[knuffel(child, default)]
pub dnd_edge_workspace_switch: DndEdgeWorkspaceSwitch,
#[knuffel(child, default)]
pub hot_corners: HotCorners,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
@@ -1205,6 +1305,52 @@ impl Default for DndEdgeViewScroll {
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct DndEdgeWorkspaceSwitch {
#[knuffel(child, unwrap(argument), default = Self::default().trigger_height)]
pub trigger_height: FloatOrInt<0, 65535>,
#[knuffel(child, unwrap(argument), default = Self::default().delay_ms)]
pub delay_ms: u16,
#[knuffel(child, unwrap(argument), default = Self::default().max_speed)]
pub max_speed: FloatOrInt<0, 1_000_000>,
}
impl Default for DndEdgeWorkspaceSwitch {
fn default() -> Self {
Self {
trigger_height: FloatOrInt(50.),
delay_ms: 100,
max_speed: FloatOrInt(1500.),
}
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct HotCorners {
#[knuffel(child)]
pub off: bool,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct Overview {
#[knuffel(child, unwrap(argument), default = Self::default().zoom)]
pub zoom: FloatOrInt<0, 1>,
#[knuffel(child, default = Self::default().backdrop_color)]
pub backdrop_color: Color,
#[knuffel(child, default)]
pub workspace_shadow: WorkspaceShadow,
}
impl Default for Overview {
fn default() -> Self {
Self {
zoom: FloatOrInt(0.5),
backdrop_color: DEFAULT_BACKDROP_COLOR,
workspace_shadow: WorkspaceShadow::default(),
}
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq, Eq)]
pub struct Environment(#[knuffel(children)] pub Vec<EnvironmentVariable>);
@@ -1311,6 +1457,8 @@ pub struct Match {
#[knuffel(property)]
pub is_window_cast_target: Option<bool>,
#[knuffel(property)]
pub is_urgent: Option<bool>,
#[knuffel(property)]
pub at_startup: Option<bool>,
}
@@ -1363,9 +1511,13 @@ pub struct BorderRule {
#[knuffel(child)]
pub inactive_color: Option<Color>,
#[knuffel(child)]
pub urgent_color: Option<Color>,
#[knuffel(child)]
pub active_gradient: Option<Gradient>,
#[knuffel(child)]
pub inactive_gradient: Option<Gradient>,
#[knuffel(child)]
pub urgent_gradient: Option<Gradient>,
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
@@ -1395,9 +1547,13 @@ pub struct TabIndicatorRule {
#[knuffel(child)]
pub inactive_color: Option<Color>,
#[knuffel(child)]
pub urgent_color: Option<Color>,
#[knuffel(child)]
pub active_gradient: Option<Gradient>,
#[knuffel(child)]
pub inactive_gradient: Option<Gradient>,
#[knuffel(child)]
pub urgent_gradient: Option<Gradient>,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
@@ -1540,7 +1696,11 @@ pub enum Action {
FocusWindowInColumn(#[knuffel(argument)] u8),
FocusWindowPrevious,
FocusColumnLeft,
#[knuffel(skip)]
FocusColumnLeftUnderMouse,
FocusColumnRight,
#[knuffel(skip)]
FocusColumnRightUnderMouse,
FocusColumnFirst,
FocusColumnLast,
FocusColumnRightOrFirst,
@@ -1589,8 +1749,13 @@ pub enum Action {
CenterWindow,
#[knuffel(skip)]
CenterWindowById(u64),
CenterVisibleColumns,
FocusWorkspaceDown,
#[knuffel(skip)]
FocusWorkspaceDownUnderMouse,
FocusWorkspaceUp,
#[knuffel(skip)]
FocusWorkspaceUpUnderMouse,
FocusWorkspace(#[knuffel(argument)] WorkspaceReference),
FocusWorkspacePrevious,
MoveWindowToWorkspaceDown,
@@ -1605,9 +1770,12 @@ pub enum Action {
reference: WorkspaceReference,
focus: bool,
},
MoveColumnToWorkspaceDown,
MoveColumnToWorkspaceUp,
MoveColumnToWorkspace(#[knuffel(argument)] WorkspaceReference),
MoveColumnToWorkspaceDown(#[knuffel(property(name = "focus"), default = true)] bool),
MoveColumnToWorkspaceUp(#[knuffel(property(name = "focus"), default = true)] bool),
MoveColumnToWorkspace(
#[knuffel(argument)] WorkspaceReference,
#[knuffel(property(name = "focus"), default = true)] bool,
),
MoveWorkspaceDown,
MoveWorkspaceUp,
MoveWorkspaceToIndex(#[knuffel(argument)] usize),
@@ -1716,6 +1884,15 @@ pub enum Action {
SetDynamicCastWindowById(u64),
SetDynamicCastMonitor(#[knuffel(argument)] Option<String>),
ClearDynamicCastTarget,
ToggleOverview,
OpenOverview,
CloseOverview,
#[knuffel(skip)]
ToggleUrgent(u64),
#[knuffel(skip)]
SetUrgent(u64),
#[knuffel(skip)]
UnsetUrgent(u64),
}
impl From<niri_ipc::Action> for Action {
@@ -1816,6 +1993,7 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::CenterColumn {} => Self::CenterColumn,
niri_ipc::Action::CenterWindow { id: None } => Self::CenterWindow,
niri_ipc::Action::CenterWindow { id: Some(id) } => Self::CenterWindowById(id),
niri_ipc::Action::CenterVisibleColumns {} => Self::CenterVisibleColumns,
niri_ipc::Action::FocusWorkspaceDown {} => Self::FocusWorkspaceDown,
niri_ipc::Action::FocusWorkspaceUp {} => Self::FocusWorkspaceUp,
niri_ipc::Action::FocusWorkspace { reference } => {
@@ -1838,10 +2016,14 @@ impl From<niri_ipc::Action> for Action {
reference: WorkspaceReference::from(reference),
focus,
},
niri_ipc::Action::MoveColumnToWorkspaceDown {} => Self::MoveColumnToWorkspaceDown,
niri_ipc::Action::MoveColumnToWorkspaceUp {} => Self::MoveColumnToWorkspaceUp,
niri_ipc::Action::MoveColumnToWorkspace { reference } => {
Self::MoveColumnToWorkspace(WorkspaceReference::from(reference))
niri_ipc::Action::MoveColumnToWorkspaceDown { focus } => {
Self::MoveColumnToWorkspaceDown(focus)
}
niri_ipc::Action::MoveColumnToWorkspaceUp { focus } => {
Self::MoveColumnToWorkspaceUp(focus)
}
niri_ipc::Action::MoveColumnToWorkspace { reference, focus } => {
Self::MoveColumnToWorkspace(WorkspaceReference::from(reference), focus)
}
niri_ipc::Action::MoveWorkspaceDown {} => Self::MoveWorkspaceDown,
niri_ipc::Action::MoveWorkspaceUp {} => Self::MoveWorkspaceUp,
@@ -1980,6 +2162,12 @@ impl From<niri_ipc::Action> for Action {
Self::SetDynamicCastMonitor(output)
}
niri_ipc::Action::ClearDynamicCastTarget {} => Self::ClearDynamicCastTarget,
niri_ipc::Action::ToggleOverview {} => Self::ToggleOverview,
niri_ipc::Action::OpenOverview {} => Self::OpenOverview,
niri_ipc::Action::CloseOverview {} => Self::CloseOverview,
niri_ipc::Action::ToggleUrgent { id } => Self::ToggleUrgent(id),
niri_ipc::Action::SetUrgent { id } => Self::SetUrgent(id),
niri_ipc::Action::UnsetUrgent { id } => Self::UnsetUrgent(id),
}
}
}
@@ -2208,12 +2396,18 @@ impl BorderRule {
if let Some(x) = other.inactive_color {
self.inactive_color = Some(x);
}
if let Some(x) = other.urgent_color {
self.urgent_color = Some(x);
}
if let Some(x) = other.active_gradient {
self.active_gradient = Some(x);
}
if let Some(x) = other.inactive_gradient {
self.inactive_gradient = Some(x);
}
if let Some(x) = other.urgent_gradient {
self.urgent_gradient = Some(x);
}
}
pub fn resolve_against(&self, mut config: Border) -> Border {
@@ -2233,12 +2427,19 @@ impl BorderRule {
config.inactive_color = x;
config.inactive_gradient = None;
}
if let Some(x) = self.urgent_color {
config.urgent_color = x;
config.urgent_gradient = None;
}
if let Some(x) = self.active_gradient {
config.active_gradient = Some(x);
}
if let Some(x) = self.inactive_gradient {
config.inactive_gradient = Some(x);
}
if let Some(x) = self.urgent_gradient {
config.urgent_gradient = Some(x);
}
config
}
@@ -2313,12 +2514,18 @@ impl TabIndicatorRule {
if let Some(x) = other.inactive_color {
self.inactive_color = Some(x);
}
if let Some(x) = other.urgent_color {
self.urgent_color = Some(x);
}
if let Some(x) = other.active_gradient {
self.active_gradient = Some(x);
}
if let Some(x) = other.inactive_gradient {
self.inactive_gradient = Some(x);
}
if let Some(x) = other.urgent_gradient {
self.urgent_gradient = Some(x);
}
}
}
@@ -2964,6 +3171,21 @@ where
}
}
impl<S> knuffel::Decode<S> for OverviewOpenCloseAnim
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
let default = Self::default().0;
Ok(Self(Animation::decode_node(node, ctx, default, |_, _| {
Ok(false)
})?))
}
}
impl Animation {
pub fn new_off() -> Self {
Self {
@@ -3958,6 +4180,7 @@ mod tests {
repeat_delay: 600,
repeat_rate: 25,
track_layout: Window,
numlock: false,
},
touchpad: Touchpad {
off: false,
@@ -4121,12 +4344,15 @@ mod tests {
},
),
focus_at_startup: true,
background_color: Color {
r: 0.09803922,
g: 0.09803922,
b: 0.4,
a: 1.0,
},
background_color: Some(
Color {
r: 0.09803922,
g: 0.09803922,
b: 0.4,
a: 1.0,
},
),
backdrop_color: None,
},
],
),
@@ -4157,6 +4383,12 @@ mod tests {
b: 0.39215687,
a: 0.0,
},
urgent_color: Color {
r: 0.60784316,
g: 0.0,
b: 0.0,
a: 1.0,
},
active_gradient: Some(
Gradient {
from: Color {
@@ -4180,6 +4412,7 @@ mod tests {
},
),
inactive_gradient: None,
urgent_gradient: None,
},
border: Border {
off: false,
@@ -4198,8 +4431,15 @@ mod tests {
b: 0.39215687,
a: 0.0,
},
urgent_color: Color {
r: 0.60784316,
g: 0.0,
b: 0.0,
a: 1.0,
},
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
},
shadow: Shadow {
on: false,
@@ -4250,8 +4490,10 @@ mod tests {
),
active_color: None,
inactive_color: None,
urgent_color: None,
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
},
insert_hint: InsertHint {
off: false,
@@ -4342,6 +4584,12 @@ mod tests {
0.0,
),
},
background_color: Color {
r: 0.25,
g: 0.25,
b: 0.25,
a: 1.0,
},
},
prefer_no_csd: true,
cursor: Cursor {
@@ -4459,6 +4707,18 @@ mod tests {
),
},
),
overview_open_close: OverviewOpenCloseAnim(
Animation {
off: false,
kind: Spring(
SpringParams {
damping_ratio: 1.0,
stiffness: 800,
epsilon: 0.0001,
},
),
},
),
},
gestures: Gestures {
dnd_edge_view_scroll: DndEdgeViewScroll {
@@ -4470,6 +4730,52 @@ mod tests {
50.0,
),
},
dnd_edge_workspace_switch: DndEdgeWorkspaceSwitch {
trigger_height: FloatOrInt(
50.0,
),
delay_ms: 100,
max_speed: FloatOrInt(
1500.0,
),
},
hot_corners: HotCorners {
off: false,
},
},
overview: Overview {
zoom: FloatOrInt(
0.5,
),
backdrop_color: Color {
r: 0.15,
g: 0.15,
b: 0.15,
a: 1.0,
},
workspace_shadow: WorkspaceShadow {
off: false,
offset: ShadowOffset {
x: FloatOrInt(
0.0,
),
y: FloatOrInt(
10.0,
),
},
softness: FloatOrInt(
40.0,
),
spread: FloatOrInt(
10.0,
),
color: Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.3137255,
},
},
},
environment: Environment(
[
@@ -4502,6 +4808,7 @@ mod tests {
is_active_in_column: None,
is_floating: None,
is_window_cast_target: None,
is_urgent: None,
at_startup: None,
},
],
@@ -4520,6 +4827,7 @@ mod tests {
is_active_in_column: None,
is_floating: None,
is_window_cast_target: None,
is_urgent: None,
at_startup: None,
},
Match {
@@ -4534,6 +4842,7 @@ mod tests {
is_active_in_column: None,
is_floating: None,
is_window_cast_target: None,
is_urgent: None,
at_startup: None,
},
],
@@ -4577,8 +4886,10 @@ mod tests {
),
active_color: None,
inactive_color: None,
urgent_color: None,
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
},
border: BorderRule {
off: false,
@@ -4590,8 +4901,10 @@ mod tests {
),
active_color: None,
inactive_color: None,
urgent_color: None,
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
},
shadow: ShadowRule {
off: false,
@@ -4613,8 +4926,10 @@ mod tests {
},
),
inactive_color: None,
urgent_color: None,
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
},
draw_border_with_background: None,
opacity: None,
@@ -4671,6 +4986,8 @@ mod tests {
inactive_color: None,
},
geometry_corner_radius: None,
place_within_backdrop: None,
baba_is_float: None,
},
],
binds: Binds(
@@ -5323,8 +5640,10 @@ mod tests {
width: None,
active_color: None,
inactive_color: None,
urgent_color: None,
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
};
for rule in rules.iter().copied() {
+1 -1
View File
@@ -12,5 +12,5 @@ Use an exact version requirement to avoid breaking changes:
```toml
[dependencies]
niri-ipc = "=25.2.0"
niri-ipc = "=25.5.0"
```
+106 -7
View File
@@ -1,8 +1,23 @@
//! Types for communicating with niri via IPC.
//!
//! After connecting to the niri socket, you can send a single [`Request`] and receive a single
//! [`Reply`], which is a `Result` wrapping a [`Response`]. If you requested an event stream, you
//! can keep reading [`Event`]s from the socket after the response.
//! After connecting to the niri socket, you can send [`Request`]s. Niri will process them one by
//! one, in order, and to each request it will respond with a single [`Reply`], which is a `Result`
//! wrapping a [`Response`].
//!
//! If you send a [`Request::EventStream`], niri will *stop* reading subsequent [`Request`]s, and
//! will start continuously writing compositor [`Event`]s to the socket. If you'd like to read an
//! event stream and write more requests at the same time, you need to use two IPC sockets.
//!
//! <div class="warning">
//!
//! Requests are *always* processed separately. Time passes between requests, even when sending
//! multiple requests to the socket at once. For example, sending [`Request::Workspaces`] and
//! [`Request::Windows`] together may not return consistent results (e.g. a window may open on a
//! new workspace in-between the two responses). This goes for actions too: sending
//! [`Action::FocusWindow`] and <code>[Action::CloseWindow] { id: None }</code> together may close
//! the wrong window because a different window got focused in-between these requests.
//!
//! </div>
//!
//! You can use the [`socket::Socket`] helper if you're fine with blocking communication. However,
//! it is a fairly simple helper, so if you need async, or if you're using a different language,
@@ -12,7 +27,9 @@
//! 2. Connect to the socket and write a JSON-formatted [`Request`] on a single line. You can follow
//! up with a line break and a flush, or just flush and shutdown the write end of the socket.
//! 3. Niri will respond with a single line JSON-formatted [`Reply`].
//! 4. If you requested an event stream, niri will keep responding with JSON-formatted [`Event`]s,
//! 4. You can keep writing [`Request`]s, each on a single line, and read [`Reply`]s, also each on a
//! separate line.
//! 5. After you request an event stream, niri will keep responding with JSON-formatted [`Event`]s,
//! on a single line each.
//!
//! ## Backwards compatibility
@@ -24,7 +41,7 @@
//!
//! ```toml
//! [dependencies]
//! niri-ipc = "=25.2.0"
//! niri-ipc = "=25.5.0"
//! ```
//!
//! ## Features
@@ -97,6 +114,8 @@ pub enum Request {
EventStream,
/// Respond with an error (for testing error handling).
ReturnError,
/// Request information about the overview.
OverviewState,
}
/// Reply from niri to client.
@@ -139,6 +158,16 @@ pub enum Response {
PickedColor(Option<PickedColor>),
/// Output configuration change result.
OutputConfigChanged(OutputConfigChanged),
/// Information about the overview.
OverviewState(Overview),
}
/// Overview information.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Overview {
/// Whether the overview is currently open.
pub is_open: bool,
}
/// Color picked from the screen.
@@ -397,6 +426,8 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Center all fully visible columns on the screen.
CenterVisibleColumns {},
/// Focus the workspace below.
FocusWorkspaceDown {},
/// Focus the workspace above.
@@ -438,14 +469,35 @@ pub enum Action {
focus: bool,
},
/// Move the focused column to the workspace below.
MoveColumnToWorkspaceDown {},
MoveColumnToWorkspaceDown {
/// Whether the focus should follow the target workspace.
///
/// If `true` (the default), the focus will follow the column to the new workspace. If
/// `false`, the focus will remain on the original workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
/// Move the focused column to the workspace above.
MoveColumnToWorkspaceUp {},
MoveColumnToWorkspaceUp {
/// Whether the focus should follow the target workspace.
///
/// If `true` (the default), the focus will follow the column to the new workspace. If
/// `false`, the focus will remain on the original workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
/// Move the focused column to a workspace by reference (index or name).
MoveColumnToWorkspace {
/// Reference (index or name) of the workspace to move the column to.
#[cfg_attr(feature = "clap", arg())]
reference: WorkspaceReferenceArg,
/// Whether the focus should follow the target workspace.
///
/// If `true` (the default), the focus will follow the column to the new workspace. If
/// `false`, the focus will remain on the original workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
/// Move the focused workspace down.
MoveWorkspaceDown {},
@@ -764,6 +816,30 @@ pub enum Action {
},
/// Clear the dynamic cast target, making it show nothing.
ClearDynamicCastTarget {},
/// Toggle (open/close) the Overview.
ToggleOverview {},
/// Open the Overview.
OpenOverview {},
/// Close the Overview.
CloseOverview {},
/// Toggle urgent status of a window.
ToggleUrgent {
/// Id of the window to toggle urgent.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Set urgent status of a window.
SetUrgent {
/// Id of the window to set urgent.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Unset urgent status of a window.
UnsetUrgent {
/// Id of the window to unset urgent.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
}
/// Change in window or column size.
@@ -1072,6 +1148,8 @@ pub struct Window {
///
/// If the window isn't floating then it is in the tiling layout.
pub is_floating: bool,
/// Whether this window requests your attention.
pub is_urgent: bool,
}
/// Output configuration change result.
@@ -1111,6 +1189,8 @@ pub struct Workspace {
///
/// Can be `None` if no outputs are currently connected.
pub output: Option<String>,
/// Whether the workspace currently has an urgent window in its output.
pub is_urgent: bool,
/// Whether the workspace is currently active on its output.
///
/// Every output has one active workspace, the one that is currently visible on that output.
@@ -1185,6 +1265,13 @@ pub enum Event {
/// workspaces are missing from here, then they were deleted.
workspaces: Vec<Workspace>,
},
/// The workspace urgency changed.
WorkspaceUrgencyChanged {
/// Id of the workspace.
id: u64,
/// Whether this workspace has an urgent window.
urgent: bool,
},
/// A workspace was activated on an output.
///
/// This doesn't always mean the workspace became focused, just that it's now the active
@@ -1232,6 +1319,13 @@ pub enum Event {
/// Id of the newly focused window, or `None` if no window is now focused.
id: Option<u64>,
},
/// Window urgency changed.
WindowUrgencyChanged {
/// Id of the window.
id: u64,
/// The new urgency state of the window.
urgent: bool,
},
/// The configured keyboard layouts have changed.
KeyboardLayoutsChanged {
/// The new keyboard layout configuration.
@@ -1242,6 +1336,11 @@ pub enum Event {
/// Index of the newly active layout.
idx: u8,
},
/// The overview was opened or closed.
OverviewOpenedOrClosed {
/// The new state of the overview.
is_open: bool,
},
}
impl FromStr for WorkspaceReferenceArg {
+42 -18
View File
@@ -16,7 +16,7 @@ pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
/// This struct is used to communicate with the niri IPC server. It handles the socket connection
/// and serialization/deserialization of messages.
pub struct Socket {
stream: UnixStream,
stream: BufReader<UnixStream>,
}
impl Socket {
@@ -37,6 +37,7 @@ impl Socket {
/// Connects to the niri IPC socket at the given path.
pub fn connect_to(path: impl AsRef<Path>) -> io::Result<Self> {
let stream = UnixStream::connect(path.as_ref())?;
let stream = BufReader::new(stream);
Ok(Self { stream })
}
@@ -47,31 +48,54 @@ impl Socket {
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
/// * `Ok(Err(message))`: error message from niri
/// * `Err(error)`: error communicating with niri
///
/// This method also returns a blocking function that you can call to keep reading [`Event`]s
/// after requesting an [`EventStream`][Request::EventStream]. This function is not useful
/// otherwise.
pub fn send(self, request: Request) -> io::Result<(Reply, impl FnMut() -> io::Result<Event>)> {
let Self { mut stream } = self;
pub fn send(&mut self, request: Request) -> io::Result<Reply> {
let mut buf = serde_json::to_string(&request).unwrap();
stream.write_all(buf.as_bytes())?;
stream.shutdown(Shutdown::Write)?;
let mut reader = BufReader::new(stream);
buf.push('\n');
self.stream.get_mut().write_all(buf.as_bytes())?;
buf.clear();
reader.read_line(&mut buf)?;
self.stream.read_line(&mut buf)?;
let reply = serde_json::from_str(&buf)?;
Ok(reply)
}
let events = move || {
/// Starts reading event stream [`Event`]s from the socket.
///
/// The returned function will block until the next [`Event`] arrives, then return it.
///
/// Use this only after requesting an [`EventStream`][Request::EventStream].
///
/// # Examples
///
/// ```no_run
/// use niri_ipc::{Request, Response};
/// use niri_ipc::socket::Socket;
///
/// fn main() -> std::io::Result<()> {
/// let mut socket = Socket::connect()?;
///
/// let reply = socket.send(Request::EventStream)?;
/// if matches!(reply, Ok(Response::Handled)) {
/// let mut read_event = socket.read_events();
/// while let Ok(event) = read_event() {
/// println!("Received event: {event:?}");
/// }
/// }
///
/// Ok(())
/// }
/// ```
pub fn read_events(self) -> impl FnMut() -> io::Result<Event> {
let Self { mut stream } = self;
let _ = stream.get_mut().shutdown(Shutdown::Write);
let mut buf = String::new();
move || {
buf.clear();
reader.read_line(&mut buf)?;
stream.read_line(&mut buf)?;
let event = serde_json::from_str(&buf)?;
Ok(event)
};
Ok((reply, events))
}
}
}
+45
View File
@@ -40,6 +40,9 @@ pub struct EventStreamState {
/// State of the keyboard layouts.
pub keyboard_layouts: KeyboardLayoutsState,
/// State of the overview.
pub overview: OverviewState,
}
/// The workspaces state communicated over the event stream.
@@ -63,12 +66,20 @@ pub struct KeyboardLayoutsState {
pub keyboard_layouts: Option<KeyboardLayouts>,
}
/// The overview state communicated over the event stream.
#[derive(Debug, Default)]
pub struct OverviewState {
/// Whether the overview is currently open.
pub is_open: bool,
}
impl EventStreamStatePart for EventStreamState {
fn replicate(&self) -> Vec<Event> {
let mut events = Vec::new();
events.extend(self.workspaces.replicate());
events.extend(self.windows.replicate());
events.extend(self.keyboard_layouts.replicate());
events.extend(self.overview.replicate());
events
}
@@ -76,6 +87,7 @@ impl EventStreamStatePart for EventStreamState {
let event = self.workspaces.apply(event)?;
let event = self.windows.apply(event)?;
let event = self.keyboard_layouts.apply(event)?;
let event = self.overview.apply(event)?;
Some(event)
}
}
@@ -91,6 +103,13 @@ impl EventStreamStatePart for WorkspacesState {
Event::WorkspacesChanged { workspaces } => {
self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect();
}
Event::WorkspaceUrgencyChanged { id, urgent } => {
for ws in self.workspaces.values_mut() {
if ws.id == id {
ws.is_urgent = urgent;
}
}
}
Event::WorkspaceActivated { id, focused } => {
let ws = self.workspaces.get(&id);
let ws = ws.expect("activated workspace was missing from the map");
@@ -162,6 +181,14 @@ impl EventStreamStatePart for WindowsState {
win.is_focused = Some(win.id) == id;
}
}
Event::WindowUrgencyChanged { id, urgent } => {
for win in self.windows.values_mut() {
if win.id == id {
win.is_urgent = urgent;
break;
}
}
}
event => return Some(event),
}
None
@@ -192,3 +219,21 @@ impl EventStreamStatePart for KeyboardLayoutsState {
None
}
}
impl EventStreamStatePart for OverviewState {
fn replicate(&self) -> Vec<Event> {
vec![Event::OverviewOpenedOrClosed {
is_open: self.is_open,
}]
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::OverviewOpenedOrClosed { is_open } => {
self.is_open = is_open;
}
event => return Some(event),
}
None
}
}
+2 -2
View File
@@ -11,8 +11,8 @@ repository.workspace = true
adw = { version = "0.7.2", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.9.6", package = "gtk4", features = ["v4_12"] }
niri = { version = "25.2.0", path = ".." }
niri-config = { version = "25.2.0", path = "../niri-config" }
niri = { version = "25.5.0", path = ".." }
niri-config = { version = "25.5.0", path = "../niri-config" }
smithay.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
@@ -23,8 +23,10 @@ impl GradientArea {
width: FloatOrInt(1.),
active_color: Color::from_rgba8_unpremul(255, 255, 255, 128),
inactive_color: Color::default(),
urgent_color: Color::default(),
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
});
Self {
@@ -81,6 +83,7 @@ impl TestCase for GradientArea {
g_size,
true,
true,
false,
Rectangle::default(),
CornerRadius::default(),
1.,
+3
View File
@@ -60,8 +60,10 @@ impl Layout {
width: FloatOrInt(4.),
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
inactive_color: Color::from_rgba8_unpremul(50, 50, 50, 255),
urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255),
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
},
..Default::default()
};
@@ -266,6 +268,7 @@ impl TestCase for Layout {
.monitor_for_output(&self.output)
.unwrap()
.render_elements(renderer, RenderTarget::Output, true)
.flat_map(|(_, iter)| iter)
.map(|elem| Box::new(elem) as _)
.collect()
}
+4
View File
@@ -272,4 +272,8 @@ impl LayoutElement for TestWindow {
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
None
}
fn is_urgent(&self) -> bool {
false
}
}
+15 -1
View File
@@ -1,7 +1,7 @@
// This config is in the KDL format: https://kdl.dev
// "/-" comments out the following node.
// Check the wiki for a full description of the configuration:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Overview
// https://github.com/YaLTeR/niri/wiki/Configuration:-Introduction
// Input device configuration.
// Find the full list of options on the wiki:
@@ -16,6 +16,9 @@ input {
// layout "us,ru"
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
}
// Enable numlock on startup, omitting this setting disables it.
numlock
}
// Next sections include libinput settings.
@@ -189,6 +192,9 @@ layout {
active-color "#ffc87f"
inactive-color "#505050"
// Color of the border around windows that request your attention.
urgent-color "#9b0000"
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
@@ -350,6 +356,11 @@ binds {
XF86AudioMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
XF86AudioMicMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }
// Open/close the Overview: a zoomed-out view of workspaces and windows.
// You can also move the mouse into the top-left hot corner,
// or do a four-finger swipe up on a touchpad.
Mod+O repeat=false { toggle-overview; }
Mod+Q { close-window; }
Mod+Left { focus-column-left; }
@@ -514,6 +525,9 @@ binds {
Mod+C { center-column; }
// Center all fully visible columns on screen.
Mod+Ctrl+C { center-visible-columns; }
// Finer width adjustments.
// This command can also:
// * set width in pixels: "1000"
+19
View File
@@ -94,6 +94,12 @@ impl Spring {
x1 = (self.to - y0 + m * x0) / m;
y1 = self.oscillate(x1);
// Overdamped springs have some numerical stability issues...
if !y1.is_finite() {
return Duration::from_secs_f64(x0);
}
i += 1;
}
@@ -187,4 +193,17 @@ mod tests {
let _ = spring.clamped_duration();
let _ = spring.value_at(Duration::ZERO);
}
#[test]
fn overdamped_spring_duration_panic() {
let spring = Spring {
from: 0.,
to: 1.,
initial_velocity: 0.,
params: SpringParams::new(6., 1200., 0.0001),
};
let _ = spring.duration();
let _ = spring.clamped_duration();
let _ = spring.value_at(Duration::ZERO);
}
}
+1
View File
@@ -20,6 +20,7 @@ pub use winit::Winit;
pub mod headless;
pub use headless::Headless;
#[allow(clippy::large_enum_variant)]
pub enum Backend {
Tty(Tty),
Winit(Winit),
+4 -3
View File
@@ -19,6 +19,7 @@ use smithay::backend::allocator::format::FormatSet;
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::Fourcc;
use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneElement};
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
use smithay::backend::drm::{
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType, VrrSupport,
};
@@ -114,7 +115,7 @@ pub type TtyRendererError<'render> = <TtyRenderer<'render> as RendererSuper>::Er
type GbmDrmCompositor = DrmCompositor<
GbmAllocator<DrmDeviceFd>,
GbmDevice<DrmDeviceFd>,
GbmFramebufferExporter<DrmDeviceFd>,
(OutputPresentationFeedback, Duration),
DrmDeviceFd,
>;
@@ -971,7 +972,7 @@ impl Tty {
surface,
None,
allocator.clone(),
device.gbm.clone(),
GbmFramebufferExporter::new(device.gbm.clone()),
SUPPORTED_COLOR_FORMATS,
// This is only used to pick a good internal format, so it can use the surface's render
// formats, even though we only ever render on the primary GPU.
@@ -1001,7 +1002,7 @@ impl Tty {
surface,
None,
allocator,
device.gbm.clone(),
GbmFramebufferExporter::new(device.gbm.clone()),
SUPPORTED_COLOR_FORMATS,
render_formats,
device.drm.cursor_size(),
+2
View File
@@ -105,4 +105,6 @@ pub enum Msg {
Version,
/// Request an error from the running niri instance.
RequestError,
/// Print the overview state.
OverviewState,
}
+15 -2
View File
@@ -12,7 +12,7 @@ use smithay::wayland::shell::xdg::PopupSurface;
use crate::layer::{MappedLayer, ResolvedLayerRules};
use crate::niri::State;
use crate::utils::{is_mapped, send_scale_transform};
use crate::utils::{is_mapped, output_size, send_scale_transform};
impl WlrLayerShellHandler for State {
fn shell_state(&mut self) -> &mut WlrLayerShellState {
@@ -125,10 +125,23 @@ impl State {
// Resolve rules for newly mapped layer surfaces.
if was_unmapped {
let config = self.niri.config.borrow();
let rules = &config.layer_rules;
let rules =
ResolvedLayerRules::compute(rules, layer, self.niri.is_at_startup);
let mapped = MappedLayer::new(layer.clone(), rules, &config);
let output_size = output_size(&output);
let scale = output.current_scale().fractional_scale();
let mapped = MappedLayer::new(
layer.clone(),
rules,
output_size,
scale,
self.niri.clock.clone(),
&config,
);
let prev = self
.niri
.mapped_layer_surfaces
+15 -9
View File
@@ -211,7 +211,7 @@ impl PointerConstraintsHandler for State {
pointer.set_location(target);
// Redraw to update the cursor position if it's visible.
if !self.niri.pointer_hidden {
if self.niri.pointer_visibility.is_visible() {
// FIXME: redraw only outputs overlapping the cursor.
self.niri.queue_redraw_all();
}
@@ -369,7 +369,7 @@ impl ClientDndGrabHandler for State {
// parameters from Smithay I guess.
//
// Assume that hidden pointer means touch DnD.
if !self.niri.pointer_hidden {
if self.niri.pointer_visibility.is_visible() {
// We can't even get the current pointer location because it's locked (we're deep
// in the grab call stack here). So use the last known one.
if let Some(output) = &self.niri.pointer_contents.output {
@@ -707,6 +707,8 @@ impl GammaControlHandler for State {
}
delegate_gamma_control!(State);
struct UrgentOnlyMarker;
impl XdgActivationHandler for State {
fn activation_state(&mut self) -> &mut XdgActivationState {
&mut self.niri.activation_state
@@ -716,11 +718,10 @@ impl XdgActivationHandler for State {
// Tokens without a serial are urgency-only. This is not specified, but it seems to be the
// common client behavior.
//
// We don't have urgency yet, so just ignore such tokens.
//
// See also: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/150
let Some((serial, seat)) = data.serial else {
return false;
data.user_data.insert_if_missing(|| UrgentOnlyMarker);
return true;
};
let Some(seat) = Seat::<State>::from_resource(&seat) else {
return false;
@@ -760,11 +761,16 @@ impl XdgActivationHandler for State {
surface: WlSurface,
) {
if token_data.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&surface) {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(&surface) {
let window = mapped.window.clone();
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
self.niri.queue_redraw_all();
if token_data.user_data.get::<UrgentOnlyMarker>().is_some() {
mapped.set_urgent(true);
self.niri.queue_redraw_all();
} else {
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
self.niri.queue_redraw_all();
}
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(&surface) {
unmapped.activation_token_data = Some(token_data);
}
+28 -5
View File
@@ -153,7 +153,7 @@ impl XdgShellHandler for State {
match start_data {
PointerOrTouchStartData::Pointer(start_data) => {
let grab = MoveGrab::new(start_data, window);
let grab = MoveGrab::new(start_data, window, false);
pointer.set_grab(self, grab, serial, Focus::Clear);
}
PointerOrTouchStartData::Touch(start_data) => {
@@ -316,10 +316,20 @@ impl XdgShellHandler for State {
} else if let Some(output) = self.niri.layout.active_output() {
let layers = layer_map_for_output(output);
if layers
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
.is_none()
{
// FIXME: somewhere here we probably need to check is_overview_open to match the logic
// in update_keyboard_focus().
if let Some(layer) = layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL) {
// This is a grab for a layer surface.
if let Some(mapped) = self.niri.mapped_layer_surfaces.get(layer) {
if mapped.place_within_backdrop() {
trace!("ignoring popup grab for a layer surface within overview backdrop");
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
}
} else {
// This is a grab for a regular window; check that there's no layer surface with a
// higher input priority.
@@ -1062,6 +1072,19 @@ impl State {
// The target geometry for the positioner should be relative to its parent's geometry, so
// we will compute that here.
let mut target = Rectangle::from_size(output_geo.size);
// Background and bottom layer popups render below the top and the overlay layer, so let's
// put them into the non-exclusive zone.
//
// FIXME: ideally this should use the "top and overlay layer" non-exclusive zone, but
// Smithay only computes the "all layers" non-exclusive zone atm.
//
// FIXME: related to the above, top layer popups should use the "overlay layer"
// non-exclusive zone.
if matches!(layer_surface.layer(), Layer::Background | Layer::Bottom) {
target = map.non_exclusive_zone();
}
target.loc -= layer_geo.loc;
target.loc -= get_popup_toplevel_coords(popup);
+781 -149
View File
File diff suppressed because it is too large Load Diff
+42 -5
View File
@@ -1,10 +1,11 @@
use smithay::backend::input::ButtonState;
use smithay::desktop::Window;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent,
GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent,
GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point};
@@ -15,14 +16,32 @@ pub struct MoveGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
window: Window,
gesture: GestureState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GestureState {
Recognizing,
Move,
}
impl MoveGrab {
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
pub fn new(
start_data: PointerGrabStartData<State>,
window: Window,
use_threshold: bool,
) -> Self {
let gesture = if use_threshold {
GestureState::Recognizing
} else {
GestureState::Move
};
Self {
last_location: start_data.location,
start_data,
window,
gesture,
}
}
@@ -53,6 +72,24 @@ impl PointerGrab<State> for MoveGrab {
let output = output.clone();
let event_delta = event.location - self.last_location;
self.last_location = event.location;
if self.gesture == GestureState::Recognizing {
let c = event.location - self.start_data.location;
// Check if the gesture moved far enough to decide.
if c.x * c.x + c.y * c.y >= 8. * 8. {
self.gesture = GestureState::Move;
data.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
}
}
if self.gesture != GestureState::Move {
return;
}
let ongoing = data.niri.layout.interactive_move_update(
&self.window,
event_delta,
+69
View File
@@ -0,0 +1,69 @@
//! Swipe gesture from scroll events.
//!
//! Tracks when to begin, update, and end a swipe gesture from pointer axis events, also whether
//! the gesture is vertical or horizontal. Necessary because libinput only provides touchpad swipe
//! gesture events for 3+ fingers.
#[derive(Debug)]
pub struct ScrollSwipeGesture {
ongoing: bool,
vertical: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
BeginUpdate,
Update,
End,
}
impl ScrollSwipeGesture {
pub const fn new() -> Self {
Self {
ongoing: false,
vertical: false,
}
}
pub fn update(&mut self, dx: f64, dy: f64) -> Action {
if dx == 0. && dy == 0. {
self.ongoing = false;
Action::End
} else if !self.ongoing {
self.ongoing = true;
self.vertical = dy != 0.;
Action::BeginUpdate
} else {
Action::Update
}
}
pub fn reset(&mut self) -> bool {
if self.ongoing {
self.ongoing = false;
true
} else {
false
}
}
pub fn is_vertical(&self) -> bool {
self.vertical
}
}
impl Default for ScrollSwipeGesture {
fn default() -> Self {
Self::new()
}
}
impl Action {
pub fn begin(self) -> bool {
self == Action::BeginUpdate
}
pub fn end(self) -> bool {
self == Action::End
}
}
+28 -8
View File
@@ -10,12 +10,14 @@ use smithay::input::SeatHandler;
use smithay::output::Output;
use smithay::utils::{Logical, Point};
use crate::layout::workspace::WorkspaceId;
use crate::niri::State;
pub struct SpatialMovementGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
output: Output,
workspace_id: WorkspaceId,
gesture: GestureState,
}
@@ -27,12 +29,24 @@ enum GestureState {
}
impl SpatialMovementGrab {
pub fn new(start_data: PointerGrabStartData<State>, output: Output) -> Self {
pub fn new(
start_data: PointerGrabStartData<State>,
output: Output,
workspace_id: WorkspaceId,
is_view_offset: bool,
) -> Self {
let gesture = if is_view_offset {
GestureState::ViewOffset
} else {
GestureState::Recognizing
};
Self {
last_location: start_data.location,
start_data,
output,
gesture: GestureState::Recognizing,
workspace_id,
gesture,
}
}
@@ -40,10 +54,8 @@ impl SpatialMovementGrab {
let layout = &mut state.niri.layout;
let res = match self.gesture {
GestureState::Recognizing => None,
GestureState::ViewOffset => layout.view_offset_gesture_end(false, Some(false)),
GestureState::WorkspaceSwitch => {
layout.workspace_switch_gesture_end(false, Some(false))
}
GestureState::ViewOffset => layout.view_offset_gesture_end(Some(false)),
GestureState::WorkspaceSwitch => layout.workspace_switch_gesture_end(Some(false)),
};
if let Some(output) = res {
@@ -81,8 +93,16 @@ impl PointerGrab<State> for SpatialMovementGrab {
if c.x * c.x + c.y * c.y >= 8. * 8. {
if c.x.abs() > c.y.abs() {
self.gesture = GestureState::ViewOffset;
layout.view_offset_gesture_begin(&self.output, false);
layout.view_offset_gesture_update(-c.x, timestamp, false)
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(self.workspace_id) {
if ws.current_output() == Some(&self.output) {
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
layout.view_offset_gesture_update(-c.x, timestamp, false)
} else {
None
}
} else {
None
}
} else {
self.gesture = GestureState::WorkspaceSwitch;
layout.workspace_switch_gesture_begin(&self.output, false);
+274
View File
@@ -0,0 +1,274 @@
use std::time::Duration;
use smithay::desktop::Window;
use smithay::input::touch::{
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
TouchGrab, TouchInnerHandle, UpEvent,
};
use smithay::input::SeatHandler;
use smithay::output::Output;
use smithay::utils::{IsAlive, Logical, Point, Serial};
use crate::layout::workspace::{Workspace, WorkspaceId};
use crate::niri::State;
use crate::window::Mapped;
// When the touch is stationary for this much time, it becomes an interactive move.
const INTERACTIVE_MOVE_THRESHOLD: Duration = Duration::from_millis(500);
pub struct TouchOverviewGrab {
start_data: TouchGrabStartData<State>,
start_timestamp: Duration,
last_location: Point<f64, Logical>,
output: Output,
start_pos_within_output: Point<f64, Logical>,
workspace_id: Option<WorkspaceId>,
workspace_matched_narrow: bool,
window: Option<Window>,
gesture: GestureState,
}
#[derive(Debug, Clone, Copy)]
enum GestureState {
Recognizing,
ViewOffset,
WorkspaceSwitch,
InteractiveMove,
}
impl TouchOverviewGrab {
pub fn new(
start_data: TouchGrabStartData<State>,
start_timestamp: Duration,
output: Output,
start_pos_within_output: Point<f64, Logical>,
workspace_id: Option<WorkspaceId>,
workspace_matched_narrow: bool,
window: Option<Window>,
) -> Self {
Self {
last_location: start_data.location,
start_timestamp,
start_data,
output,
start_pos_within_output,
workspace_id,
workspace_matched_narrow,
window,
gesture: GestureState::Recognizing,
}
}
fn on_ungrab(&mut self, state: &mut State) {
let layout = &mut state.niri.layout;
match self.gesture {
GestureState::Recognizing => {
// Tap to activate.
layout.focus_output(&self.output);
// Activate the workspace if necessary.
if self.window.is_some() || self.workspace_matched_narrow {
// When activating a window, we want to activate the window's current
// workspace. Otherwise, find the workspace that we tapped on.
let ws_matches = |ws: &Workspace<Mapped>| {
if let Some(window) = &self.window {
ws.has_window(window)
} else if let Some(ws_id) = self.workspace_id {
ws.id() == ws_id
} else {
false
}
};
let ws_idx = if let Some((Some(mon), ws_idx, _)) =
layout.workspaces().find(|(_, _, ws)| ws_matches(ws))
{
// The workspace could've moved to a different output in the meantime.
(*mon.output() == self.output).then_some(ws_idx)
} else {
None
};
if let Some(ws_idx) = ws_idx {
layout.toggle_overview_to_workspace(ws_idx);
}
}
if let Some(window) = self.window.as_ref() {
layout.activate_window(window);
}
}
GestureState::ViewOffset => {
layout.view_offset_gesture_end(Some(false));
}
GestureState::WorkspaceSwitch => {
layout.workspace_switch_gesture_end(Some(false));
}
GestureState::InteractiveMove => {
layout.interactive_move_end(self.window.as_ref().unwrap());
}
};
state.niri.queue_redraw_all();
}
}
impl TouchGrab<State> for TouchOverviewGrab {
fn down(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
event: &DownEvent,
seq: Serial,
) {
handle.down(data, None, event, seq);
}
fn up(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &UpEvent,
seq: Serial,
) {
handle.up(data, event, seq);
if event.slot != self.start_data.slot {
return;
}
handle.unset_grab(self, data);
}
fn motion(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
event: &MotionEvent,
seq: Serial,
) {
handle.motion(data, None, event, seq);
if event.slot != self.start_data.slot {
return;
}
let timestamp = Duration::from_millis(u64::from(event.time));
let layout = &mut data.niri.layout;
// Check if we should become interactive move.
if matches!(self.gesture, GestureState::Recognizing) {
if let Some(window) = self.window.as_ref().filter(|win| win.alive()) {
let passed = timestamp.saturating_sub(self.start_timestamp);
if INTERACTIVE_MOVE_THRESHOLD <= passed
&& layout.interactive_move_begin(
window.clone(),
&self.output,
self.start_pos_within_output,
)
{
self.gesture = GestureState::InteractiveMove;
}
}
}
// Check if we should become a spatial scroll.
if matches!(self.gesture, GestureState::Recognizing) {
let c = event.location - self.start_data.location;
// Check if the gesture moved far enough to decide. Threshold copied from libadwaita.
if c.x * c.x + c.y * c.y >= 16. * 16. {
if let Some(ws_id) = self.workspace_id.filter(|_| c.x.abs() > c.y.abs()) {
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(ws_id) {
if ws.current_output() == Some(&self.output) {
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
self.gesture = GestureState::ViewOffset;
}
}
}
if matches!(self.gesture, GestureState::Recognizing) {
layout.workspace_switch_gesture_begin(&self.output, false);
self.gesture = GestureState::WorkspaceSwitch;
}
}
}
// Do nothing if still recognizing.
if matches!(self.gesture, GestureState::Recognizing) {
return;
}
let delta = event.location - self.last_location;
self.last_location = event.location;
let ongoing = match self.gesture {
GestureState::Recognizing => unreachable!(),
GestureState::ViewOffset => layout
.view_offset_gesture_update(-delta.x, timestamp, false)
.is_some(),
GestureState::WorkspaceSwitch => layout
.workspace_switch_gesture_update(-delta.y, timestamp, false)
.is_some(),
GestureState::InteractiveMove => {
let window = self.window.as_ref().unwrap();
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
let output = output.clone();
data.niri.layout.interactive_move_update(
window,
delta,
output,
pos_within_output,
)
} else {
false
}
}
};
if ongoing {
data.niri.queue_redraw_all();
} else {
handle.unset_grab(self, data);
}
}
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
handle.frame(data, seq);
}
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
handle.cancel(data, seq);
handle.unset_grab(self, data);
}
fn shape(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &ShapeEvent,
seq: Serial,
) {
handle.shape(data, event, seq);
}
fn orientation(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
event: &OrientationEvent,
seq: Serial,
) {
handle.orientation(data, event, seq);
}
fn start_data(&self) -> &TouchGrabStartData<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}
+78 -37
View File
@@ -1,3 +1,4 @@
use std::io::ErrorKind;
use std::iter::Peekable;
use std::slice;
@@ -5,8 +6,8 @@ use anyhow::{anyhow, bail, Context};
use niri_config::OutputName;
use niri_ipc::socket::Socket;
use niri_ipc::{
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response,
Transform, Window,
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Overview, Request,
Response, Transform, Window,
};
use serde_json::json;
@@ -32,24 +33,35 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
Msg::KeyboardLayouts => Request::KeyboardLayouts,
Msg::EventStream => Request::EventStream,
Msg::RequestError => Request::ReturnError,
Msg::OverviewState => Request::OverviewState,
};
let socket = Socket::connect().context("error connecting to the niri socket")?;
let mut socket = Socket::connect().context("error connecting to the niri socket")?;
let (reply, mut read_event) = socket
.send(request)
.context("error communicating with niri")?;
let result = socket.send(request);
let compositor_version = match reply {
Err(_) if !matches!(msg, Msg::Version) => {
// If we got an error, it might be that the CLI is a different version from the running
// niri instance. Request the running instance version to compare and print a message.
Socket::connect()
.and_then(|socket| socket.send(Request::Version))
.ok()
.map(|(reply, _read_event)| reply)
// For errors that can be caused by a version mismatch between the running niri instance and
// the niri msg CLI, we will try to fetch and compare the versions.
let check_compositor_version = match &result {
Err(err) => {
// Response JSON parsing errors.
matches!(
err.kind(),
ErrorKind::InvalidData | ErrorKind::UnexpectedEof
)
}
_ => None,
// Error returned from niri.
Ok(Err(_)) => true,
_ => false,
};
let compositor_version = if check_compositor_version && !matches!(msg, Msg::Version) {
// Reconnect to support older niri versions with one request per connection.
Socket::connect()
.and_then(|mut socket| socket.send(Request::Version))
.ok()
} else {
None
};
// Default SIGPIPE so that our prints don't panic on stdout closing.
@@ -57,32 +69,31 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
let response = reply.map_err(|err_msg| {
// Check for CLI-server version mismatch to add helpful context.
match compositor_version {
Some(Ok(Response::Version(compositor_version))) => {
let cli_version = version();
if cli_version != compositor_version {
eprintln!("Running niri compositor has a different version from the niri CLI:");
eprintln!("Compositor version: {compositor_version}");
eprintln!("CLI version: {cli_version}");
eprintln!("Did you forget to restart niri after an update?");
eprintln!();
}
}
Some(_) => {
eprintln!("Unable to get the running niri compositor version.");
// Check for CLI-server version mismatch to add helpful context.
match compositor_version {
Some(Ok(Response::Version(compositor_version))) => {
let cli_version = version();
if cli_version != compositor_version {
eprintln!("Running niri compositor has a different version from the niri CLI:");
eprintln!("Compositor version: {compositor_version}");
eprintln!("CLI version: {cli_version}");
eprintln!("Did you forget to restart niri after an update?");
eprintln!();
}
None => {
// Communication error, or the original request was already a version request.
// Don't add irrelevant context.
}
}
Some(_) => {
eprintln!("Unable to get the running niri compositor version.");
eprintln!("Did you forget to restart niri after an update?");
eprintln!();
}
None => {
// Communication error, or the original request was already a version request, or the
// original request had succeeded. Don't add irrelevant context.
}
}
anyhow!(err_msg).context("niri returned an error")
})?;
let reply = result.context("error communicating with niri")?;
let response = reply.map_err(|err_msg| anyhow!(err_msg).context("niri returned an error"))?;
match msg {
Msg::RequestError => {
@@ -391,6 +402,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!("Started reading events.");
}
let mut read_event = socket.read_events();
loop {
let event = read_event().context("error reading event from niri")?;
@@ -404,6 +416,9 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
Event::WorkspacesChanged { workspaces } => {
println!("Workspaces changed: {workspaces:?}");
}
Event::WorkspaceUrgencyChanged { id, urgent } => {
println!("Workspace {id}: urgency changed to {urgent}");
}
Event::WorkspaceActivated { id, focused } => {
let word = if focused { "focused" } else { "activated" };
println!("Workspace {word}: {id}");
@@ -429,15 +444,40 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
Event::WindowFocusChanged { id } => {
println!("Window focus changed: {id:?}");
}
Event::WindowUrgencyChanged { id, urgent } => {
println!("Window {id}: urgency changed to {urgent}");
}
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
println!("Keyboard layouts changed: {keyboard_layouts:?}");
}
Event::KeyboardLayoutSwitched { idx } => {
println!("Keyboard layout switched: {idx}");
}
Event::OverviewOpenedOrClosed { is_open: opened } => {
println!("Overview toggled: {opened}");
}
}
}
}
Msg::OverviewState => {
let Response::OverviewState(response) = response else {
bail!("unexpected response: expected Overview, got {response:?}");
};
if json {
let response =
serde_json::to_string(&response).context("error formatting response")?;
println!("{response}");
return Ok(());
}
let Overview { is_open } = response;
if is_open {
println!("Overview is open.");
} else {
println!("Overview is closed.");
}
}
}
Ok(())
@@ -541,7 +581,8 @@ fn print_output(output: Output) -> anyhow::Result<()> {
fn print_window(window: &Window) {
let focused = if window.is_focused { " (focused)" } else { "" };
println!("Window ID {}:{focused}", window.id);
let urgent = if window.is_urgent { " (urgent)" } else { "" };
println!("Window ID {}:{focused}{urgent}", window.id);
if let Some(title) = &window.title {
println!(" Title: \"{title}\"");
+113 -64
View File
@@ -16,7 +16,9 @@ use futures_util::io::{AsyncReadExt, BufReader};
use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, FutureExt as _};
use niri_config::OutputName;
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace};
use niri_ipc::{
Event, KeyboardLayouts, OutputConfigChanged, Overview, Reply, Request, Response, Workspace,
};
use smithay::desktop::layer_map_for_output;
use smithay::input::pointer::{
CursorIcon, CursorImageStatus, Focus, GrabStartData as PointerGrabStartData,
@@ -183,76 +185,86 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
async fn handle_client(ctx: ClientCtx, stream: Async<'static, UnixStream>) -> anyhow::Result<()> {
let (read, mut write) = stream.split();
let mut buf = String::new();
let mut read = BufReader::new(read);
// Read a single line to allow extensibility in the future to keep reading.
BufReader::new(read)
.read_line(&mut buf)
.await
.context("error reading request")?;
let request = serde_json::from_str(&buf)
.context("error parsing request")
.map_err(|err| err.to_string());
let requested_error = matches!(request, Ok(Request::ReturnError));
let requested_event_stream = matches!(request, Ok(Request::EventStream));
let reply = match request {
Ok(request) => process(&ctx, request).await,
Err(err) => Err(err),
};
if let Err(err) = &reply {
if !requested_error {
warn!("error processing IPC request: {err:?}");
}
}
let mut buf = serde_json::to_vec(&reply).context("error formatting reply")?;
buf.push(b'\n');
write.write_all(&buf).await.context("error writing reply")?;
if requested_event_stream {
let (events_tx, events_rx) = async_channel::bounded(EVENT_STREAM_BUFFER_SIZE);
let (disconnect_tx, disconnect_rx) = async_channel::bounded(1);
// Spawn a task for the client.
let client = EventStreamClient {
events: events_rx,
disconnect: disconnect_rx,
write: Box::new(write) as _,
};
let future = async move {
if let Err(err) = handle_event_stream_client(client).await {
warn!("error handling IPC event stream client: {err:?}");
}
};
if let Err(err) = ctx.scheduler.schedule(future) {
warn!("error scheduling IPC event stream future: {err:?}");
}
// Send the initial state.
{
let state = ctx.event_stream_state.borrow();
for event in state.replicate() {
events_tx
.try_send(event)
.expect("initial event burst had more events than buffer size");
loop {
// Don't keep buf around to avoid clients wasting RAM by filling it with bogus data.
let mut buf = Vec::new();
let res = read.read_until(b'\n', &mut buf).await;
match res {
Ok(0) => return Ok(()),
Ok(_) => (),
// Normal client disconnection.
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
Err(err) => {
return Err(err).context("error reading request");
}
}
// Add it to the list.
{
let mut streams = ctx.event_streams.borrow_mut();
let sender = EventStreamSender {
events: events_tx,
disconnect: disconnect_tx,
let request = serde_json::from_slice(&buf)
.context("error parsing request")
.map_err(|err| err.to_string());
let requested_error = matches!(request, Ok(Request::ReturnError));
let requested_event_stream = matches!(request, Ok(Request::EventStream));
let reply = match request {
Ok(request) => process(&ctx, request).await,
Err(err) => Err(err),
};
if let Err(err) = &reply {
if !requested_error {
warn!("error processing IPC request: {err:?}");
}
}
buf.clear();
serde_json::to_writer(&mut buf, &reply).context("error formatting reply")?;
buf.push(b'\n');
write.write_all(&buf).await.context("error writing reply")?;
if requested_event_stream {
let (events_tx, events_rx) = async_channel::bounded(EVENT_STREAM_BUFFER_SIZE);
let (disconnect_tx, disconnect_rx) = async_channel::bounded(1);
// Spawn a task for the client.
let client = EventStreamClient {
events: events_rx,
disconnect: disconnect_rx,
write: Box::new(write) as _,
};
streams.push(sender);
let future = async move {
if let Err(err) = handle_event_stream_client(client).await {
warn!("error handling IPC event stream client: {err:?}");
}
};
if let Err(err) = ctx.scheduler.schedule(future) {
warn!("error scheduling IPC event stream future: {err:?}");
}
// Send the initial state.
{
let state = ctx.event_stream_state.borrow();
for event in state.replicate() {
events_tx
.try_send(event)
.expect("initial event burst had more events than buffer size");
}
}
// Add it to the list.
{
let mut streams = ctx.event_streams.borrow_mut();
let sender = EventStreamSender {
events: events_tx,
disconnect: disconnect_tx,
};
streams.push(sender);
}
return Ok(());
}
}
Ok(())
}
async fn process(ctx: &ClientCtx, request: Request) -> Reply {
@@ -428,6 +440,11 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
Response::FocusedOutput(output)
}
Request::EventStream => Response::Handled,
Request::OverviewState => {
let state = ctx.event_stream_state.borrow();
let is_open = state.overview.is_open;
Response::OverviewState(Overview { is_open })
}
};
Ok(response)
@@ -469,6 +486,7 @@ fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_i
workspace_id: workspace_id.map(|id| id.get()),
is_focused: mapped.is_focused(),
is_floating: mapped.is_floating(),
is_urgent: mapped.is_urgent(),
})
}
@@ -524,6 +542,7 @@ impl State {
pub fn ipc_refresh_layout(&mut self) {
self.ipc_refresh_workspaces();
self.ipc_refresh_windows();
self.ipc_refresh_overview();
}
fn ipc_refresh_workspaces(&mut self) {
@@ -571,6 +590,12 @@ impl State {
});
}
// Check if this workspace urgent state changed.
let urgent = ws.is_urgent();
if urgent != ipc_ws.is_urgent {
events.push(Event::WorkspaceUrgencyChanged { id, urgent });
}
// Check if this workspace became focused.
let is_focused = Some(id) == focused_ws_id;
if is_focused && !ipc_ws.is_focused {
@@ -602,6 +627,7 @@ impl State {
idx: u8::try_from(ws_idx + 1).unwrap_or(u8::MAX),
name: ws.name().cloned(),
output: mon.map(|mon| mon.output_name().clone()),
is_urgent: ws.is_urgent(),
is_active: mon.is_some_and(|mon| mon.active_workspace_idx() == ws_idx),
is_focused: Some(id) == focused_ws_id,
active_window_id: ws.active_window().map(|win| win.id().get()),
@@ -665,6 +691,11 @@ impl State {
if mapped.is_focused() && !ipc_win.is_focused {
events.push(Event::WindowFocusChanged { id: Some(id) });
}
let urgent = mapped.is_urgent();
if urgent != ipc_win.is_urgent {
events.push(Event::WindowUrgencyChanged { id, urgent })
}
});
// Check for closed windows.
@@ -690,4 +721,22 @@ impl State {
server.send_event(event);
}
}
pub fn ipc_refresh_overview(&mut self) {
let Some(server) = &self.niri.ipc_server else {
return;
};
let mut state = server.event_stream_state.borrow_mut();
let state = &mut state.overview;
let is_open = self.niri.layout.is_overview_open();
if state.is_open == is_open {
return;
}
let event = Event::OverviewOpenedOrClosed { is_open };
state.apply(event.clone());
server.send_event(event);
}
}
+66 -5
View File
@@ -6,14 +6,17 @@ use smithay::backend::renderer::element::surface::{
use smithay::backend::renderer::element::Kind;
use smithay::desktop::{LayerSurface, PopupManager};
use smithay::utils::{Logical, Point, Scale, Size};
use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer};
use super::ResolvedLayerRules;
use crate::animation::Clock;
use crate::layout::shadow::Shadow;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::{RenderTarget, SplitElements};
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
#[derive(Debug)]
pub struct MappedLayer {
@@ -28,6 +31,15 @@ pub struct MappedLayer {
/// The shadow around the surface.
shadow: Shadow,
/// The view size for the layer surface's output.
view_size: Size<f64, Logical>,
/// Scale of the output the layer surface is on (and rounds its sizes to).
scale: f64,
/// Clock for driving animations.
clock: Clock,
}
niri_render_elements! {
@@ -39,7 +51,14 @@ niri_render_elements! {
}
impl MappedLayer {
pub fn new(surface: LayerSurface, rules: ResolvedLayerRules, config: &Config) -> Self {
pub fn new(
surface: LayerSurface,
rules: ResolvedLayerRules,
view_size: Size<f64, Logical>,
scale: f64,
clock: Clock,
config: &Config,
) -> Self {
let mut shadow_config = config.layout.shadow;
// Shadows for layer surfaces need to be explicitly enabled.
shadow_config.on = false;
@@ -49,7 +68,10 @@ impl MappedLayer {
surface,
rules,
block_out_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
view_size,
scale,
shadow: Shadow::new(shadow_config),
clock,
}
}
@@ -65,16 +87,27 @@ impl MappedLayer {
self.shadow.update_shaders();
}
pub fn update_render_elements(&mut self, size: Size<f64, Logical>, scale: Scale<f64>) {
pub fn update_sizes(&mut self, view_size: Size<f64, Logical>, scale: f64) {
self.view_size = view_size;
self.scale = scale;
}
pub fn update_render_elements(&mut self, size: Size<f64, Logical>) {
// Round to physical pixels.
let size = size.to_physical_precise_round(scale).to_logical(scale);
let size = size
.to_physical_precise_round(self.scale)
.to_logical(self.scale);
self.block_out_buffer.resize(size);
let radius = self.rules.geometry_corner_radius.unwrap_or_default();
// FIXME: is_active based on keyboard focus?
self.shadow
.update_render_elements(size, true, radius, scale.x, 1.);
.update_render_elements(size, true, radius, self.scale, 1.);
}
pub fn are_animations_ongoing(&self) -> bool {
self.rules.baba_is_float
}
pub fn surface(&self) -> &LayerSurface {
@@ -96,16 +129,44 @@ impl MappedLayer {
true
}
pub fn place_within_backdrop(&self) -> bool {
if !self.rules.place_within_backdrop {
return false;
}
if self.surface.layer() != Layer::Background {
return false;
}
let state = self.surface.cached_state();
if state.exclusive_zone != ExclusiveZone::DontCare {
return false;
}
true
}
pub fn bob_offset(&self) -> Point<f64, Logical> {
if !self.rules.baba_is_float {
return Point::from((0., 0.));
}
let y = baba_is_float_offset(self.clock.now(), self.view_size.h);
let y = round_logical_in_physical(self.scale, y);
Point::from((0., y))
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<f64, Logical>,
scale: Scale<f64>,
target: RenderTarget,
) -> SplitElements<LayerSurfaceRenderElement<R>> {
let mut rv = SplitElements::default();
let scale = Scale::from(self.scale);
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
let location = location + self.bob_offset();
if target.should_block_out(self.rules.block_out_from) {
// Round to physical pixels.
+14
View File
@@ -19,6 +19,12 @@ pub struct ResolvedLayerRules {
/// Corner radius to assume this layer surface has.
pub geometry_corner_radius: Option<CornerRadius>,
/// Whether to place this layer surface within the overview backdrop.
pub place_within_backdrop: bool,
/// Whether to bob this window up and down.
pub baba_is_float: bool,
}
impl ResolvedLayerRules {
@@ -37,6 +43,8 @@ impl ResolvedLayerRules {
inactive_color: None,
},
geometry_corner_radius: None,
place_within_backdrop: false,
baba_is_float: false,
}
}
@@ -73,6 +81,12 @@ impl ResolvedLayerRules {
if let Some(x) = rule.geometry_corner_radius {
resolved.geometry_corner_radius = Some(x);
}
if let Some(x) = rule.place_within_backdrop {
resolved.place_within_backdrop = x;
}
if let Some(x) = rule.baba_is_float {
resolved.baba_is_float = x;
}
resolved.shadow.merge_with(&rule.shadow);
}
+7 -2
View File
@@ -59,6 +59,7 @@ impl FocusRing {
win_size: Size<f64, Logical>,
is_active: bool,
is_border: bool,
is_urgent: bool,
view_rect: Rectangle<f64, Logical>,
radius: CornerRadius,
scale: f64,
@@ -67,7 +68,9 @@ impl FocusRing {
let width = self.config.width.0;
self.full_size = win_size + Size::from((width, width)).upscale(2.);
let color = if is_active {
let color = if is_urgent {
self.config.urgent_color
} else if is_active {
self.config.active_color
} else {
self.config.inactive_color
@@ -79,7 +82,9 @@ impl FocusRing {
let radius = radius.fit_to(self.full_size.w as f32, self.full_size.h as f32);
let gradient = if is_active {
let gradient = if is_urgent {
self.config.urgent_gradient
} else if is_active {
self.config.active_gradient
} else {
self.config.inactive_gradient
+5 -1
View File
@@ -19,8 +19,10 @@ impl InsertHintElement {
width: FloatOrInt(0.),
active_color: config.color,
inactive_color: config.color,
urgent_color: config.color,
active_gradient: config.gradient,
inactive_gradient: config.gradient,
urgent_gradient: config.gradient,
}),
}
}
@@ -31,8 +33,10 @@ impl InsertHintElement {
width: FloatOrInt(0.),
active_color: config.color,
inactive_color: config.color,
urgent_color: config.color,
active_gradient: config.gradient,
inactive_gradient: config.gradient,
urgent_gradient: config.gradient,
});
}
@@ -48,7 +52,7 @@ impl InsertHintElement {
scale: f64,
) {
self.inner
.update_render_elements(size, true, false, view_rect, radius, scale, 1.);
.update_render_elements(size, true, false, false, view_rect, radius, scale, 1.);
}
pub fn render(
+766 -175
View File
File diff suppressed because it is too large Load Diff
+977 -200
View File
File diff suppressed because it is too large Load Diff
+158 -111
View File
@@ -3,14 +3,14 @@ use std::iter::{self, zip};
use std::rc::Rc;
use std::time::Duration;
use niri_config::{CenterFocusedColumn, CornerRadius, PresetSize, Struts};
use niri_config::{CenterFocusedColumn, PresetSize, Struts};
use niri_ipc::{ColumnDisplay, SizeChange};
use ordered_float::NotNan;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size};
use super::closing_window::{ClosingWindow, ClosingWindowRenderElement};
use super::insert_hint_element::{InsertHintElement, InsertHintRenderElement};
use super::monitor::InsertPosition;
use super::tab_indicator::{TabIndicator, TabIndicatorRenderElement, TabInfo};
use super::tile::{Tile, TileRenderElement, TileRenderSnapshot};
use super::workspace::{InteractiveResize, ResolvedSize};
@@ -67,12 +67,6 @@ pub struct ScrollingSpace<W: LayoutElement> {
/// Windows in the closing animation.
closing_windows: Vec<ClosingWindow>,
/// Indication where an interactively-moved window is about to be placed.
insert_hint: Option<InsertHint>,
/// Insert hint element for rendering.
insert_hint_element: InsertHintElement,
/// View size for this space.
view_size: Size<f64, Logical>,
@@ -96,23 +90,9 @@ niri_render_elements! {
Tile = TileRenderElement<R>,
ClosingWindow = ClosingWindowRenderElement,
TabIndicator = TabIndicatorRenderElement,
InsertHint = InsertHintRenderElement,
}
}
#[derive(Debug, PartialEq)]
pub enum InsertPosition {
NewColumn(usize),
InColumn(usize, usize),
Floating,
}
#[derive(Debug)]
pub struct InsertHint {
pub position: InsertPosition,
pub corner_radius: CornerRadius,
}
/// Extra per-column data.
#[derive(Debug, Clone, Copy, PartialEq)]
struct ColumnData {
@@ -133,6 +113,10 @@ pub(super) enum ViewOffset {
#[derive(Debug)]
pub(super) struct ViewGesture {
current_view_offset: f64,
/// Animation for the extra offset to the current position.
///
/// For example, when we need to activate a specific window during a DnD scroll.
animation: Option<Animation>,
tracker: SwipeTracker,
delta_from_tracker: f64,
// The view offset we'll use if needed for activate_prev_column_on_removal.
@@ -283,8 +267,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
activate_prev_column_on_removal: None,
view_offset_before_fullscreen: None,
closing_windows: Vec::new(),
insert_hint: None,
insert_hint_element: InsertHintElement::new(options.insert_hint),
view_size,
working_area,
scale,
@@ -307,20 +289,21 @@ impl<W: LayoutElement> ScrollingSpace<W> {
data.update(column);
}
self.insert_hint_element.update_config(options.insert_hint);
self.view_size = view_size;
self.working_area = working_area;
self.scale = scale;
self.options = options;
// Apply always-center and such right away.
if !self.columns.is_empty() && !self.view_offset.is_gesture() {
self.animate_view_offset_to_column(None, self.active_column_idx, None);
}
}
pub fn update_shaders(&mut self) {
for tile in self.tiles_mut() {
tile.update_shaders();
}
self.insert_hint_element.update_shaders();
}
pub fn advance_animations(&mut self) {
@@ -347,6 +330,12 @@ impl<W: LayoutElement> ScrollingSpace<W> {
gesture.dnd_nonzero_start_time = None;
}
}
if let Some(anim) = &mut gesture.animation {
if anim.is_done() {
gesture.animation = None;
}
}
}
for col in &mut self.columns {
@@ -360,7 +349,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
}
pub fn are_animations_ongoing(&self) -> bool {
self.view_offset.is_animation()
self.view_offset.is_animation_ongoing()
|| self.columns.iter().any(Column::are_animations_ongoing)
|| !self.closing_windows.is_empty()
}
@@ -382,18 +371,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
let view_rect = Rectangle::new(col_pos, view_size);
col.update_render_elements(is_active, view_rect);
}
if let Some(insert_hint) = &self.insert_hint {
if let Some(area) = self.insert_hint_area(insert_hint) {
let view_rect = Rectangle::new(area.loc.upscale(-1.), view_size);
self.insert_hint_element.update_render_elements(
area.size,
view_rect,
insert_hint.corner_radius,
self.scale,
);
}
}
}
pub fn tiles(&self) -> impl Iterator<Item = &Tile<W>> + '_ {
@@ -616,6 +593,11 @@ impl<W: LayoutElement> ScrollingSpace<W> {
return self.compute_new_view_offset_for_column_fit(target_x, idx);
};
// Activating the same column.
if prev_idx == idx {
return self.compute_new_view_offset_for_column_fit(target_x, idx);
}
// Always take the left or right neighbor of the target as the source.
let source_idx = if prev_idx > idx {
min(idx + 1, self.columns.len() - 1)
@@ -664,8 +646,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
new_view_offset: f64,
config: niri_config::Animation,
) {
self.view_offset.cancel_gesture();
let new_col_x = self.column_x(idx);
let old_col_x = self.column_x(self.active_column_idx);
let offset_delta = old_col_x - new_col_x;
@@ -682,14 +662,28 @@ impl<W: LayoutElement> ScrollingSpace<W> {
return;
}
// FIXME: also compute and use current velocity.
self.view_offset = ViewOffset::Animation(Animation::new(
self.clock.clone(),
self.view_offset.current(),
new_view_offset,
0.,
config,
));
match &mut self.view_offset {
ViewOffset::Gesture(gesture) if gesture.dnd_last_event_time.is_some() => {
gesture.stationary_view_offset = new_view_offset;
let current_pos = gesture.current_view_offset - gesture.delta_from_tracker;
gesture.delta_from_tracker = new_view_offset - current_pos;
let offset_delta = new_view_offset - gesture.current_view_offset;
gesture.current_view_offset = new_view_offset;
gesture.animate_from(-offset_delta, self.clock.clone(), config);
}
_ => {
// FIXME: also compute and use current velocity.
self.view_offset = ViewOffset::Animation(Animation::new(
self.clock.clone(),
self.view_offset.current(),
new_view_offset,
0.,
config,
));
}
}
}
fn animate_view_offset_to_column_centered(
@@ -735,7 +729,10 @@ impl<W: LayoutElement> ScrollingSpace<W> {
}
fn activate_column_with_anim_config(&mut self, idx: usize, config: niri_config::Animation) {
if self.active_column_idx == idx {
if self.active_column_idx == idx
// During a DnD scroll, animate even when activating the same window, for DnD hold.
&& (self.columns.is_empty() || !self.view_offset.is_dnd_scroll())
{
return;
}
@@ -746,26 +743,17 @@ impl<W: LayoutElement> ScrollingSpace<W> {
config,
);
self.active_column_idx = idx;
if self.active_column_idx != idx {
self.active_column_idx = idx;
// A different column was activated; reset the flag.
self.activate_prev_column_on_removal = None;
self.view_offset_before_fullscreen = None;
self.interactive_resize = None;
}
pub fn set_insert_hint(&mut self, insert_hint: InsertHint) {
if self.options.insert_hint.off {
return;
// A different column was activated; reset the flag.
self.activate_prev_column_on_removal = None;
self.view_offset_before_fullscreen = None;
self.interactive_resize = None;
}
self.insert_hint = Some(insert_hint);
}
pub fn clear_insert_hint(&mut self) {
self.insert_hint = None;
}
pub fn get_insert_position(&self, pos: Point<f64, Logical>) -> InsertPosition {
pub(super) fn insert_position(&self, pos: Point<f64, Logical>) -> InsertPosition {
if self.columns.is_empty() {
return InsertPosition::NewColumn(0);
}
@@ -1612,7 +1600,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
// Preserve the camera position when moving to the left.
let view_offset_delta = -self.column_x(self.active_column_idx) + current_col_x;
self.view_offset.cancel_gesture();
self.view_offset.offset(view_offset_delta);
// The column we just moved is offset by the difference between its new and old position.
@@ -2152,6 +2139,64 @@ impl<W: LayoutElement> ScrollingSpace<W> {
self.center_column();
}
pub fn center_visible_columns(&mut self) {
if self.columns.is_empty() {
return;
}
if self.is_centering_focused_column() {
return;
}
// Consider the end of an ongoing animation because that's what compute to fit does too.
let view_x = self.target_view_pos();
let working_x = self.working_area.loc.x;
let working_w = self.working_area.size.w;
// Count all columns that are fully visible inside the working area.
let mut width_taken = 0.;
let mut leftmost_col_x = None;
let mut active_col_x = None;
let gap = self.options.gaps;
let col_xs = self.column_xs(self.data.iter().copied());
for (idx, col_x) in col_xs.take(self.columns.len()).enumerate() {
if col_x < view_x + working_x + gap {
// Column goes off-screen to the left.
continue;
}
leftmost_col_x.get_or_insert(col_x);
let width = self.data[idx].width;
if view_x + working_x + working_w < col_x + width + gap {
// Column goes off-screen to the right. We can stop here.
break;
}
if idx == self.active_column_idx {
active_col_x = Some(col_x);
}
width_taken += width + gap;
}
if active_col_x.is_none() {
// The active column wasn't fully on screen, so we can't meaningfully do anything.
return;
}
let col = &mut self.columns[self.active_column_idx];
cancel_resize_for_column(&mut self.interactive_resize, col);
let free_space = working_w - width_taken + gap;
let new_view_x = leftmost_col_x.unwrap() - free_space / 2. - working_x;
self.animate_view_offset(self.active_column_idx, new_view_x - active_col_x.unwrap());
// Just in case.
self.animate_view_offset_to_column(None, self.active_column_idx, None);
}
pub fn view_pos(&self) -> f64 {
self.column_x(self.active_column_idx) + self.view_offset.current()
}
@@ -2274,8 +2319,11 @@ impl<W: LayoutElement> ScrollingSpace<W> {
})
}
fn insert_hint_area(&self, insert_hint: &InsertHint) -> Option<Rectangle<f64, Logical>> {
let mut hint_area = match insert_hint.position {
pub(super) fn insert_hint_area(
&self,
position: InsertPosition,
) -> Option<Rectangle<f64, Logical>> {
let mut hint_area = match position {
InsertPosition::NewColumn(column_index) => {
if column_index == 0 || column_index == self.columns.len() {
let size =
@@ -2366,19 +2414,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
hint_area.loc.x -= self.view_pos();
}
let view_size = self.view_size;
// Make sure the hint is at least partially visible.
if matches!(insert_hint.position, InsertPosition::NewColumn(_)) {
hint_area.loc.x = hint_area.loc.x.max(-hint_area.size.w / 2.);
hint_area.loc.x = hint_area.loc.x.min(view_size.w - hint_area.size.w / 2.);
}
// Round to physical pixels.
hint_area = hint_area
.to_physical_precise_round(self.scale)
.to_logical(self.scale);
Some(hint_area)
}
@@ -2729,17 +2764,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
let scale = Scale::from(self.scale);
// Draw the insert hint.
if let Some(insert_hint) = &self.insert_hint {
if let Some(area) = self.insert_hint_area(insert_hint) {
rv.extend(
self.insert_hint_element
.render(renderer, area.loc)
.map(ScrollingSpaceRenderElement::InsertHint),
);
}
}
// Draw the closing windows on top of the other windows.
let view_rect = Rectangle::new(Point::from((self.view_pos(), 0.)), self.view_size);
for closing in self.closing_windows.iter().rev() {
@@ -2854,6 +2878,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
let gesture = ViewGesture {
current_view_offset: self.view_offset.current(),
animation: None,
tracker: SwipeTracker::new(),
delta_from_tracker: self.view_offset.current(),
stationary_view_offset: self.view_offset.stationary(),
@@ -2876,6 +2901,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
let gesture = ViewGesture {
current_view_offset: self.view_offset.current(),
animation: None,
tracker: SwipeTracker::new(),
delta_from_tracker: self.view_offset.current(),
stationary_view_offset: self.view_offset.stationary(),
@@ -2916,14 +2942,14 @@ impl<W: LayoutElement> ScrollingSpace<W> {
Some(true)
}
pub fn dnd_scroll_gesture_scroll(&mut self, delta: f64) {
pub fn dnd_scroll_gesture_scroll(&mut self, delta: f64) -> bool {
let ViewOffset::Gesture(gesture) = &mut self.view_offset else {
return;
return false;
};
let Some(last_time) = gesture.dnd_last_event_time else {
// Not a DnD scroll.
return;
return false;
};
let config = &self.options.gestures.dnd_edge_view_scroll;
@@ -2934,7 +2960,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
if delta == 0. {
// We're outside the scrolling zone.
gesture.dnd_nonzero_start_time = None;
return;
return false;
}
let nonzero_start = *gesture.dnd_nonzero_start_time.get_or_insert(now);
@@ -2943,7 +2969,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
// monitors.
let delay = Duration::from_millis(u64::from(config.delay_ms));
if now.saturating_sub(nonzero_start) < delay {
return;
return true;
}
let time_delta = now.saturating_sub(last_time).as_secs_f64();
@@ -2987,9 +3013,10 @@ impl<W: LayoutElement> ScrollingSpace<W> {
gesture.delta_from_tracker += clamped_offset - view_offset;
gesture.current_view_offset = clamped_offset;
true
}
pub fn view_offset_gesture_end(&mut self, _cancelled: bool, is_touchpad: Option<bool>) -> bool {
pub fn view_offset_gesture_end(&mut self, is_touchpad: Option<bool>) -> bool {
let ViewOffset::Gesture(gesture) = &mut self.view_offset else {
return false;
};
@@ -3279,7 +3306,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
return;
}
self.view_offset_gesture_end(false, None);
self.view_offset_gesture_end(None);
}
pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool {
@@ -3570,7 +3597,10 @@ impl ViewOffset {
match self {
ViewOffset::Static(offset) => *offset,
ViewOffset::Animation(anim) => anim.value(),
ViewOffset::Gesture(gesture) => gesture.current_view_offset,
ViewOffset::Gesture(gesture) => {
gesture.current_view_offset
+ gesture.animation.as_ref().map_or(0., |anim| anim.value())
}
}
}
@@ -3600,21 +3630,30 @@ impl ViewOffset {
matches!(self, Self::Static(_))
}
pub fn is_animation(&self) -> bool {
matches!(self, Self::Animation(_))
}
pub fn is_gesture(&self) -> bool {
matches!(self, Self::Gesture(_))
}
pub fn is_dnd_scroll(&self) -> bool {
matches!(&self, ViewOffset::Gesture(gesture) if gesture.dnd_last_event_time.is_some())
}
pub fn is_animation_ongoing(&self) -> bool {
match self {
ViewOffset::Static(_) => false,
ViewOffset::Animation(_) => true,
ViewOffset::Gesture(gesture) => gesture.animation.is_some(),
}
}
pub fn offset(&mut self, delta: f64) {
match self {
ViewOffset::Static(offset) => *offset += delta,
ViewOffset::Animation(anim) => anim.offset(delta),
ViewOffset::Gesture(_gesture) => {
// Is this needed?
error!("cancel gesture before offsetting");
ViewOffset::Gesture(gesture) => {
gesture.stationary_view_offset += delta;
gesture.delta_from_tracker += delta;
gesture.current_view_offset += delta;
}
}
}
@@ -3630,6 +3669,13 @@ impl ViewOffset {
}
}
impl ViewGesture {
fn animate_from(&mut self, from: f64, clock: Clock, config: niri_config::Animation) {
let current = self.animation.as_ref().map_or(0., Animation::value);
self.animation = Some(Animation::new(clock, from + current, 0., 0., config));
}
}
impl ColumnData {
pub fn new<W: LayoutElement>(column: &Column<W>) -> Self {
let mut rv = Self { width: 0. };
@@ -3832,8 +3878,9 @@ impl<W: LayoutElement> Column<W> {
.enumerate()
.map(|(tile_idx, (tile, tile_off))| {
let is_active = tile_idx == active_idx;
let is_urgent = tile.window().is_urgent();
let tile_pos = tile_off + tile.render_offset();
TabInfo::from_tile(tile, tile_pos, is_active, &config)
TabInfo::from_tile(tile, tile_pos, is_active, is_urgent, &config)
});
// Hide the tab indicator in fullscreen. If you have it configured to overlap the window,
+18 -7
View File
@@ -10,7 +10,9 @@ use crate::animation::{Animation, Clock};
use crate::niri_render_elements;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
use crate::utils::{floor_logical_in_physical_max1, round_logical_in_physical};
use crate::utils::{
floor_logical_in_physical_max1, round_logical_in_physical, round_logical_in_physical_max1,
};
#[derive(Debug)]
pub struct TabIndicator {
@@ -77,12 +79,14 @@ impl TabIndicator {
scale: f64,
) -> impl Iterator<Item = Rectangle<f64, Logical>> {
let round = |logical: f64| round_logical_in_physical(scale, logical);
let round_max1 = |logical: f64| round_logical_in_physical_max1(scale, logical);
let progress = self.open_anim.as_ref().map_or(1., |a| a.value().max(0.));
let width = round(self.config.width.0);
let gap = round(self.config.gap.0);
let gaps_between = round(self.config.gaps_between_tabs.0);
let width = round_max1(self.config.width.0);
let gap = self.config.gap.0;
let gap = round_max1(gap.abs()).copysign(gap);
let gaps_between = round_max1(self.config.gaps_between_tabs.0);
let position = self.config.position;
let side = match position {
@@ -346,13 +350,16 @@ impl TabInfo {
tile: &Tile<W>,
position: Point<f64, Logical>,
is_active: bool,
is_urgent: bool,
config: &niri_config::TabIndicator,
) -> Self {
let rules = tile.window().rules();
let rule = rules.tab_indicator;
let gradient_from_rule = || {
let (color, gradient) = if is_active {
let (color, gradient) = if is_urgent {
(rule.urgent_color, rule.urgent_gradient)
} else if is_active {
(rule.active_color, rule.active_gradient)
} else {
(rule.inactive_color, rule.inactive_gradient)
@@ -362,7 +369,9 @@ impl TabInfo {
};
let gradient_from_config = || {
let (color, gradient) = if is_active {
let (color, gradient) = if is_urgent {
(config.urgent_color, config.urgent_gradient)
} else if is_active {
(config.active_color, config.active_gradient)
} else {
(config.inactive_color, config.inactive_gradient)
@@ -382,7 +391,9 @@ impl TabInfo {
focus_ring_config
};
let (color, gradient) = if is_active {
let (color, gradient) = if is_urgent {
(config.urgent_color, config.urgent_gradient)
} else if is_active {
(config.active_color, config.active_gradient)
} else {
(config.inactive_color, config.inactive_gradient)
+165 -29
View File
@@ -261,6 +261,10 @@ impl LayoutElement for TestWindow {
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
None
}
fn is_urgent(&self) -> bool {
false
}
}
fn arbitrary_bbox() -> impl Strategy<Value = Rectangle<i32, Logical>> {
@@ -460,6 +464,7 @@ enum Op {
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
id: Option<usize>,
},
CenterVisibleColumns,
FocusWorkspaceDown,
FocusWorkspaceUp,
FocusWorkspace(#[proptest(strategy = "0..=4usize")] usize),
@@ -473,9 +478,9 @@ enum Op {
#[proptest(strategy = "0..=4usize")]
workspace_idx: usize,
},
MoveColumnToWorkspaceDown,
MoveColumnToWorkspaceUp,
MoveColumnToWorkspace(#[proptest(strategy = "0..=4usize")] usize),
MoveColumnToWorkspaceDown(bool),
MoveColumnToWorkspaceUp(bool),
MoveColumnToWorkspace(#[proptest(strategy = "0..=4usize")] usize, bool),
MoveWorkspaceDown,
MoveWorkspaceUp,
MoveWorkspaceToIndex {
@@ -508,7 +513,13 @@ enum Op {
#[proptest(strategy = "proptest::option::of(0..=4usize)")]
target_ws_idx: Option<usize>,
},
MoveColumnToOutput(#[proptest(strategy = "1..=5usize")] usize),
MoveColumnToOutput {
#[proptest(strategy = "1..=5usize")]
output_id: usize,
#[proptest(strategy = "proptest::option::of(0..=4usize)")]
target_ws_idx: Option<usize>,
activate: bool,
},
SwitchPresetColumnWidth,
SwitchPresetWindowWidth {
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
@@ -576,6 +587,8 @@ enum Op {
ViewOffsetGestureBegin {
#[proptest(strategy = "1..=5usize")]
output_idx: usize,
#[proptest(strategy = "proptest::option::of(0..=4usize)")]
workspace_idx: Option<usize>,
is_touchpad: bool,
},
ViewOffsetGestureUpdate {
@@ -599,9 +612,15 @@ enum Op {
is_touchpad: bool,
},
WorkspaceSwitchGestureEnd {
cancelled: bool,
is_touchpad: Option<bool>,
},
OverviewGestureBegin,
OverviewGestureUpdate {
#[proptest(strategy = "-400f64..400f64")]
delta: f64,
timestamp: Duration,
},
OverviewGestureEnd,
InteractiveMoveBegin {
#[proptest(strategy = "1..=5usize")]
window: usize,
@@ -657,6 +676,7 @@ enum Op {
#[proptest(strategy = "1..=5usize")]
window: usize,
},
ToggleOverview,
}
impl Op {
@@ -1049,6 +1069,7 @@ impl Op {
let id = id.filter(|id| layout.has_window(id));
layout.center_window(id.as_ref());
}
Op::CenterVisibleColumns => layout.center_visible_columns(),
Op::FocusWorkspaceDown => layout.switch_workspace_down(),
Op::FocusWorkspaceUp => layout.switch_workspace_up(),
Op::FocusWorkspace(idx) => layout.switch_workspace(idx),
@@ -1065,9 +1086,9 @@ impl Op {
let window_id = window_id.filter(|id| layout.has_window(id));
layout.move_to_workspace(window_id.as_ref(), workspace_idx, ActivateWindow::Smart);
}
Op::MoveColumnToWorkspaceDown => layout.move_column_to_workspace_down(),
Op::MoveColumnToWorkspaceUp => layout.move_column_to_workspace_up(),
Op::MoveColumnToWorkspace(idx) => layout.move_column_to_workspace(idx),
Op::MoveColumnToWorkspaceDown(focus) => layout.move_column_to_workspace_down(focus),
Op::MoveColumnToWorkspaceUp(focus) => layout.move_column_to_workspace_up(focus),
Op::MoveColumnToWorkspace(idx, focus) => layout.move_column_to_workspace(idx, focus),
Op::MoveWindowToOutput {
window_id,
output_id: id,
@@ -1088,13 +1109,17 @@ impl Op {
ActivateWindow::Smart,
);
}
Op::MoveColumnToOutput(id) => {
Op::MoveColumnToOutput {
output_id: id,
target_ws_idx,
activate,
} => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
layout.move_column_to_output(&output);
layout.move_column_to_output(&output, target_ws_idx, activate);
}
Op::MoveWorkspaceDown => layout.move_workspace_down(),
Op::MoveWorkspaceUp => layout.move_workspace_up(),
@@ -1345,6 +1370,7 @@ impl Op {
}
Op::ViewOffsetGestureBegin {
output_idx: id,
workspace_idx,
is_touchpad: normalize,
} => {
let name = format!("output{id}");
@@ -1352,7 +1378,7 @@ impl Op {
return;
};
layout.view_offset_gesture_begin(&output, normalize);
layout.view_offset_gesture_begin(&output, workspace_idx, normalize);
}
Op::ViewOffsetGestureUpdate {
delta,
@@ -1362,8 +1388,7 @@ impl Op {
layout.view_offset_gesture_update(delta, timestamp, is_touchpad);
}
Op::ViewOffsetGestureEnd { is_touchpad } => {
// We don't handle cancels in this gesture.
layout.view_offset_gesture_end(false, is_touchpad);
layout.view_offset_gesture_end(is_touchpad);
}
Op::WorkspaceSwitchGestureBegin {
output_idx: id,
@@ -1383,11 +1408,17 @@ impl Op {
} => {
layout.workspace_switch_gesture_update(delta, timestamp, is_touchpad);
}
Op::WorkspaceSwitchGestureEnd {
cancelled,
is_touchpad,
} => {
layout.workspace_switch_gesture_end(cancelled, is_touchpad);
Op::WorkspaceSwitchGestureEnd { is_touchpad } => {
layout.workspace_switch_gesture_end(is_touchpad);
}
Op::OverviewGestureBegin => {
layout.overview_gesture_begin();
}
Op::OverviewGestureUpdate { delta, timestamp } => {
layout.overview_gesture_update(delta, timestamp);
}
Op::OverviewGestureEnd => {
layout.overview_gesture_end();
}
Op::InteractiveMoveBegin {
window,
@@ -1442,6 +1473,9 @@ impl Op {
Op::InteractiveResizeEnd { window } => {
layout.interactive_resize_end(&window);
}
Op::ToggleOverview => {
layout.toggle_overview();
}
}
}
}
@@ -1542,10 +1576,10 @@ fn operations_dont_panic() {
window_id: None,
workspace_idx: 2,
},
Op::MoveColumnToWorkspaceDown,
Op::MoveColumnToWorkspaceUp,
Op::MoveColumnToWorkspace(1),
Op::MoveColumnToWorkspace(2),
Op::MoveColumnToWorkspaceDown(true),
Op::MoveColumnToWorkspaceUp(true),
Op::MoveColumnToWorkspace(1, true),
Op::MoveColumnToWorkspace(2, true),
Op::MoveWindowDown,
Op::MoveWindowDownOrToWorkspaceDown,
Op::MoveWindowUp,
@@ -1717,11 +1751,11 @@ fn operations_from_starting_state_dont_panic() {
window_id: None,
workspace_idx: 3,
},
Op::MoveColumnToWorkspaceDown,
Op::MoveColumnToWorkspaceUp,
Op::MoveColumnToWorkspace(1),
Op::MoveColumnToWorkspace(2),
Op::MoveColumnToWorkspace(3),
Op::MoveColumnToWorkspaceDown(true),
Op::MoveColumnToWorkspaceUp(true),
Op::MoveColumnToWorkspace(1, true),
Op::MoveColumnToWorkspace(2, true),
Op::MoveColumnToWorkspace(3, true),
Op::MoveWindowDown,
Op::MoveWindowDownOrToWorkspaceDown,
Op::MoveWindowUp,
@@ -2040,8 +2074,8 @@ fn workspace_transfer_during_switch_gets_cleaned_up() {
},
Op::RemoveOutput(1),
Op::AddOutput(2),
Op::MoveColumnToWorkspaceDown,
Op::MoveColumnToWorkspaceDown,
Op::MoveColumnToWorkspaceDown(true),
Op::MoveColumnToWorkspaceDown(true),
Op::AddOutput(1),
];
@@ -2265,6 +2299,7 @@ fn unfullscreen_view_offset_not_reset_on_gesture() {
Op::FullscreenWindow(1),
Op::ViewOffsetGestureBegin {
output_idx: 1,
workspace_idx: None,
is_touchpad: true,
},
Op::ViewOffsetGestureEnd {
@@ -3335,6 +3370,107 @@ fn interactive_resize_on_pending_unfullscreen_column() {
check_ops(&ops);
}
#[test]
fn move_column_to_workspace_unfocused_with_multiple_monitors() {
let ops = [
Op::AddOutput(1),
Op::SetWorkspaceName {
new_ws_name: 101,
ws_name: None,
},
Op::AddWindow {
params: TestWindowParams::new(1),
},
Op::FocusWorkspaceDown,
Op::SetWorkspaceName {
new_ws_name: 102,
ws_name: None,
},
Op::AddWindow {
params: TestWindowParams::new(2),
},
Op::AddOutput(2),
Op::FocusOutput(2),
Op::SetWorkspaceName {
new_ws_name: 201,
ws_name: None,
},
Op::AddWindow {
params: TestWindowParams::new(3),
},
Op::AddWindow {
params: TestWindowParams::new(4),
},
Op::MoveColumnToOutput {
output_id: 1,
target_ws_idx: Some(0),
activate: false,
},
Op::FocusOutput(1),
];
let layout = check_ops(&ops);
assert_eq!(layout.active_workspace().unwrap().name().unwrap(), "ws102");
for (mon, win) in layout.windows() {
let mon = mon.unwrap();
let ws = mon
.workspaces
.iter()
.find(|w| w.has_window(win.id()))
.unwrap();
assert_eq!(
ws.name().unwrap(),
match win.id() {
1 | 4 => "ws101",
2 => "ws102",
3 => "ws201",
_ => unreachable!(),
}
);
}
}
#[test]
fn interactive_move_unfullscreen_to_floating_stops_dnd_scroll() {
let ops = [
Op::AddOutput(3),
Op::AddWindow {
params: TestWindowParams {
is_floating: true,
..TestWindowParams::new(4)
},
},
// This moves the window to tiling.
Op::SetFullscreenWindow {
window: 4,
is_fullscreen: true,
},
// This starts a DnD scroll since we're dragging a tiled window.
Op::InteractiveMoveBegin {
window: 4,
output_idx: 3,
px: 0.0,
py: 0.0,
},
// This will cause the window to unfullscreen to floating, and should stop the DnD scroll
// since we're no longer dragging a tiled window, but rather a floating one.
Op::InteractiveMoveUpdate {
window: 4,
dx: 0.0,
dy: 15035.31210741684,
output_idx: 3,
px: 0.0,
py: 0.0,
},
Op::InteractiveMoveEnd { window: 4 },
];
check_ops(&ops);
}
fn parent_id_causes_loop(layout: &Layout<TestWindow>, id: usize, mut parent_id: usize) -> bool {
if parent_id == id {
return true;
+4 -4
View File
@@ -25,8 +25,8 @@ use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::RenderTarget;
use crate::utils::round_logical_in_physical;
use crate::utils::transaction::Transaction;
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
/// Toplevel window with decorations.
#[derive(Debug)]
@@ -366,6 +366,7 @@ impl<W: LayoutElement> Tile<W> {
self.animated_window_size(),
is_active,
!draw_border_with_background,
self.window.is_urgent(),
Rectangle::new(
view_rect.loc - Point::from((border_width, border_width)),
view_rect.size,
@@ -400,6 +401,7 @@ impl<W: LayoutElement> Tile<W> {
self.animated_tile_size(),
is_active,
!draw_focus_ring_with_background,
self.window.is_urgent(),
view_rect,
radius,
self.scale,
@@ -798,9 +800,7 @@ impl<W: LayoutElement> Tile<W> {
return Point::from((0., 0.));
}
let now = self.clock.now().as_secs_f64();
let amplitude = self.view_size.h / 96.;
let y = amplitude * ((f64::consts::TAU * now / 3.6).sin() - 1.);
let y = baba_is_float_offset(self.clock.now(), self.view_size.h);
let y = round_logical_in_physical(self.scale, y);
Point::from((0., y))
}
+94 -20
View File
@@ -2,7 +2,9 @@ use std::cmp::max;
use std::rc::Rc;
use std::time::Duration;
use niri_config::{CenterFocusedColumn, OutputName, PresetSize, Workspace as WorkspaceConfig};
use niri_config::{
CenterFocusedColumn, CornerRadius, OutputName, PresetSize, Workspace as WorkspaceConfig,
};
use niri_ipc::{ColumnDisplay, PositionChange, SizeChange};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::{layer_map_for_output, Window};
@@ -15,16 +17,18 @@ use smithay::wayland::shell::xdg::SurfaceCachedState;
use super::floating::{FloatingSpace, FloatingSpaceRenderElement};
use super::scrolling::{
Column, ColumnWidth, InsertHint, InsertPosition, ScrollDirection, ScrollingSpace,
ScrollingSpaceRenderElement,
Column, ColumnWidth, ScrollDirection, ScrollingSpace, ScrollingSpaceRenderElement,
};
use super::shadow::Shadow;
use super::tile::{Tile, TileRenderSnapshot};
use super::{
ActivateWindow, HitType, InteractiveResizeData, LayoutElement, Options, RemovedTile, SizeFrac,
ActivateWindow, HitType, InsertPosition, InteractiveResizeData, LayoutElement, Options,
RemovedTile, SizeFrac,
};
use crate::animation::Clock;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::RenderTarget;
use crate::utils::id::IdCounter;
use crate::utils::transaction::{Transaction, TransactionBlocker};
@@ -80,6 +84,9 @@ pub struct Workspace<W: LayoutElement> {
/// zones.
working_area: Rectangle<f64, Logical>,
/// This workspace's shadow in the overview.
shadow: Shadow,
/// Clock for driving animations.
pub(super) clock: Clock,
@@ -228,6 +235,9 @@ impl<W: LayoutElement> Workspace<W> {
options.clone(),
);
let shadow_config =
compute_workspace_shadow_config(options.overview.workspace_shadow, view_size);
Self {
scrolling,
floating,
@@ -237,6 +247,7 @@ impl<W: LayoutElement> Workspace<W> {
transform: output.current_transform(),
view_size,
working_area,
shadow: Shadow::new(shadow_config),
output: Some(output),
clock,
base_options,
@@ -281,6 +292,9 @@ impl<W: LayoutElement> Workspace<W> {
options.clone(),
);
let shadow_config =
compute_workspace_shadow_config(options.overview.workspace_shadow, view_size);
Self {
scrolling,
floating,
@@ -291,6 +305,7 @@ impl<W: LayoutElement> Workspace<W> {
original_output,
view_size,
working_area,
shadow: Shadow::new(shadow_config),
clock,
base_options,
options,
@@ -343,6 +358,14 @@ impl<W: LayoutElement> Workspace<W> {
let view_rect = Rectangle::from_size(self.view_size);
self.floating
.update_render_elements(is_active && self.floating_is_active.get(), view_rect);
self.shadow.update_render_elements(
self.view_size,
true,
CornerRadius::default(),
self.scale.fractional_scale(),
1.,
);
}
pub fn update_config(&mut self, base_options: Rc<Options>) {
@@ -363,6 +386,10 @@ impl<W: LayoutElement> Workspace<W> {
options.clone(),
);
let shadow_config =
compute_workspace_shadow_config(options.overview.workspace_shadow, self.view_size);
self.shadow.update_config(shadow_config);
self.base_options = base_options;
self.options = options;
}
@@ -370,6 +397,7 @@ impl<W: LayoutElement> Workspace<W> {
pub fn update_shaders(&mut self) {
self.scrolling.update_shaders();
self.floating.update_shaders();
self.shadow.update_shaders();
}
pub fn windows(&self) -> impl Iterator<Item = &W> + '_ {
@@ -501,6 +529,10 @@ impl<W: LayoutElement> Workspace<W> {
scale.fractional_scale(),
self.options.clone(),
);
let shadow_config =
compute_workspace_shadow_config(self.options.overview.workspace_shadow, size);
self.shadow.update_config(shadow_config);
}
if scale_transform_changed {
@@ -1068,6 +1100,13 @@ impl<W: LayoutElement> Workspace<W> {
}
}
pub fn center_visible_columns(&mut self) {
if self.floating_is_active.get() {
return;
}
self.scrolling.center_visible_columns();
}
pub fn toggle_width(&mut self) {
if self.floating_is_active.get() {
self.floating.toggle_window_width(None);
@@ -1409,7 +1448,10 @@ impl<W: LayoutElement> Workspace<W> {
renderer: &mut R,
target: RenderTarget,
focus_ring: bool,
) -> impl Iterator<Item = WorkspaceRenderElement<R>> {
) -> (
impl Iterator<Item = WorkspaceRenderElement<R>>,
impl Iterator<Item = WorkspaceRenderElement<R>>,
) {
let scrolling_focus_ring = focus_ring && !self.floating_is_active();
let scrolling = self
.scrolling
@@ -1424,8 +1466,16 @@ impl<W: LayoutElement> Workspace<W> {
.render_elements(renderer, view_rect, target, floating_focus_ring);
floating.into_iter().map(WorkspaceRenderElement::from)
});
let floating = floating.into_iter().flatten();
floating.into_iter().flatten().chain(scrolling)
(floating, scrolling)
}
pub fn render_shadow<R: NiriRenderer>(
&self,
renderer: &mut R,
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
self.shadow.render(renderer, Point::from((0., 0.)))
}
pub fn render_above_top_layer(&self) -> bool {
@@ -1565,6 +1615,10 @@ impl<W: LayoutElement> Workspace<W> {
self.scrolling.scroll_amount_to_activate(window)
}
pub fn is_urgent(&self) -> bool {
self.windows().any(|win| win.is_urgent())
}
pub fn activate_window(&mut self, window: &W::Id) -> bool {
if self.floating.activate_window(window) {
self.floating_is_active = FloatingActive::Yes;
@@ -1593,16 +1647,15 @@ impl<W: LayoutElement> Workspace<W> {
}
}
pub fn set_insert_hint(&mut self, insert_hint: InsertHint) {
self.scrolling.set_insert_hint(insert_hint);
pub(super) fn scrolling_insert_position(&self, pos: Point<f64, Logical>) -> InsertPosition {
self.scrolling.insert_position(pos)
}
pub fn clear_insert_hint(&mut self) {
self.scrolling.clear_insert_hint();
}
pub fn get_insert_position(&self, pos: Point<f64, Logical>) -> InsertPosition {
self.scrolling.get_insert_position(pos)
pub(super) fn insert_hint_area(
&self,
position: InsertPosition,
) -> Option<Rectangle<f64, Logical>> {
self.scrolling.insert_hint_area(position)
}
pub fn view_offset_gesture_begin(&mut self, is_touchpad: bool) {
@@ -1619,16 +1672,15 @@ impl<W: LayoutElement> Workspace<W> {
.view_offset_gesture_update(delta_x, timestamp, is_touchpad)
}
pub fn view_offset_gesture_end(&mut self, cancelled: bool, is_touchpad: Option<bool>) -> bool {
self.scrolling
.view_offset_gesture_end(cancelled, is_touchpad)
pub fn view_offset_gesture_end(&mut self, is_touchpad: Option<bool>) -> bool {
self.scrolling.view_offset_gesture_end(is_touchpad)
}
pub fn dnd_scroll_gesture_begin(&mut self) {
self.scrolling.dnd_scroll_gesture_begin();
}
pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>) {
pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>, speed: f64) -> bool {
let config = &self.options.gestures.dnd_edge_view_scroll;
let trigger_width = config.trigger_width.0;
@@ -1654,8 +1706,9 @@ impl<W: LayoutElement> Workspace<W> {
// Normalize to [0, 1].
delta / trigger_width
};
let delta = delta * speed;
self.scrolling.dnd_scroll_gesture_scroll(delta);
self.scrolling.dnd_scroll_gesture_scroll(delta)
}
pub fn dnd_scroll_gesture_end(&mut self) {
@@ -1706,6 +1759,10 @@ impl<W: LayoutElement> Workspace<W> {
self.floating.logical_to_size_frac(logical_pos)
}
pub fn working_area(&self) -> Rectangle<f64, Logical> {
self.working_area
}
#[cfg(test)]
pub fn scrolling(&self) -> &ScrollingSpace<W> {
&self.scrolling
@@ -1775,6 +1832,23 @@ impl<W: LayoutElement> Workspace<W> {
}
}
fn compute_working_area(output: &Output) -> Rectangle<f64, Logical> {
pub(super) fn compute_working_area(output: &Output) -> Rectangle<f64, Logical> {
layer_map_for_output(output).non_exclusive_zone().to_f64()
}
fn compute_workspace_shadow_config(
config: niri_config::WorkspaceShadow,
view_size: Size<f64, Logical>,
) -> niri_config::Shadow {
// Gaps between workspaces are a multiple of the view height, so shadow settings should also be
// normalized to the view height to prevent them from overlapping on lower resolutions.
let norm = view_size.h / 1080.;
let mut config = niri_config::Shadow::from(config);
config.softness.0 *= norm;
config.spread.0 *= norm;
config.offset.x.0 *= norm;
config.offset.y.0 *= norm;
config
}
+5 -7
View File
@@ -161,12 +161,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
let mut config_errored = false;
let mut config = Config::load(&path)
.map_err(|err| {
warn!("{err:?}");
config_errored = true;
})
let config_load_result = Config::load(&path);
let config_errored = config_load_result.is_err();
let mut config = config_load_result
.map_err(|err| warn!("{err:?}"))
.unwrap_or_default();
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
@@ -355,7 +353,7 @@ fn config_path(cli_path: Option<PathBuf>) -> (PathBuf, PathBuf, bool) {
let system_path = system_config_path();
if let Some(path) = default_config_path() {
if path.exists() {
return (path.clone(), path, true);
return (path.clone(), path, false);
}
if system_path.exists() {
+530 -131
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -83,6 +83,7 @@ pub struct Cast {
scheduled_redraw: Option<RegistrationToken>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum CastState {
ResizePending {
+12 -7
View File
@@ -1,10 +1,11 @@
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
use smithay::backend::renderer::element::{Element, Id, Kind};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::Color32F;
use smithay::utils::Scale;
use super::renderer::NiriRenderer;
use super::solid_color::SolidColorRenderElement;
use crate::niri::OutputRenderElements;
pub fn draw_opaque_regions<R: NiriRenderer>(
@@ -35,9 +36,9 @@ pub fn draw_opaque_regions<R: NiriRenderer>(
for rect in opaque {
let color = SolidColorRenderElement::new(
Id::new(),
rect,
rect.to_f64().to_logical(scale),
CommitCounter::default(),
[0., 0., 0.2, 0.2],
Color32F::from([0., 0., 0.2, 0.2]),
Kind::Unspecified,
);
elements.insert(i - 1, OutputRenderElements::SolidColor(color));
@@ -47,9 +48,9 @@ pub fn draw_opaque_regions<R: NiriRenderer>(
for rect in semitransparent {
let color = SolidColorRenderElement::new(
Id::new(),
rect,
rect.to_f64().to_logical(scale),
CommitCounter::default(),
[0.3, 0., 0., 0.3],
Color32F::from([0.3, 0., 0., 0.3]),
Kind::Unspecified,
);
elements.insert(i - 1, OutputRenderElements::SolidColor(color));
@@ -64,6 +65,10 @@ pub fn draw_damage<R: NiriRenderer>(
) {
let _span = tracy_client::span!("draw_damage");
let Ok((_, scale, _)) = damage_tracker.mode().try_into() else {
return;
};
let Ok((Some(damage), _)) = damage_tracker.damage_output(1, elements) else {
return;
};
@@ -71,9 +76,9 @@ pub fn draw_damage<R: NiriRenderer>(
for rect in damage {
let color = SolidColorRenderElement::new(
Id::new(),
*rect,
rect.to_f64().to_logical(scale),
CommitCounter::default(),
[0.3, 0., 0., 0.3],
Color32F::from([0.3, 0., 0., 0.3]),
Kind::Unspecified,
);
elements.insert(0, OutputRenderElements::SolidColor(color));
+9 -9
View File
@@ -13,7 +13,7 @@ use smithay::backend::renderer::utils::{
CommitCounter, DamageBag, DamageSet, DamageSnapshot, OpaqueRegions,
};
use smithay::backend::renderer::{
Bind as _, Color32F, Frame as _, Offscreen as _, Renderer, Texture as _,
Bind as _, Color32F, ContextId, Frame as _, Offscreen as _, Renderer, Texture as _,
};
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
@@ -36,8 +36,8 @@ pub struct OffscreenBuffer {
struct Inner {
/// The texture with offscreened contents.
texture: GlesTexture,
/// Id of the renderer that the texture comes from.
renderer_id: usize,
/// Id of the renderer context that the texture comes from.
renderer_context_id: ContextId<GlesTexture>,
/// Scale of the texture.
scale: Scale<f64>,
/// Damage tracker for drawing to the texture.
@@ -50,7 +50,7 @@ struct Inner {
pub struct OffscreenRenderElement {
id: Id,
texture: GlesTexture,
renderer_id: usize,
renderer_context_id: ContextId<GlesTexture>,
scale: Scale<f64>,
damage: DamageSnapshot<i32, Buffer>,
offset: Point<f64, Logical>,
@@ -92,7 +92,7 @@ impl OffscreenBuffer {
let mut reason = "";
if let Some(Inner {
texture,
renderer_id,
renderer_context_id,
..
}) = inner.as_mut()
{
@@ -109,7 +109,7 @@ impl OffscreenBuffer {
reason = "not unique";
*inner = None;
} else if *renderer_id != renderer.id() {
} else if *renderer_context_id != renderer.context_id() {
reason = "renderer id changed";
*inner = None;
@@ -134,7 +134,7 @@ impl OffscreenBuffer {
inner.insert(Inner {
texture,
renderer_id: renderer.id(),
renderer_context_id: renderer.context_id(),
scale,
damage,
outer_damage: DamageBag::default(),
@@ -180,7 +180,7 @@ impl OffscreenBuffer {
let elem = OffscreenRenderElement {
id: self.id.clone(),
texture: inner.texture.clone(),
renderer_id: inner.renderer_id,
renderer_context_id: inner.renderer_context_id.clone(),
scale,
damage: inner.outer_damage.snapshot(),
offset,
@@ -305,7 +305,7 @@ impl RenderElement<GlesRenderer> for OffscreenRenderElement {
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
if frame.id() != self.renderer_id {
if frame.context_id() != self.renderer_context_id {
warn!("trying to render texture from different renderer");
return Ok(());
}
+1
View File
@@ -29,6 +29,7 @@ macro_rules! niri_render_elements {
// in this line, we cannot condition based on $R like elsewhere, so we condition on duplicate
// names instead. Like this: $($name_R<SomeRenderer>)? $($name_no_R)? so only one is chosen.
(@impl $name:ident ($($name_no_R:ident)?) ($($name_R:ident<$R:ident>)?) => { $($variant:ident = $type:ty),+ }) => {
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum $name$(<$R: $crate::render_helpers::renderer::NiriRenderer>)? {
$($variant($type)),+
+5
View File
@@ -245,6 +245,11 @@ impl ShaderRenderElement {
self.area.loc = location;
self
}
pub fn with_alpha(mut self, alpha: f32) -> Self {
self.alpha = alpha;
self
}
}
impl Element for ShaderRenderElement {
+5
View File
@@ -175,6 +175,11 @@ impl ShadowRenderElement {
self
}
pub fn with_alpha(mut self, alpha: f32) -> Self {
self.inner = self.inner.with_alpha(alpha);
self
}
pub fn has_shader(renderer: &mut impl NiriRenderer) -> bool {
Shaders::get(renderer)
.program(ProgramType::Shadow)
+1 -1
View File
@@ -53,7 +53,7 @@ pub fn render_snapshot_from_surface_tree(
}
let data = data.lock().unwrap();
let Some(texture) = data.texture::<GlesRenderer>(renderer.id()) else {
let Some(texture) = data.texture(renderer.context_id()) else {
return;
};
+8 -8
View File
@@ -2,17 +2,17 @@ use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::GlesTexture;
use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions};
use smithay::backend::renderer::{Frame as _, ImportMem, Renderer, Texture};
use smithay::backend::renderer::{ContextId, Frame as _, ImportMem, Renderer, Texture};
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use super::memory::MemoryBuffer;
/// Smithay's texture buffer, but with fractional scale.
#[derive(Debug, Clone)]
pub struct TextureBuffer<T> {
pub struct TextureBuffer<T: Texture> {
id: Id,
commit_counter: CommitCounter,
renderer_id: usize,
renderer_context_id: ContextId<T>,
texture: T,
scale: Scale<f64>,
transform: Transform,
@@ -21,7 +21,7 @@ pub struct TextureBuffer<T> {
/// Render element for a [`TextureBuffer`].
#[derive(Debug, Clone)]
pub struct TextureRenderElement<T> {
pub struct TextureRenderElement<T: Texture> {
buffer: TextureBuffer<T>,
location: Point<f64, Logical>,
alpha: f32,
@@ -30,7 +30,7 @@ pub struct TextureRenderElement<T> {
kind: Kind,
}
impl<T> TextureBuffer<T> {
impl<T: Texture> TextureBuffer<T> {
pub fn from_texture<R: Renderer<TextureId = T>>(
renderer: &R,
texture: T,
@@ -41,7 +41,7 @@ impl<T> TextureBuffer<T> {
TextureBuffer {
id: Id::new(),
commit_counter: CommitCounter::default(),
renderer_id: renderer.id(),
renderer_context_id: renderer.context_id(),
texture,
scale: scale.into(),
transform,
@@ -122,7 +122,7 @@ impl TextureBuffer<GlesTexture> {
}
}
impl<T> TextureRenderElement<T> {
impl<T: Texture> TextureRenderElement<T> {
pub fn from_texture_buffer(
buffer: TextureBuffer<T>,
location: impl Into<Point<f64, Logical>>,
@@ -226,7 +226,7 @@ where
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), R::Error> {
if frame.id() != self.buffer.renderer_id {
if frame.context_id() != self.buffer.renderer_context_id {
warn!("trying to render texture from different renderer");
return Ok(());
}
+1
View File
@@ -6,4 +6,5 @@ mod server;
mod floating;
mod fullscreen;
mod transactions;
mod window_opening;
+105
View File
@@ -0,0 +1,105 @@
use std::fmt::Write as _;
use insta::assert_snapshot;
use niri_ipc::SizeChange;
use wayland_client::protocol::wl_surface::WlSurface;
use super::client::ClientId;
use super::*;
use crate::layout::LayoutElement;
use crate::niri::Niri;
fn format_window_sizes(niri: &Niri) -> String {
let mut buf = String::new();
for (_out, mapped) in niri.layout.windows() {
let size = mapped.size();
writeln!(&mut buf, "{} × {}", size.w, size.h).unwrap();
}
buf
}
fn create_window(f: &mut Fixture, id: ClientId, w: u16, h: u16) -> WlSurface {
let window = f.client(id).create_window();
let surface = window.surface.clone();
window.commit();
f.roundtrip(id);
let window = f.client(id).window(&surface);
window.attach_new_buffer();
window.set_size(w, h);
window.ack_last_and_commit();
f.roundtrip(id);
surface
}
#[test]
fn column_resize_waits_for_both_windows() {
let mut f = Fixture::new();
f.add_output(1, (1920, 1080));
let id = f.add_client();
let surface1 = create_window(&mut f, id, 100, 100);
let surface2 = create_window(&mut f, id, 200, 200);
f.double_roundtrip(id);
let _ = f.client(id).window(&surface1).recent_configures();
let _ = f.client(id).window(&surface2).recent_configures();
// Consume into one column.
f.niri().layout.consume_or_expel_window_left(None);
f.double_roundtrip(id);
// Commit for the column consume.
let window = f.client(id).window(&surface1);
assert_snapshot!(
window.format_recent_configures(),
@"size: 936 × 516, bounds: 1888 × 1048, states: []"
);
window.ack_last_and_commit();
let window = f.client(id).window(&surface2);
assert_snapshot!(
window.format_recent_configures(),
@"size: 936 × 516, bounds: 1888 × 1048, states: [Activated]"
);
window.ack_last_and_commit();
f.double_roundtrip(id);
// This should say 100 × 100 and 200 × 200.
assert_snapshot!(format_window_sizes(f.niri()), @r"
100 × 100
200 × 200
");
// Issue a resize.
f.niri()
.layout
.set_column_width(SizeChange::AdjustFixed(10));
f.double_roundtrip(id);
// Commit window 1 in response to resize.
let window = f.client(id).window(&surface1);
window.set_size(300, 300);
window.ack_last_and_commit();
f.double_roundtrip(id);
// This should still say 100 × 100 as we're waiting in a transaction for the second window.
assert_snapshot!(format_window_sizes(f.niri()), @r"
100 × 100
200 × 200
");
// Commit window 2 in response to resize.
let window = f.client(id).window(&surface2);
window.set_size(400, 400);
window.ack_last_and_commit();
f.double_roundtrip(id);
// This should say 300 × 300 and 400 × 400 as the transaction completed.
assert_snapshot!(format_window_sizes(f.niri()), @r"
300 × 300
400 × 400
");
}
+14 -12
View File
@@ -211,33 +211,33 @@ fn render(
]);
// Prefer move-column-to-workspace-down, but fall back to move-window-to-workspace-down.
if binds
if let Some(bind) = binds
.iter()
.any(|bind| bind.action == Action::MoveColumnToWorkspaceDown)
.find(|bind| matches!(bind.action, Action::MoveColumnToWorkspaceDown(_)))
{
actions.push(&Action::MoveColumnToWorkspaceDown);
actions.push(&bind.action);
} else if binds
.iter()
.any(|bind| bind.action == Action::MoveWindowToWorkspaceDown)
.any(|bind| matches!(bind.action, Action::MoveWindowToWorkspaceDown))
{
actions.push(&Action::MoveWindowToWorkspaceDown);
} else {
actions.push(&Action::MoveColumnToWorkspaceDown);
actions.push(&Action::MoveColumnToWorkspaceDown(true));
}
// Same for -up.
if binds
if let Some(bind) = binds
.iter()
.any(|bind| bind.action == Action::MoveColumnToWorkspaceUp)
.find(|bind| matches!(bind.action, Action::MoveColumnToWorkspaceUp(_)))
{
actions.push(&Action::MoveColumnToWorkspaceUp);
actions.push(&bind.action);
} else if binds
.iter()
.any(|bind| bind.action == Action::MoveWindowToWorkspaceUp)
.any(|bind| matches!(bind.action, Action::MoveWindowToWorkspaceUp))
{
actions.push(&Action::MoveWindowToWorkspaceUp);
} else {
actions.push(&Action::MoveColumnToWorkspaceUp);
actions.push(&Action::MoveColumnToWorkspaceUp(true));
}
actions.extend(&[
@@ -247,6 +247,7 @@ fn render(
&Action::ConsumeOrExpelWindowRight,
&Action::ToggleWindowFloating,
&Action::SwitchFocusBetweenFloatingAndTiling,
&Action::ToggleOverview,
]);
// Screenshot is not as important, can omit if not bound.
@@ -423,8 +424,8 @@ fn action_name(action: &Action) -> String {
Action::MoveColumnRight => String::from("Move Column Right"),
Action::FocusWorkspaceDown => String::from("Switch Workspace Down"),
Action::FocusWorkspaceUp => String::from("Switch Workspace Up"),
Action::MoveColumnToWorkspaceDown => String::from("Move Column to Workspace Down"),
Action::MoveColumnToWorkspaceUp => String::from("Move Column to Workspace Up"),
Action::MoveColumnToWorkspaceDown(_) => String::from("Move Column to Workspace Down"),
Action::MoveColumnToWorkspaceUp(_) => String::from("Move Column to Workspace Up"),
Action::MoveWindowToWorkspaceDown => String::from("Move Window to Workspace Down"),
Action::MoveWindowToWorkspaceUp => String::from("Move Window to Workspace Up"),
Action::SwitchPresetColumnWidth => String::from("Switch Preset Column Widths"),
@@ -435,6 +436,7 @@ fn action_name(action: &Action) -> String {
Action::SwitchFocusBetweenFloatingAndTiling => {
String::from("Switch Focus Between Floating and Tiling")
}
Action::ToggleOverview => String::from("Open the Overview"),
Action::Screenshot(_) => String::from("Take a Screenshot"),
Action::Spawn(args) => format!(
"Spawn <span face='monospace' bgcolor='#000000'>{}</span>",
+206 -51
View File
@@ -1,6 +1,7 @@
use std::cell::RefCell;
use std::cmp::{max, min};
use std::collections::HashMap;
use std::f64::consts::TAU;
use std::iter::zip;
use std::rc::Rc;
@@ -11,14 +12,14 @@ use niri_ipc::SizeChange;
use pango::{Alignment, FontDescription};
use pangocairo::cairo::{self, ImageSurface};
use smithay::backend::allocator::Fourcc;
use smithay::backend::input::{ButtonState, MouseButton};
use smithay::backend::input::TouchSlot;
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::{ExportMem, Texture as _};
use smithay::input::keyboard::{Keysym, ModifiersState};
use smithay::output::{Output, WeakOutput};
use smithay::utils::{Physical, Point, Rectangle, Scale, Size, Transform};
use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Size, Transform};
use crate::animation::{Animation, Clock};
use crate::layout::floating::DIRECTIONAL_MOVE_PX;
@@ -32,6 +33,7 @@ use crate::utils::to_physical_precise_round;
const SELECTION_BORDER: i32 = 2;
const PADDING: i32 = 8;
const RADIUS: i32 = 16;
const FONT: &str = "sans 14px";
const BORDER: i32 = 4;
const TEXT_HIDE_P: &str =
@@ -56,7 +58,7 @@ pub enum ScreenshotUi {
Open {
selection: (Output, Point<i32, Physical>, Point<i32, Physical>),
output_data: HashMap<Output, OutputData>,
mouse_down: bool,
button: Button,
show_pointer: bool,
open_anim: Animation,
clock: Clock,
@@ -64,6 +66,15 @@ pub enum ScreenshotUi {
},
}
pub enum Button {
Up,
Down {
touch_slot: Option<TouchSlot>,
on_capture_button: bool,
last_pos: (Output, Point<i32, Physical>),
},
}
pub struct OutputData {
size: Size<i32, Physical>,
scale: f64,
@@ -88,6 +99,22 @@ niri_render_elements! {
}
}
impl Button {
fn is_down(&self) -> bool {
matches!(self, Self::Down { .. })
}
fn is_dragging_selection(&self) -> bool {
matches!(
self,
Self::Down {
on_capture_button: false,
..
}
)
}
}
impl ScreenshotUi {
pub fn new(clock: Clock, config: Rc<RefCell<Config>>) -> Self {
Self::Closed {
@@ -193,7 +220,7 @@ impl ScreenshotUi {
*self = Self::Open {
selection,
output_data,
mouse_down: false,
button: Button::Up,
show_pointer,
open_anim,
clock: clock.clone(),
@@ -489,7 +516,7 @@ impl ScreenshotUi {
let Self::Open {
output_data,
show_pointer,
mouse_down,
button,
open_anim,
..
} = self
@@ -509,17 +536,15 @@ impl ScreenshotUi {
// The help panel goes on top.
if let Some((show, hide)) = &output_data.panel {
let buffer = if *show_pointer { hide } else { show };
let size = buffer.texture().size();
let padding: i32 = to_physical_precise_round(scale, PADDING);
let x = max(0, (output_data.size.w - size.w) / 2);
let y = max(0, output_data.size.h - size.h - padding * 2);
let location = Point::<_, Physical>::from((x, y))
let alpha = if button.is_dragging_selection() {
0.3
} else {
0.9
};
let location = panel_location(output_data, buffer.texture().size())
.to_f64()
.to_logical(scale);
let alpha = if *mouse_down { 0.3 } else { 0.9 };
let elem = PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
buffer.clone(),
location,
@@ -660,76 +685,155 @@ impl ScreenshotUi {
}
/// The pointer has moved to `point` relative to the current selection output.
pub fn pointer_motion(&mut self, point: Point<i32, Physical>) {
pub fn pointer_motion(&mut self, point: Point<i32, Physical>, slot: Option<TouchSlot>) {
let Self::Open {
selection,
mouse_down: true,
button:
Button::Down {
touch_slot,
on_capture_button,
last_pos,
},
..
} = self
else {
return;
};
if *touch_slot != slot {
return;
}
last_pos.1 = point;
if *on_capture_button {
return;
}
selection.2 = point;
self.update_buffers();
}
pub fn pointer_button(
pub fn pointer_down(
&mut self,
output: Output,
point: Point<i32, Physical>,
button: MouseButton,
state: ButtonState,
slot: Option<TouchSlot>,
) -> bool {
let Self::Open {
selection,
output_data,
mouse_down,
show_pointer,
button,
..
} = self
else {
return false;
};
if button != MouseButton::Left {
if button.is_down() {
return false;
}
let down = state == ButtonState::Pressed;
if *mouse_down == down {
let Some(output_data) = output_data.get(&output) else {
return false;
}
};
if down && !output_data.contains_key(&output) {
return false;
}
if let Some((show, hide)) = &output_data.panel {
let buffer = if *show_pointer { hide } else { show };
let panel_size = buffer.texture().size();
let location = panel_location(output_data, panel_size);
*mouse_down = down;
if down {
*selection = (output, point, point);
} else {
// Check if the resulting selection is zero-sized, and try to come up with a small
// default rectangle.
let (output, a, b) = selection;
let mut rect = rect_from_corner_points(*a, *b);
if rect.size.is_empty() || rect.size == Size::from((1, 1)) {
let data = &output_data[output];
rect = Rectangle::new(
Point::from((rect.loc.x - 16, rect.loc.y - 16)),
Size::from((32, 32)),
)
.intersection(Rectangle::from_size(data.size))
.unwrap_or_default();
*a = rect.loc;
*b = rect.loc + rect.size - Size::from((1, 1));
if is_within_capture_button(output_data.scale, panel_size, point - location) {
*button = Button::Down {
touch_slot: slot,
on_capture_button: true,
last_pos: (output, point),
};
return false;
}
}
*button = Button::Down {
touch_slot: slot,
on_capture_button: false,
last_pos: (output.clone(), point),
};
*selection = (output, point, point);
self.update_buffers();
true
}
pub fn pointer_up(&mut self, slot: Option<TouchSlot>) -> Option<bool> {
let Self::Open {
selection,
output_data,
button,
show_pointer,
..
} = self
else {
return None;
};
let Button::Down {
touch_slot,
on_capture_button,
ref last_pos,
} = *button
else {
return None;
};
if touch_slot != slot {
return None;
}
let last_pos = last_pos.clone();
*button = Button::Up;
// Check if we released still on the capture button.
if on_capture_button {
let (output, point) = last_pos;
#[allow(clippy::question_mark)]
let Some(output_data) = output_data.get(&output) else {
return None;
};
if let Some((show, hide)) = &output_data.panel {
let buffer = if *show_pointer { hide } else { show };
let panel_size = buffer.texture().size();
let location = panel_location(output_data, panel_size);
if is_within_capture_button(output_data.scale, panel_size, point - location) {
return Some(true);
}
}
}
// Check if the resulting selection is zero-sized, and try to come up with a small
// default rectangle.
let (output, a, b) = selection;
let mut rect = rect_from_corner_points(*a, *b);
if rect.size.is_empty() || rect.size == Size::from((1, 1)) {
let data = &output_data[output];
rect = Rectangle::new(
Point::from((rect.loc.x - 16, rect.loc.y - 16)),
Size::from((32, 32)),
)
.intersection(Rectangle::from_size(data.size))
.unwrap_or_default();
*a = rect.loc;
*b = rect.loc + rect.size - Size::from((1, 1));
}
self.update_buffers();
Some(false)
}
}
impl OutputScreenshot {
@@ -819,6 +923,29 @@ pub fn rect_from_corner_points(
Rectangle::from_extremities((x1, y1), (x2 + 1, y2 + 1))
}
fn panel_location(output_data: &OutputData, panel_size: Size<i32, Buffer>) -> Point<i32, Physical> {
let scale = output_data.scale;
let padding: i32 = to_physical_precise_round(scale, PADDING);
let x = max(0, (output_data.size.w - panel_size.w) / 2);
let y = max(0, output_data.size.h - panel_size.h - padding * 2);
Point::from((x, y))
}
fn is_within_capture_button(
scale: f64,
panel_size: Size<i32, Buffer>,
pos_within_panel: Point<i32, Physical>,
) -> bool {
let padding: i32 = to_physical_precise_round(scale, PADDING);
let radius = to_physical_precise_round::<i32>(scale, RADIUS) - 2;
let xc = padding + radius;
let yc = panel_size.h / 2;
let pos = pos_within_panel;
(pos.x - xc) * (pos.x - xc) + (pos.y - yc) * (pos.y - yc) <= radius * radius
}
fn render_panel(
renderer: &mut GlesRenderer,
scale: f64,
@@ -827,6 +954,11 @@ fn render_panel(
let _span = tracy_client::span!("screenshot_ui::render_panel");
let padding: i32 = to_physical_precise_round(scale, PADDING);
// Keep the border width even to avoid blurry edges.
let border_width = (f64::from(BORDER) / 2. * scale).round() * 2.;
let half_border_width = (border_width / 2.) as i32;
let radius: i32 = to_physical_precise_round(scale, RADIUS);
let circle_stroke: f64 = to_physical_precise_round(scale, 2.);
// Add 2 px of spacing to separate the backgrounds of the "Space" and "P" keys.
let spacing = to_physical_precise_round::<i32>(scale, 2) * 1024;
@@ -839,12 +971,14 @@ fn render_panel(
let layout = pangocairo::functions::create_layout(&cr);
layout.context().set_round_glyph_positions(false);
layout.set_font_description(Some(&font));
layout.set_alignment(Alignment::Center);
layout.set_alignment(Alignment::Left);
layout.set_markup(text);
layout.set_spacing(spacing);
let (mut width, mut height) = layout.pixel_size();
width += padding * 2;
width += padding + radius * 2 + padding - half_border_width + padding;
height = max(height, radius * 2);
height += padding * 2;
let surface = ImageSurface::create(cairo::Format::ARgb32, width, height)?;
@@ -852,11 +986,33 @@ fn render_panel(
cr.set_source_rgb(0.1, 0.1, 0.1);
cr.paint()?;
cr.move_to(padding.into(), padding.into());
let padding = f64::from(padding);
let half_border_width = f64::from(half_border_width);
let r = f64::from(radius);
let yc = f64::from(height / 2);
cr.new_sub_path();
cr.arc(padding + r, yc, r, 0., TAU);
cr.set_source_rgb(1., 1., 1.);
cr.fill()?;
cr.new_sub_path();
cr.arc(padding + r, yc, r - circle_stroke, 0., TAU);
cr.set_source_rgb(0.1, 0.1, 0.1);
cr.fill()?;
cr.new_sub_path();
cr.arc(padding + r, yc, r - circle_stroke * 2., 0., TAU);
cr.set_source_rgb(1., 1., 1.);
cr.fill()?;
cr.move_to(padding + r * 2. + padding - half_border_width, padding);
let layout = pangocairo::functions::create_layout(&cr);
layout.context().set_round_glyph_positions(false);
layout.set_font_description(Some(&font));
layout.set_alignment(Alignment::Center);
layout.set_alignment(Alignment::Left);
layout.set_markup(text);
layout.set_spacing(spacing);
@@ -869,8 +1025,7 @@ fn render_panel(
cr.line_to(0., height.into());
cr.line_to(0., 0.);
cr.set_source_rgb(0.3, 0.3, 0.3);
// Keep the border width even to avoid blurry edges.
cr.set_line_width((f64::from(BORDER) / 2. * scale).round() * 2.);
cr.set_line_width(border_width);
cr.stroke()?;
drop(cr);
+7
View File
@@ -1,4 +1,5 @@
use std::cmp::{max, min};
use std::f64;
use std::ffi::{CString, OsStr};
use std::io::Write;
use std::os::unix::prelude::OsStrExt;
@@ -396,6 +397,12 @@ pub fn center_preferring_top_left_in_area(
area.loc + offset
}
pub fn baba_is_float_offset(now: Duration, view_height: f64) -> f64 {
let now = now.as_secs_f64();
let amplitude = view_height / 96.;
amplitude * ((f64::consts::TAU * now / 3.6).sin() - 1.)
}
#[cfg(feature = "dbus")]
pub fn show_screenshot_notification(image_path: Option<PathBuf>) -> anyhow::Result<()> {
use std::collections::HashMap;
+2
View File
@@ -93,6 +93,8 @@ impl Transaction {
let _span = trace_span!("deadline timer", transaction = ?Weak::as_ptr(&inner))
.entered();
// FIXME: come up with some way to control the deadline timer from tests.
#[cfg(not(test))]
if let Some(inner) = inner.upgrade() {
trace!("deadline reached, completing transaction");
inner.complete();
+23
View File
@@ -77,6 +77,9 @@ pub struct Mapped {
/// If `None`, then the window is not offscreened.
offscreen_data: RefCell<Option<OffscreenData>>,
/// Whether this has an urgent indicator.
is_urgent: bool,
/// Whether this window has the keyboard focus.
is_focused: bool,
@@ -231,6 +234,7 @@ impl Mapped {
needs_configure: false,
needs_frame_callback: false,
offscreen_data: RefCell::new(None),
is_urgent: false,
is_focused: false,
is_active_in_column: true,
is_floating: false,
@@ -328,6 +332,7 @@ impl Mapped {
}
self.is_focused = is_focused;
self.is_urgent = false;
self.need_to_recompute_rules = true;
}
@@ -510,6 +515,20 @@ impl Mapped {
pub fn is_windowed_fullscreen(&self) -> bool {
self.is_windowed_fullscreen
}
pub fn set_urgent(&mut self, urgent: bool) {
if self.is_focused && urgent {
return;
}
let changed = self.is_urgent != urgent;
self.is_urgent = urgent;
self.need_to_recompute_rules |= changed;
}
pub fn is_urgent(&self) -> bool {
self.is_urgent
}
}
impl Drop for Mapped {
@@ -830,6 +849,10 @@ impl LayoutElement for Mapped {
}
}
fn is_urgent(&self) -> bool {
self.is_urgent
}
fn set_activated(&mut self, active: bool) {
let changed = self.toplevel().with_pending_state(|state| {
if active {
+20 -1
View File
@@ -100,7 +100,7 @@ pub struct ResolvedWindowRules {
/// Whether to clip this window to its geometry, including the corner radius.
pub clip_to_geometry: Option<bool>,
/// Whether bob this window up and down.
/// Whether to bob this window up and down.
pub baba_is_float: Option<bool>,
/// Whether to block out this window from certain render targets.
@@ -131,6 +131,13 @@ impl<'a> WindowRef<'a> {
}
}
pub fn is_urgent(self) -> bool {
match self {
WindowRef::Unmapped(_) => false,
WindowRef::Mapped(mapped) => mapped.is_urgent(),
}
}
pub fn is_active_in_column(self) -> bool {
match self {
WindowRef::Unmapped(_) => true,
@@ -184,8 +191,10 @@ impl ResolvedWindowRules {
width: None,
active_color: None,
inactive_color: None,
urgent_color: None,
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
},
border: BorderRule {
off: false,
@@ -193,8 +202,10 @@ impl ResolvedWindowRules {
width: None,
active_color: None,
inactive_color: None,
urgent_color: None,
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
},
shadow: ShadowRule {
off: false,
@@ -209,8 +220,10 @@ impl ResolvedWindowRules {
tab_indicator: TabIndicatorRule {
active_color: None,
inactive_color: None,
urgent_color: None,
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
},
draw_border_with_background: None,
opacity: None,
@@ -427,6 +440,12 @@ fn window_matches(window: WindowRef, role: &XdgToplevelSurfaceRoleAttributes, m:
}
}
if let Some(is_urgent) = m.is_urgent {
if window.is_urgent() != is_urgent {
return false;
}
}
if let Some(is_active) = m.is_active {
// Our "is-active" definition corresponds to the window having a pending Activated state.
let pending_activated = server_pending
+2
View File
@@ -77,6 +77,8 @@ On some systems, Steam will show a fully black window.
To fix this, navigate to Settings -> Interface (via Steam's tray icon, or by blindly finding the Steam menu at the top left of the window), then **disable** GPU accelerated rendering in web views.
Restart Steam and it should now work fine.
If you do not want to disable GPU accelerated rendering you can instead try to pass the launch argument `-system-composer` instead.
Steam notifications don't run through the standard notification daemon and show up as floating windows in the center of the screen.
You can move them to a more convenient location by adding a window rule in your niri config:
+21 -3
View File
@@ -50,6 +50,10 @@ animations {
duration-ms 200
curve "ease-out-quad"
}
overview-open-close {
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
}
}
```
@@ -159,7 +163,7 @@ animations {
##### `custom-shader`
<sup>Since: 0.1.6, experimental</sup>
<sup>Since: 0.1.6</sup>
You can write a custom shader for drawing the window during an open animation.
@@ -219,7 +223,7 @@ animations {
##### `custom-shader`
<sup>Since: 0.1.6, experimental</sup>
<sup>Since: 0.1.6</sup>
You can write a custom shader for drawing the window during a close animation.
@@ -315,7 +319,7 @@ animations {
##### `custom-shader`
<sup>Since: 0.1.6, experimental</sup>
<sup>Since: 0.1.6</sup>
You can write a custom shader for drawing the window during a resize animation.
@@ -374,6 +378,20 @@ animations {
}
```
#### `overview-open-close`
<sup>Since: 25.05</sup>
The open/close zoom animation of the [Overview](./Overview.md).
```kdl
animations {
overview-open-close {
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
}
}
```
### Synchronized Animations
<sup>Since: 0.1.5</sup>
+3 -3
View File
@@ -4,7 +4,7 @@ Niri has several options that are only useful for debugging, or are experimental
They are not meant for normal use.
> [!CAUTION]
> These options are **not** covered by the [config breaking change policy](./Configuration:-Overview.md#breaking-change-policy).
> These options are **not** covered by the [config breaking change policy](./Configuration:-Introduction.md#breaking-change-policy).
> They can change or stop working at any point with little notice.
Here are all the options at a glance:
@@ -155,7 +155,7 @@ debug {
### `wait-for-frame-completion-in-pipewire`
<sup>Since: next release</sup>
<sup>Since: 25.05</sup>
Wait until every screencast frame is done rendering before handing it over to PipeWire.
@@ -258,7 +258,7 @@ debug {
### `honor-xdg-activation-with-invalid-serial`
<sup>Since: next release</sup>
<sup>Since: 25.05</sup>
Widely-used clients such as Discord and Telegram make fresh xdg-activation tokens upon clicking on their tray icon or on their notification.
Most of the time, these fresh tokens will have invalid serials, because the app needs to be focused to get a valid serial, and if the user clicks on a tray icon or a notification, it is usually because the app *isn't* focused, and the user wants to focus it.
+53
View File
@@ -14,6 +14,16 @@ gestures {
delay-ms 100
max-speed 1500
}
dnd-edge-workspace-switch {
trigger-height 50
delay-ms 100
max-speed 1500
}
hot-corners {
// off
}
}
```
@@ -41,3 +51,46 @@ gestures {
}
}
```
### `dnd-edge-workspace-switch`
<sup>Since: 25.05</sup>
Scroll the workspaces up/down when moving the mouse cursor against a monitor edge during drag-and-drop (DnD) while in the overview.
Also works on a touchscreen.
The options are:
- `trigger-height`: size of the area near the monitor edge that will trigger the scrolling, in logical pixels.
- `delay-ms`: delay in milliseconds before the scrolling starts.
Avoids unwanted scrolling when dragging things across monitors.
- `max-speed`: maximum scrolling speed; 1500 corresponds to one screen height per second.
The scrolling speed increases linearly as you move your mouse cursor from `trigger-width` to the very edge of the monitor.
```kdl
gestures {
// Increase the trigger area and maximum speed.
dnd-edge-workspace-switch {
trigger-height 100
max-speed 3000
}
}
```
### `hot-corners`
<sup>Since: 25.05</sup>
Put your mouse at the very top-left corner of a monitor to toggle the overview.
Also works during drag-and-dropping something.
`off` disables the hot corners.
```kdl
// Disable the hot corners.
gestures {
hot-corners {
off
}
}
```
+22 -3
View File
@@ -23,6 +23,7 @@ input {
// repeat-delay 600
// repeat-rate 25
// track-layout "global"
numlock
}
touchpad {
@@ -166,6 +167,24 @@ input {
}
```
#### Num Lock
<sup>Since: 25.05</sup>
Set the `numlock` flag to turn on Num Lock automatically at startup.
You might want to disable (comment out) `numlock` if you're using a laptop with a keyboard that overlays Num Lock keys on top of regular keys.
Note that there's a [known issue](https://github.com/YaLTeR/niri/issues/1501) with this setting: Num Lock only turns on after you press some modifier key (Super, Alt, etc.).
```kdl
input {
keyboard {
numlock
}
}
```
### Pointing Devices
Most settings for the pointing devices are passed directly to libinput.
@@ -192,7 +211,7 @@ Settings specific to `touchpad`s:
- `tap`: tap-to-click.
- `dwt`: disable-when-typing.
- `dwtp`: disable-when-trackpointing.
- `drag`: can be `true` or `false`, controls if tap-and-drag is enabled.
- `drag`: <sup>Since: 25.05</sup> can be `true` or `false`, controls if tap-and-drag is enabled.
- `drag-lock`: <sup>Since: 25.02</sup> if set, lifting the finger off for a short time while dragging will not drop the dragged item. See the [libinput documentation](https://wayland.freedesktop.org/libinput/doc/latest/tapping.html#tap-and-drag).
- `tap-button-map`: can be `left-right-middle` or `left-middle-right`, controls which button corresponds to a two-finger tap and a three-finger tap.
- `click-method`: can be `button-areas` or `clickfinger`, changes the [click method](https://wayland.freedesktop.org/libinput/doc/latest/clickpad-softbuttons.html).
@@ -254,7 +273,7 @@ input {
By default, the cursor warps *separately* horizontally and vertically.
I.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then the mouse will move only horizontally, and not vertically.
<sup>Since: next release</sup> You can customize this with the `mode` property.
<sup>Since: 25.05</sup> You can customize this with the `mode` property.
- `mode="center-xy"`: warps by both X and Y coordinates together.
So if the mouse was anywhere outside the newly focused window, it will warp to the center of the window.
@@ -309,7 +328,7 @@ input {
#### `mod-key`, `mod-key-nested`
<sup>Since: next release</sup>
<sup>Since: 25.05</sup>
Customize the `Mod` key for [key bindings](./Configuration:-Key-Bindings.md).
Only valid modifiers are allowed, e.g. `Super`, `Alt`, `Mod3`, `Mod5`, `Ctrl`, `Shift`.
+150
View File
@@ -0,0 +1,150 @@
### Per-Section Documentation
You can find documentation for various sections of the config on these wiki pages:
* [`input {}`](./Configuration:-Input.md)
* [`output "eDP-1" {}`](./Configuration:-Outputs.md)
* [`binds {}`](./Configuration:-Key-Bindings.md)
* [`switch-events {}`](./Configuration:-Switch-Events.md)
* [`layout {}`](./Configuration:-Layout.md)
* [top-level options](./Configuration:-Miscellaneous.md)
* [`window-rule {}`](./Configuration:-Window-Rules.md)
* [`layer-rule {}`](./Configuration:-Layer-Rules.md)
* [`animations {}`](./Configuration:-Animations.md)
* [`gestures {}`](./Configuration:-Gestures.md)
* [`debug {}`](./Configuration:-Debug-Options.md)
### Loading
Niri will load configuration from `$XDG_CONFIG_HOME/niri/config.kdl` or `~/.config/niri/config.kdl`, falling back to `/etc/niri/config.kdl`.
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
Please use the default configuration file as the starting point for your custom configuration.
The configuration is live-reloaded.
Simply edit and save the config file, and your changes will be applied.
This includes key bindings, output settings like mode, window rules, and everything else.
You can run `niri validate` to parse the config and see any errors.
To use a different config file path, pass it in the `--config` or `-c` argument to `niri`.
You can also set `$NIRI_CONFIG` to the path of the config file.
`--config` always takes precedence.
If `--config` or `$NIRI_CONFIG` doesn't point to a real file, the config will not be loaded.
If `$NIRI_CONFIG` is set to an empty string, it is ignored and the default config location is used instead.
### Syntax
The config is written in [KDL].
#### Comments
Lines starting with `//` are comments; they are ignored.
Also, you can put `/-` in front of a section to comment out the entire section:
```kdl
/-output "eDP-1" {
// Everything inside here is ignored.
// The display won't be turned off
// as the whole section is commented out.
off
}
```
#### Flags
Toggle options in niri are commonly represented as flags.
Writing out the flag enables it, and omitting it or commenting it out disables it.
For example:
```kdl
// "Focus follows mouse" is enabled.
input {
focus-follows-mouse
// Other settings...
}
```
```kdl
// "Focus follows mouse" is disabled.
input {
// focus-follows-mouse
// Other settings...
}
```
#### Sections
Most sections cannot be repeated. For example:
```kdl
// This is valid: every section appears once.
input {
keyboard {
// ...
}
touchpad {
// ...
}
}
```
```kdl,must-fail
// This is NOT valid: input section appears twice.
input {
keyboard {
// ...
}
}
input {
touchpad {
// ...
}
}
```
Exceptions are, for example, sections that configure different devices by name:
<!-- NOTE: this may break in the future -->
```kdl
output "eDP-1" {
// ...
}
// This is valid: this section configures a different output.
output "HDMI-A-1" {
// ...
}
// This is NOT valid: "eDP-1" already appeared above.
// It will either throw a config parsing error, or otherwise not work.
output "eDP-1" {
// ...
}
```
### Defaults
Omitting most of the sections of the config file will leave you with the default values for that section.
A notable exception is [`binds {}`](./Configuration:-Key-Bindings.md): they do not get filled with defaults, so make sure you do not erase this section.
### Breaking Change Policy
As a rule, niri updates should not break existing config files.
(For example, the default config from niri v0.1.0 still parses fine on v25.02 as I'm writing this.)
Exceptions can be made for parsing bugs.
For example, niri used to accept multiple binds to the same key, but this was not intended and did not do anything (the first bind was always used).
A patch release changed niri from silently accepting this to causing a parsing failure.
This is not a blanket rule, I will consider the potential impact of every breaking change like this before deciding to carry on with it.
Keep in mind that the breaking change policy applies only to niri releases.
Commits between releases can and do occasionally break the config as new features are ironed out.
However, I do try to limit these, since several people are running git builds.
[KDL]: https://kdl.dev/
+11 -2
View File
@@ -31,7 +31,7 @@ Valid modifiers are:
This way, you can test niri in a window without causing too many conflicts with the host compositor's key bindings.
For this reason, most of the default keys use the `Mod` modifier.
<sup>Since: next release</sup> You can customize the `Mod` key [in the `input` section of the config](./Configuration:-Input.md#mod-key-mod-key-nested).
<sup>Since: 25.05</sup> You can customize the `Mod` key [in the `input` section of the config](./Configuration:-Input.md#mod-key-mod-key-nested).
> [!TIP]
> To find an XKB name for a particular key, you may use a program like [`wev`](https://git.sr.ht/~sircmpwn/wev).
@@ -52,6 +52,15 @@ For this reason, most of the default keys use the `Mod` modifier.
>
> Here, look at `sym: Left` and `sym: Right`: these are the key names.
> I was pressing the left and the right arrow in this example.
>
> Keep in mind that binding shifted keys requires spelling out Shift and the unshifted version of the key, according to your XKB layout.
> For example, on the US QWERTY layout, <kbd>&lt;</kbd> is on <kbd>Shift</kbd> + <kbd>,</kbd>, so to bind it, you spell out something like `Mod+Shift+Comma`.
>
> As another example, if you've configured the French [BÉPO](https://en.wikipedia.org/wiki/B%C3%89PO) XKB layout, your <kbd>&lt;</kbd> is on <kbd>AltGr</kbd> + <kbd>«</kbd>.
> <kbd>AltGr</kbd> is `ISO_Level3_Shift`, or equivalently `Mod5`, so to bind it, you spell out something like `Mod+Mod5+guillemotleft`.
>
> When resolving latin keys, niri will search for the *first* configured XKB layout that has the latin key.
> So for example with US QWERTY and RU layouts configured, US QWERTY will be used for latin binds.
<sup>Since: 0.1.8</sup> Binds will repeat by default (i.e. holding down a bind will make it trigger repeatedly).
You can disable that for specific binds with `repeat=false`:
@@ -328,7 +337,7 @@ binds {
In the interactive screenshot UI, pressing <kbd>Ctrl</kbd><kbd>C</kbd> will copy the screenshot to the clipboard without writing it to disk.
<sup>Since: next release</sup> You can hide the mouse pointer in screenshots with the `show-pointer=false` property:
<sup>Since: 25.05</sup> You can hide the mouse pointer in screenshots with the `show-pointer=false` property:
```kdl
binds {
+40 -1
View File
@@ -32,6 +32,8 @@ layer-rule {
}
geometry-corner-radius 12
place-within-backdrop true
baba-is-float true
}
```
@@ -129,7 +131,7 @@ That is, enabling shadows in the layout config section won't automatically enabl
// Add a shadow for fuzzel.
layer-rule {
match namespace="^launcher$"
shadow {
on
}
@@ -149,6 +151,43 @@ This setting will only affect the shadow—it will round its corners to match th
```kdl
layer-rule {
match namespace="^launcher$"
geometry-corner-radius 12
}
```
#### `place-within-backdrop`
<sup>Since: 25.05</sup>
Set to `true` to place the surface into the backdrop visible in the [Overview](./Overview.md) and between workspaces.
This will only work for *background* layer surfaces that ignore exclusive zones (typical for wallpaper tools).
Layers within the backdrop will ignore all input.
```kdl
// Put swaybg inside the overview backdrop.
layer-rule {
match namespace="^wallpaper$"
place-within-backdrop true
}
```
#### `baba-is-float`
<sup>Since: 25.05</sup>
Make your layer surfaces FLOAT up and down.
This is a natural extension of the [April Fools' 2025 feature](./Configuration:-Window-Rules.md#baba-is-float).
```kdl
// Make fuzzel FLOAT.
layer-rule {
match namespace="^launcher$"
baba-is-float true
}
```
+27 -2
View File
@@ -11,6 +11,7 @@ layout {
always-center-single-column
empty-workspace-above-first
default-column-display "tabbed"
background-color "#003300"
preset-column-widths {
proportion 0.33333
@@ -31,8 +32,10 @@ layout {
width 4
active-color "#7fc8ff"
inactive-color "#505050"
urgent-color "#9b0000"
// active-gradient from="#80c8ff" to="#bbddff" angle=45
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
// urgent-gradient from="#800" to="#a33" angle=45
}
border {
@@ -40,8 +43,10 @@ layout {
width 4
active-color "#ffc87f"
inactive-color "#505050"
urgent-color "#9b0000"
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" in="srgb-linear"
// urgent-gradient from="#800" to="#a33" angle=45
}
shadow {
@@ -66,8 +71,10 @@ layout {
corner-radius 8
active-color "red"
inactive-color "gray"
urgent-color "blue"
// active-gradient from="#80c8ff" to="#bbddff" angle=45
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
// urgent-gradient from="#800" to="#a33" angle=45
}
insert-hint {
@@ -269,6 +276,9 @@ layout {
active-color "#ffc87f"
inactive-color "#505050"
// Color of the border around windows that request your attention.
urgent-color "#9b0000"
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" in="srgb-linear"
}
@@ -372,7 +382,7 @@ Set `on` to enable the shadow.
Setting `softness 0` will give you hard shadows.
`spread` is the distance to expand the window rectangle in logical pixels, same as CSS box-shadow spread.
<sup>Since: next release</sup> Spread can be negative.
<sup>Since: 25.05</sup> Spread can be negative.
`offset` moves the shadow relative to the window in logical pixels, same as CSS box-shadow offset.
For example, `offset x=2 y=2` will move the shadow 2 logical pixels downwards and to the right.
@@ -440,7 +450,7 @@ It can be `left`, `right`, `top`, or `bottom`.
`corner-radius` sets the rounded corner radius for tabs in the indicator in logical pixels.
When `gaps-between-tabs` is zero, only the first and the last tabs have rounded corners, otherwise all tabs do.
`active-color`, `inactive-color`, `active-gradient`, `inactive-gradient` let you override the colors for the tabs.
`active-color`, `inactive-color`, `urgent-color`, `active-gradient`, `inactive-gradient`, `urgent-gradient` let you override the colors for the tabs.
They have the same semantics as the border and focus ring colors and gradients.
Tab colors are picked in this order:
@@ -526,3 +536,18 @@ layout {
}
}
```
### `background-color`
<sup>Since: 25.05</sup>
Set the default background color that niri draws for workspaces.
This is visible when you're not using any background tools like swaybg.
```kdl
layout {
background-color "#003300"
}
```
You can also set the color per-output [in the output config](./Configuration:-Outputs.md#background-color).
+65 -2
View File
@@ -1,5 +1,3 @@
### Overview
This page documents all top-level options that don't otherwise have dedicated pages.
Here are all of these options at a glance:
@@ -25,6 +23,19 @@ cursor {
hide-after-inactive-ms 1000
}
overview {
zoom 0.5
backdrop-color "#262626"
workspace-shadow {
// off
softness 40
spread 10
offset x=0 y=10
color "#00000050"
}
}
clipboard {
disable-primary
}
@@ -143,6 +154,58 @@ cursor {
}
```
### `overview`
<sup>Since: 25.05</sup>
Settings for the [Overview](./Overview.md).
#### `zoom`
Control how much the workspaces zoom out in the overview.
`zoom` ranges from 0 to 0.75 where lower values make everything smaller.
```kdl
// Make workspaces four times smaller than normal in the overview.
overview {
zoom 0.25
}
```
#### `backdrop-color`
Set the backdrop color behind workspaces in the overview.
The backdrop is also visible between workspaces when switching.
The alpha channel for this color will be ignored.
```kdl
// Make the backdrop light.
overview {
backdrop-color "#777777"
}
```
You can also set the color per-output [in the output config](./Configuration:-Outputs.md#backdrop-color).
#### `workspace-shadow`
Control the shadow behind workspaces visible in the overview.
Settings here mirror the normal [`shadow` config in the layout section](./Configuration:-Layout.md#shadow), so check the documentation there.
Workspace shadows are configured for a workspace size normalized to 1080 pixels tall, then zoomed out together with the workspace.
Practically, this means that you'll want bigger spread, offset, and softness compared to window shadows.
```kdl
// Disable workspace shadows in the overview.
overview {
workspace-shadow {
off
}
}
```
### `clipboard`
<sup>Since: 25.02</sup>
+19 -3
View File
@@ -15,6 +15,7 @@ output "eDP-1" {
variable-refresh-rate // on-demand=true
focus-at-startup
background-color "#003300"
backdrop-color "#001100"
}
output "HDMI-A-1" {
@@ -167,7 +168,7 @@ output "HDMI-A-1" {
### `focus-at-startup`
<sup>Since: next release</sup>
<sup>Since: 25.05</sup>
Focus this output by default when niri starts.
@@ -191,13 +192,28 @@ output "DP-2" {
<sup>Since: 0.1.8</sup>
Set the background color that niri draws for this output.
Set the background color that niri draws for workspaces on this output.
This is visible when you're not using any background tools like swaybg.
The alpha channel for this color will be ignored.
<sup>Until: 25.05</sup> The alpha channel for this color will be ignored.
```kdl
output "HDMI-A-1" {
background-color "#003300"
}
```
### `backdrop-color`
<sup>Since: 25.05</sup>
Set the backdrop color that niri draws for this output.
This is visible between workspaces or in the overview.
The alpha channel for this color will be ignored.
```kdl
output "HDMI-A-1" {
backdrop-color "#001100"
}
```
+1 -150
View File
@@ -1,150 +1 @@
### Per-Section Documentation
You can find documentation for various sections of the config on these wiki pages:
* [`input {}`](./Configuration:-Input.md)
* [`output "eDP-1" {}`](./Configuration:-Outputs.md)
* [`binds {}`](./Configuration:-Key-Bindings.md)
* [`switch-events {}`](./Configuration:-Switch-Events.md)
* [`layout {}`](./Configuration:-Layout.md)
* [top-level options](./Configuration:-Miscellaneous.md)
* [`window-rule {}`](./Configuration:-Window-Rules.md)
* [`layer-rule {}`](./Configuration:-Layer-Rules.md)
* [`animations {}`](./Configuration:-Animations.md)
* [`gestures {}`](./Configuration:-Gestures.md)
* [`debug {}`](./Configuration:-Debug-Options.md)
### Loading
Niri will load configuration from `$XDG_CONFIG_HOME/niri/config.kdl` or `~/.config/niri/config.kdl`, falling back to `/etc/niri/config.kdl`.
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
Please use the default configuration file as the starting point for your custom configuration.
The configuration is live-reloaded.
Simply edit and save the config file, and your changes will be applied.
This includes key bindings, output settings like mode, window rules, and everything else.
You can run `niri validate` to parse the config and see any errors.
To use a different config file path, pass it in the `--config` or `-c` argument to `niri`.
You can also set `$NIRI_CONFIG` to the path of the config file.
`--config` always takes precedence.
If `--config` or `$NIRI_CONFIG` doesn't point to a real file, the config will not be loaded.
If `$NIRI_CONFIG` is set to an empty string, it is ignored and the default config location is used instead.
### Syntax
The config is written in [KDL].
#### Comments
Lines starting with `//` are comments; they are ignored.
Also, you can put `/-` in front of a section to comment out the entire section:
```kdl
/-output "eDP-1" {
// Everything inside here is ignored.
// The display won't be turned off
// as the whole section is commented out.
off
}
```
#### Flags
Toggle options in niri are commonly represented as flags.
Writing out the flag enables it, and omitting it or commenting it out disables it.
For example:
```kdl
// "Focus follows mouse" is enabled.
input {
focus-follows-mouse
// Other settings...
}
```
```kdl
// "Focus follows mouse" is disabled.
input {
// focus-follows-mouse
// Other settings...
}
```
#### Sections
Most sections cannot be repeated. For example:
```kdl
// This is valid: every section appears once.
input {
keyboard {
// ...
}
touchpad {
// ...
}
}
```
```kdl,must-fail
// This is NOT valid: input section appears twice.
input {
keyboard {
// ...
}
}
input {
touchpad {
// ...
}
}
```
Exceptions are, for example, sections that configure different devices by name:
<!-- NOTE: this may break in the future -->
```kdl
output "eDP-1" {
// ...
}
// This is valid: this section configures a different output.
output "HDMI-A-1" {
// ...
}
// This is NOT valid: "eDP-1" already appeared above.
// It will either throw a config parsing error, or otherwise not work.
output "eDP-1" {
// ...
}
```
### Defaults
Omitting most of the sections of the config file will leave you with the default values for that section.
A notable exception is [`binds {}`](./Configuration:-Key-Bindings.md): they do not get filled with defaults, so make sure you do not erase this section.
### Breaking Change Policy
As a rule, niri updates should not break existing config files.
(For example, the default config from niri v0.1.0 still parses fine on v25.02 as I'm writing this.)
Exceptions can be made for parsing bugs.
For example, niri used to accept multiple binds to the same key, but this was not intended and did not do anything (the first bind was always used).
A patch release changed niri from silently accepting this to causing a parsing failure.
This is not a blanket rule, I will consider the potential impact of every breaking change like this before deciding to carry on with it.
Keep in mind that the breaking change policy applies only to niri releases.
Commits between releases can and do occasionally break the config as new features are ironed out.
However, I do try to limit these, since several people are running git builds.
[KDL]: https://kdl.dev/
This wiki page has moved to: [Introduction](./Configuration:-Introduction.md).
+19 -1
View File
@@ -35,6 +35,7 @@ window-rule {
match is-active-in-column=true
match is-floating=true
match is-window-cast-target=true
match is-urgent=true
match at-startup=true
// Properties that apply once upon window opening.
@@ -63,8 +64,10 @@ window-rule {
width 4
active-color "#7fc8ff"
inactive-color "#505050"
urgent-color "#9b0000"
// active-gradient from="#80c8ff" to="#bbddff" angle=45
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
// urgent-gradient from="#800" to="#a33" angle=45
}
border {
@@ -85,8 +88,10 @@ window-rule {
tab-indicator {
active-color "red"
inactive-color "gray"
urgent-color "blue"
// active-gradient from="#80c8ff" to="#bbddff" angle=45
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
// urgent-gradient from="#800" to="#a33" angle=45
}
geometry-corner-radius 12
@@ -282,6 +287,19 @@ Example:
![](https://github.com/user-attachments/assets/375b381e-3a87-4e94-8676-44404971d893)
#### `is-urgent`
<sup>Since: 25.05</sup>
Can be `true` or `false`.
Matches windows that request the user's attention.
```kdl
window-rule {
match is-urgent=true
}
```
#### `at-startup`
<sup>Since: 0.1.6</sup>
@@ -829,7 +847,7 @@ window-rule {
#### `tiled-state`
<sup>Since: next release</sup>
<sup>Since: 25.05</sup>
Informs the window that it is tiled.
Usually, windows will react by becoming rectangular and hiding their client-side shadows.
+1 -1
View File
@@ -6,7 +6,7 @@ Then niri will ask windows to omit client-side decorations, and also inform them
Note that currently this will prevent edge window resize handles from showing up.
You can still resize windows by holding <kbd>Mod</kbd> and the right mouse button.
### Why is the border/focus ring showing up through semitransparent windows?
### Why are transparent windows tinted? / Why is the border/focus ring showing up through semitransparent windows?
Uncomment the [`prefer-no-csd` setting](./Configuration:-Miscellaneous.md#prefer-no-csd) at the top level of the config, and then restart your apps.
Niri will draw focus rings and borders *around* windows that agree to omit their client-side decorations.
+23
View File
@@ -68,3 +68,26 @@ Move the view horizontally with three-finger horizontal swipes.
Scroll the tiling view when moving the mouse cursor against a monitor edge during drag-and-drop (DnD).
Also works on a touchscreen.
#### Drag-and-Drop Edge Workspace Switch
<sup>Since: 25.05</sup>
Scroll the workspaces up/down when moving the mouse cursor against a monitor edge during drag-and-drop (DnD) while in the overview.
Also works on a touchscreen.
#### Drag-and-Drop Hold to Activate
<sup>Since: 25.05</sup>
While drag-and-dropping, hold your mouse over a window to activate it.
This will bring a floating window to the top for example.
In the overview, you can also hold the mouse over a workspace to switch to it.
#### Hot Corner to Toggle the Overview
<sup>Since: 25.05</sup>
Put your mouse at the very top-left corner of a monitor to toggle the overview.
Also works during drag-and-dropping something.
+1 -1
View File
@@ -17,7 +17,7 @@ Then it will open as a window, where you can give it a try.
Note that this windowed mode is mainly meant for development, so it is a bit buggy (in particular, there are issues with hotkeys).
Next, see the [list of important software](./Important-Software.md) required for normal desktop use, like a notification daemon and portals.
Also, check the [configuration overview](./Configuration:-Overview.md) page to get started configuring niri.
Also, check the [configuration introduction](./Configuration:-Introduction.md) page to get started configuring niri.
There you can find links to other pages containing thorough documentation and examples for all options.
Finally, the [Xwayland](./Xwayland.md) page explains how to run X11 applications on niri.
+1
View File
@@ -2,3 +2,4 @@ Things to keep in mind with layer-shell components (bars, launchers, etc.):
1. When a full-screen window is active and covers the entire screen, it will render above the top layer, and it will be prioritized for keyboard focus. If your launcher uses the top layer, and you try to run it while looking at a full-screen window, it won't show up. Only the overlay layer will show up on top of full-screen windows.
1. Components on the bottom and background layers will receive *on-demand* keyboard focus as expected. However, they will only receive *exclusive* keyboard focus when there are no windows on the workspace.
1. When opening the [Overview](./Overview.md), components on the bottom and background layers will zoom out and remain on the workspaces, while the top and overlay layers remain on top of the Overview. So, if you want the bar to remain on top, put it on the *top* layer.
+104
View File
@@ -0,0 +1,104 @@
### Overview
<sup>Since: 25.05</sup>
The Overview is a zoomed-out view of your workspaces and windows.
It lets you see what's going on at a glance, navigate, and drag windows around.
https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995
Open it with the `toggle-overview` bind, via the top-left hot corner, or using a touchpad four-finger swipe up.
While in the overview, all keyboard shortcuts keep working, while pointing devices get easier:
- Mouse: left click and drag windows to move them, right click and drag to scroll workspaces left/right, scroll to switch workspaces (no holding Mod required).
- Touchpad: two-finger scrolling that matches the normal three-finger gestures.
- Touchscreen: one-finger scrolling, or one-finger long press to move a window.
> [!TIP]
> The overview needs to draw a background under every workspace.
> So, layer-shell surfaces work this way: the *background* and *bottom* layers zoom out together with the workspaces, while the *top* and *overlay* layers remain on top of the overview.
>
> Put your bar on the *top* layer.
Drag-and-drop will scroll the workspaces up/down in the overview, and will activate a workspace when holding it for a moment.
Combined with the hot corner, this lets you do a mouse-only DnD across workspaces.
https://github.com/user-attachments/assets/5f09c5b7-ff40-462b-8b9c-f1b8073a2cbb
You can also drag-and-drop a window to a new workspace above, below, or between existing workspaces.
https://github.com/user-attachments/assets/b76d5349-aa20-4889-ab90-0a51554c789d
### Configuration
See the full documentation for the `overview {}` section [here](./Configuration:-Miscellaneous.md#overview).
You can set the zoom-out level like this:
```kdl
// Make workspaces four times smaller than normal in the overview.
overview {
zoom 0.25
}
```
To change the color behind the workspaces, use the `backdrop-color` setting:
```kdl
// Make the backdrop light.
overview {
backdrop-color "#777777"
}
```
You can also disable the hot corner:
```kdl
// Disable the hot corners.
gestures {
hot-corners {
off
}
}
```
### Backdrop customization
Apart from setting a custom backdrop color like described above, you can also put a layer-shell wallpaper into the backdrop with a [layer rule](./Configuration:-Layer-Rules.md#place-within-backdrop), for example:
```kdl
// Put swaybg inside the overview backdrop.
layer-rule {
match namespace="^wallpaper$"
place-within-backdrop true
}
```
This will only work for *background* layer surfaces that ignore exclusive zones (typical for wallpaper tools).
You can run two different wallpaper tools (like swaybg and swww), one for the backdrop and one for the normal workspace background.
This way you could set the backdrop one to a blurred version of the wallpaper for a nice effect.
You can also combine this with a transparent background color if you don't like the wallpaper moving together with workspaces:
```kdl
// Make the wallpaper stationary, rather than moving with workspaces.
layer-rule {
// This is for swaybg; change for other wallpaper tools.
// Find the right namespace by running niri msg layers.
match namespace="^wallpaper$"
place-within-backdrop true
}
// Set transparent workspace background color.
layout {
background-color "transparent"
}
// Optionally, disable the workspace shadows in the overview.
overview {
workspace-shadow {
off
}
}
```
+2 -2
View File
@@ -46,7 +46,7 @@ Check [the corresponding wiki section](./Configuration:-Window-Rules.md#block-ou
### Dynamic screencast target
<sup>Since: next release</sup>
<sup>Since: 25.05</sup>
Niri provides a special screencast stream that you can change dynamically.
It shows up as "niri Dynamic Cast Target" in the screencast window dialog.
@@ -113,7 +113,7 @@ Example:
### Windowed (fake/detached) fullscreen
<sup>Since: next release</sup>
<sup>Since: 25.05</sup>
When screencasting browser-based presentations like Google Slides, you usually want to hide the browser UI, which requires making the browser fullscreen.
This is not always convenient, for example if you have an ultrawide monitor, or just want to leave the browser as a smaller window, without taking up an entire monitor.
+2 -1
View File
@@ -5,6 +5,7 @@
* [Workspaces](./Workspaces.md)
* [Floating Windows](./Floating-Windows.md)
* [Tabs](./Tabs.md)
* [Overview](./Overview.md)
* [Screencasting](./Screencasting.md)
* [LayerShell Components](./Layer%E2%80%90Shell-Components.md)
* [IPC, `niri msg`](./IPC.md)
@@ -15,7 +16,7 @@
* [FAQ](./FAQ.md)
## Configuration
* [Overview](./Configuration:-Overview.md)
* [Introduction](./Configuration:-Introduction.md)
* [Input](./Configuration:-Input.md)
* [Outputs](./Configuration:-Outputs.md)
* [Key Bindings](./Configuration:-Key-Bindings.md)