Compare commits
33 commits
post-1.8-m
...
local/yoda
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f3a82beb4 | |||
| 11f60acc94 | |||
|
|
41497b42d9 | ||
|
|
3d55cba06c | ||
|
|
895ce91543 | ||
|
|
115cdac039 | ||
|
|
7596cf7f2f | ||
|
|
295eaa8f63 | ||
|
|
569cfa6442 | ||
|
|
4e72d2ce98 | ||
|
|
6e0aea0eb2 | ||
|
|
b975370968 | ||
|
|
af025856c7 | ||
|
|
d8e4683be8 | ||
|
|
ed7814e51e | ||
|
|
467d1fbc7f | ||
|
|
e7b65b1fc8 | ||
|
|
4d854ecb94 | ||
|
|
9f746ea3ff | ||
| 30d8868cca | |||
| 51c6cfedf6 | |||
|
|
e5010c23c9 | ||
|
|
f60d2c8436 | ||
|
|
58837e27b8 | ||
|
|
6645edff64 | ||
|
|
0fbd4574ef | ||
|
|
d36480c1b5 | ||
|
|
50c170c355 | ||
|
|
26daf75d93 | ||
|
|
cd881a4df2 | ||
|
|
651f701f84 | ||
|
|
a7349b72e3 | ||
|
|
f9e02f4321 |
32 changed files with 1139 additions and 298 deletions
16
.github/PULL_REQUEST_TEMPLATE.md
vendored
16
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,8 +1,10 @@
|
||||||
- [ ] I have disclosed use of any AI generated code in my commit messages.
|
Replace this text with a description of your changes, and why these changes are important. Read and acknowledge the list below. PRs without a completed checkbox will be closed.
|
||||||
- If you are using an LLM, and do not fully understand the changes it is making to the code base, do not create a PR.
|
|
||||||
- In our experience, AI generated code often results in overly complex code that lacks enough context for a proper fix or feature inclusion. This results in considerably longer code reviews. Due to this, AI authored or partially authored PRs may be closed without comment.
|
|
||||||
- [ ] I understand these changes in full and will be able to respond to review comments.
|
|
||||||
- [ ] My change is accurately described in the commit message.
|
|
||||||
- [ ] My contribution is tested and working as described.
|
|
||||||
- [ ] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions.
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- I have not included any LLM (also known as AI) generated content in this PR, including code, comments, and descriptions.
|
||||||
|
- I understand these changes in full and will be able to respond to review comments.
|
||||||
|
- My change is accurately described in the commit message.
|
||||||
|
- My contribution is tested and working as described.
|
||||||
|
- I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions.
|
||||||
|
- [ ] I have read and followed all of the above requirements. For more information, see our [contributor guide](https://github.com/pop-os/pop/blob/master/CONTRIBUTING.md).
|
||||||
|
|
|
||||||
55
.github/workflows/ci.yml
vendored
55
.github/workflows/ci.yml
vendored
|
|
@ -13,18 +13,12 @@ jobs:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
- name: Rust toolchain
|
- name: Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
- name: Cargo cache
|
rustflags: ""
|
||||||
uses: actions/cache@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
|
||||||
- name: Format
|
- name: Format
|
||||||
run: cargo +stable fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
clippy-check:
|
clippy-check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
@ -32,20 +26,14 @@ jobs:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
- name: Rust toolchain
|
- name: Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
components: clippy
|
components: clippy
|
||||||
- name: Cargo cache
|
rustflags: ""
|
||||||
uses: actions/cache@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
|
||||||
- name: System dependencies
|
- name: System dependencies
|
||||||
run: sudo apt-get update; sudo apt-get install libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libdisplay-info-dev
|
run: sudo apt-get update; sudo apt-get install libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libdisplay-info-dev
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo +stable clippy --all-features -- -D warnings
|
run: cargo clippy --all-features -- -D warnings
|
||||||
|
|
||||||
check-features:
|
check-features:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
@ -53,20 +41,15 @@ jobs:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
- name: Rust toolchain
|
- name: Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
- name: Cargo cache
|
|
||||||
uses: actions/cache@v5
|
|
||||||
with:
|
with:
|
||||||
path: |
|
rustflags: ""
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
|
||||||
- name: System dependencies
|
- name: System dependencies
|
||||||
run: sudo apt-get update; sudo apt-get install libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libdisplay-info-dev
|
run: sudo apt-get update; sudo apt-get install libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libdisplay-info-dev
|
||||||
- run: cargo +stable check --no-default-features
|
- run: cargo check --no-default-features
|
||||||
- run: cargo +stable check --features debug
|
- run: cargo check --features debug
|
||||||
- run: cargo +stable check --features profile-with-tracy
|
- run: cargo check --features profile-with-tracy
|
||||||
- run: cargo +stable check --features profile-with-tracy-gpu
|
- run: cargo check --features profile-with-tracy-gpu
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
@ -74,21 +57,11 @@ jobs:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
- name: Rust toolchain
|
- name: Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
- name: Cargo cache
|
|
||||||
uses: actions/cache@v5
|
|
||||||
with:
|
with:
|
||||||
path: |
|
rustflags: ""
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
|
||||||
- name: System dependencies
|
- name: System dependencies
|
||||||
run: sudo apt-get update; sudo apt-get install libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libdisplay-info-dev libpixman-1-dev meson ninja-build
|
run: sudo apt-get update; sudo apt-get install libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libdisplay-info-dev libpixman-1-dev meson ninja-build
|
||||||
- name: Build cache
|
|
||||||
uses: actions/cache@v5
|
|
||||||
with:
|
|
||||||
path: target
|
|
||||||
key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.toml') }}
|
|
||||||
- name: Build and install Libseat
|
- name: Build and install Libseat
|
||||||
run: |
|
run: |
|
||||||
wget https://git.sr.ht/~kennylevinsen/seatd/archive/0.9.1.tar.gz -O libseat-source.tar.gz
|
wget https://git.sr.ht/~kennylevinsen/seatd/archive/0.9.1.tar.gz -O libseat-source.tar.gz
|
||||||
|
|
|
||||||
14
.github/workflows/require-checklist.yml
vendored
Normal file
14
.github/workflows/require-checklist.yml
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# From https://github.com/mheap/require-checklist-action, license MIT
|
||||||
|
name: Require Checklist
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, edited, synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: mheap/require-checklist-action@v2
|
||||||
|
with:
|
||||||
|
requireChecklist: true
|
||||||
39
Cargo.lock
generated
39
Cargo.lock
generated
|
|
@ -21,7 +21,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "accesskit"
|
name = "accesskit"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
source = "git+https://github.com/wash2/accesskit?tag=cosmic-0.14#f0599eed5f18111228266fe3f28991cc48b5964f"
|
source = "git+https://github.com/wash2/accesskit?rev=6c20249#6c20249d3b9fd45e7002efad9f6b6171a9257912"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
@ -29,7 +29,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "accesskit_consumer"
|
name = "accesskit_consumer"
|
||||||
version = "0.32.0"
|
version = "0.32.0"
|
||||||
source = "git+https://github.com/wash2/accesskit?tag=cosmic-0.14#f0599eed5f18111228266fe3f28991cc48b5964f"
|
source = "git+https://github.com/wash2/accesskit?rev=6c20249#6c20249d3b9fd45e7002efad9f6b6171a9257912"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
|
|
@ -38,7 +38,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "accesskit_macos"
|
name = "accesskit_macos"
|
||||||
version = "0.23.0"
|
version = "0.23.0"
|
||||||
source = "git+https://github.com/wash2/accesskit?tag=cosmic-0.14#f0599eed5f18111228266fe3f28991cc48b5964f"
|
source = "git+https://github.com/wash2/accesskit?rev=6c20249#6c20249d3b9fd45e7002efad9f6b6171a9257912"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_consumer",
|
"accesskit_consumer",
|
||||||
|
|
@ -51,7 +51,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "accesskit_windows"
|
name = "accesskit_windows"
|
||||||
version = "0.30.0"
|
version = "0.30.0"
|
||||||
source = "git+https://github.com/wash2/accesskit?tag=cosmic-0.14#f0599eed5f18111228266fe3f28991cc48b5964f"
|
source = "git+https://github.com/wash2/accesskit?rev=6c20249#6c20249d3b9fd45e7002efad9f6b6171a9257912"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_consumer",
|
"accesskit_consumer",
|
||||||
|
|
@ -64,7 +64,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "accesskit_winit"
|
name = "accesskit_winit"
|
||||||
version = "0.30.0"
|
version = "0.30.0"
|
||||||
source = "git+https://github.com/wash2/accesskit?tag=cosmic-0.14#f0599eed5f18111228266fe3f28991cc48b5964f"
|
source = "git+https://github.com/wash2/accesskit?rev=6c20249#6c20249d3b9fd45e7002efad9f6b6171a9257912"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_macos",
|
"accesskit_macos",
|
||||||
|
|
@ -1012,7 +1012,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-randr-shell"
|
name = "cosmic-randr-shell"
|
||||||
version = "1.3.0"
|
version = "1.10.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"kdl",
|
"kdl",
|
||||||
"slotmap",
|
"slotmap",
|
||||||
|
|
@ -1021,7 +1021,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-settings-config"
|
name = "cosmic-settings-config"
|
||||||
version = "0.1.0"
|
version = "1.10.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cosmic-config",
|
"cosmic-config",
|
||||||
"ron 0.11.0",
|
"ron 0.11.0",
|
||||||
|
|
@ -1040,7 +1040,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-settings-daemon-config"
|
name = "cosmic-settings-daemon-config"
|
||||||
version = "0.1.0"
|
version = "1.10.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cosmic-config",
|
"cosmic-config",
|
||||||
"cosmic-theme",
|
"cosmic-theme",
|
||||||
|
|
@ -1564,7 +1564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1963,8 +1963,8 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"windows-link 0.2.1",
|
"windows-link 0.1.3",
|
||||||
"windows-result 0.4.1",
|
"windows-result 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2329,7 +2329,7 @@ dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-core 0.62.2",
|
"windows-core 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2833,7 +2833,7 @@ dependencies = [
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"portable-atomic-util",
|
"portable-atomic-util",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3072,6 +3072,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"palette",
|
"palette",
|
||||||
"phf 0.13.1",
|
"phf 0.13.1",
|
||||||
|
"roxmltree 0.20.0",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"serde",
|
"serde",
|
||||||
"slotmap",
|
"slotmap",
|
||||||
|
|
@ -3135,9 +3136,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.23"
|
version = "0.1.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed"
|
checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -4810,7 +4811,7 @@ dependencies = [
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.4.15",
|
"linux-raw-sys 0.4.15",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4823,7 +4824,7 @@ dependencies = [
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.12.1",
|
"linux-raw-sys 0.12.1",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5432,7 +5433,7 @@ dependencies = [
|
||||||
"getrandom 0.4.1",
|
"getrandom 0.4.1",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -6546,7 +6547,7 @@ version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ tracing = { version = "0.1.44", features = [
|
||||||
"max_level_debug",
|
"max_level_debug",
|
||||||
"release_max_level_info",
|
"release_max_level_info",
|
||||||
] }
|
] }
|
||||||
tracing-journald = "0.3.2"
|
tracing-journald = { version = "0.3.2", optional = true }
|
||||||
tracing-subscriber = { version = "0.3.22", features = [
|
tracing-subscriber = { version = "0.3.22", features = [
|
||||||
"env-filter",
|
"env-filter",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
|
|
@ -125,7 +125,7 @@ optional = true
|
||||||
debug = ["egui", "egui_plot", "smithay-egui", "anyhow/backtrace"]
|
debug = ["egui", "egui_plot", "smithay-egui", "anyhow/backtrace"]
|
||||||
default = ["systemd"]
|
default = ["systemd"]
|
||||||
logind = ["logind-zbus"]
|
logind = ["logind-zbus"]
|
||||||
systemd = ["libsystemd", "logind"]
|
systemd = ["libsystemd", "logind", "tracing-journald"]
|
||||||
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
|
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
|
||||||
profile-with-tracy-gpu = ["profile-with-tracy", "smithay/tracy_gpu_profiling"]
|
profile-with-tracy-gpu = ["profile-with-tracy", "smithay/tracy_gpu_profiling"]
|
||||||
|
|
||||||
|
|
|
||||||
67
Makefile
67
Makefile
|
|
@ -1,67 +0,0 @@
|
||||||
export prefix ?= /usr
|
|
||||||
sysconfdir ?= /etc
|
|
||||||
bindir = $(prefix)/bin
|
|
||||||
libdir = $(prefix)/lib
|
|
||||||
sharedir = $(prefix)/share
|
|
||||||
|
|
||||||
BINARY = cosmic-comp
|
|
||||||
CARGO_TARGET_DIR ?= target
|
|
||||||
TARGET = debug
|
|
||||||
DEBUG ?= 0
|
|
||||||
|
|
||||||
.PHONY = all clean install uninstall vendor
|
|
||||||
|
|
||||||
ifeq ($(DEBUG),0)
|
|
||||||
TARGET = release
|
|
||||||
ARGS += --release
|
|
||||||
endif
|
|
||||||
|
|
||||||
VENDOR ?= 0
|
|
||||||
ifneq ($(VENDOR),0)
|
|
||||||
ARGS += --offline --locked
|
|
||||||
endif
|
|
||||||
|
|
||||||
TARGET_BIN="$(DESTDIR)$(bindir)/$(BINARY)"
|
|
||||||
|
|
||||||
KEYBINDINGS_CONF="$(DESTDIR)$(sharedir)/cosmic/com.system76.CosmicSettings.Shortcuts/v1/defaults"
|
|
||||||
TILING_EXCEPTIONS_CONF="$(DESTDIR)$(sharedir)/cosmic/com.system76.CosmicSettings.WindowRules/v1/tiling_exception_defaults"
|
|
||||||
|
|
||||||
all: extract-vendor
|
|
||||||
cargo build $(ARGS)
|
|
||||||
|
|
||||||
clean:
|
|
||||||
cargo clean
|
|
||||||
|
|
||||||
distclean:
|
|
||||||
rm -rf .cargo vendor vendor.tar target
|
|
||||||
|
|
||||||
vendor:
|
|
||||||
mkdir -p .cargo
|
|
||||||
cargo vendor | head -n -1 > .cargo/config
|
|
||||||
echo 'directory = "vendor"' >> .cargo/config
|
|
||||||
[ -n "$(SOURCE_GIT_HASH)" ] && printf '\n[env]\nGIT_HASH = "%s"\n' "$(SOURCE_GIT_HASH)" >> .cargo/config || true
|
|
||||||
tar pcf vendor.tar vendor
|
|
||||||
rm -rf vendor
|
|
||||||
|
|
||||||
extract-vendor:
|
|
||||||
ifeq ($(VENDOR),1)
|
|
||||||
rm -rf vendor; tar pxf vendor.tar
|
|
||||||
endif
|
|
||||||
|
|
||||||
install:
|
|
||||||
install -Dm0755 "$(CARGO_TARGET_DIR)/$(TARGET)/$(BINARY)" "$(TARGET_BIN)"
|
|
||||||
install -Dm0644 "data/keybindings.ron" "$(KEYBINDINGS_CONF)"
|
|
||||||
install -Dm0644 "data/tiling-exceptions.ron" "$(TILING_EXCEPTIONS_CONF)"
|
|
||||||
|
|
||||||
install-bare-session: install
|
|
||||||
install -Dm0644 "data/cosmic.desktop" "$(DESTDIR)$(sharedir)/wayland-sessions/cosmic.desktop"
|
|
||||||
install -Dm0644 "data/cosmic-session.target" "$(DESTDIR)$(libdir)/systemd/user/cosmic-session.target"
|
|
||||||
install -Dm0644 "data/cosmic-session-pre.target" "$(DESTDIR)$(libdir)/systemd/user/cosmic-session-pre.target"
|
|
||||||
install -Dm0644 "data/cosmic-comp.service" "$(DESTDIR)$(libdir)/systemd/user/cosmic-comp.service"
|
|
||||||
install -Dm0755 "data/cosmic-service" "$(DESTDIR)/$(bindir)/cosmic-service"
|
|
||||||
|
|
||||||
uninstall:
|
|
||||||
rm "$(TARGET_BIN)" "$(KEYBINDINGS_CONF)"
|
|
||||||
|
|
||||||
uninstall-bare-session:
|
|
||||||
rm "$(DESTDIR)$(sharedir)/wayland-sessions/cosmic.desktop"
|
|
||||||
31
README.md
Normal file
31
README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# cosmic-comp
|
||||||
|
Compositor for the COSMIC desktop environment
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
This project uses [just](https://github.com/casey/just) as a command runner.
|
||||||
|
|
||||||
|
### Available Commands
|
||||||
|
|
||||||
|
#### Building
|
||||||
|
- `just build-debug` - Compile with debug profile
|
||||||
|
- `just build-release` - Compile with release profile (default)
|
||||||
|
- `just build-vendored` - Compile release profile with vendored dependencies
|
||||||
|
- Requires vendoring first, which can be done with `just vendor`
|
||||||
|
|
||||||
|
#### Code Quality
|
||||||
|
- `just check` - Run clippy linter with pedantic warnings
|
||||||
|
|
||||||
|
#### Installation
|
||||||
|
- `just install` - Install binary and configuration files
|
||||||
|
- `just install-bare-session` - Install with bare session files (wayland session, systemd units)
|
||||||
|
- `just uninstall` - Remove installed files
|
||||||
|
- `just uninstall-bare-session` - Remove installed files including bare session files
|
||||||
|
|
||||||
|
#### Cleanup
|
||||||
|
- `just clean` - Run `cargo clean`
|
||||||
|
- `just clean-dist` - Run `cargo clean` and remove vendored dependencies
|
||||||
|
|
||||||
|
#### Vendoring
|
||||||
|
- `just vendor` - Vendor dependencies locally and create vendor.tar
|
||||||
|
- `just vendor-extract` - Extract vendored dependencies from vendor.tar
|
||||||
53
cargo.just
Normal file
53
cargo.just
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
cargo-target-dir := env('CARGO_TARGET_DIR', 'target')
|
||||||
|
|
||||||
|
# Compile with debug profile
|
||||||
|
build-debug *args:
|
||||||
|
cargo build {{ args }}
|
||||||
|
|
||||||
|
# Compile with release profile
|
||||||
|
build-release *args: (build-debug '--release' args)
|
||||||
|
|
||||||
|
# Compile with a vendored tarball
|
||||||
|
build-vendored *args: vendor-extract (build-release '--frozen --offline' args)
|
||||||
|
|
||||||
|
# Check for errors and linter warnings
|
||||||
|
check *args:
|
||||||
|
cargo clippy --all-features {{ args }} -- -W clippy::pedantic
|
||||||
|
|
||||||
|
# Remove Cargo build artifacts
|
||||||
|
[no-cd]
|
||||||
|
clean:
|
||||||
|
cargo clean
|
||||||
|
|
||||||
|
# Also remove .cargo and vendored dependencies
|
||||||
|
[no-cd]
|
||||||
|
clean-dist: clean
|
||||||
|
rm -rf .cargo vendor vendor.tar
|
||||||
|
|
||||||
|
# Vendor Cargo dependencies locally
|
||||||
|
[no-cd]
|
||||||
|
vendor:
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
mkdir -p .cargo
|
||||||
|
cargo vendor --sync Cargo.toml | head -n -1 > .cargo/config.toml
|
||||||
|
echo 'directory = "vendor"' >> .cargo/config.toml
|
||||||
|
echo >> .cargo/config.toml
|
||||||
|
echo '[env]' >> .cargo/config.toml
|
||||||
|
if [ -n "${SOURCE_DATE_EPOCH}" ]
|
||||||
|
then
|
||||||
|
source_date="$(date -d "@${SOURCE_DATE_EPOCH}" "+%Y-%m-%d")"
|
||||||
|
echo "VERGEN_GIT_COMMIT_DATE = \"${source_date}\"" >> .cargo/config.toml
|
||||||
|
fi
|
||||||
|
if [ -n "${SOURCE_GIT_HASH}" ]
|
||||||
|
then
|
||||||
|
echo "GIT_HASH = \"${SOURCE_GIT_HASH}\"" >> .cargo/config.toml
|
||||||
|
fi
|
||||||
|
tar pcf vendor.tar .cargo vendor
|
||||||
|
rm -rf .cargo vendor
|
||||||
|
|
||||||
|
# Extracts vendored dependencies
|
||||||
|
[no-cd]
|
||||||
|
[private]
|
||||||
|
vendor-extract:
|
||||||
|
rm -rf vendor
|
||||||
|
tar pxf vendor.tar
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
use cosmic_config::{CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry};
|
use cosmic_config::{CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::input::TouchpadOverride;
|
use crate::input::TouchpadOverride;
|
||||||
|
|
||||||
|
|
@ -106,8 +107,8 @@ pub struct CosmicCompConfig {
|
||||||
pub edge_snap_threshold: u32,
|
pub edge_snap_threshold: u32,
|
||||||
pub accessibility_zoom: ZoomConfig,
|
pub accessibility_zoom: ZoomConfig,
|
||||||
pub appearance_settings: AppearanceConfig,
|
pub appearance_settings: AppearanceConfig,
|
||||||
/// Hide the cursor after this many seconds of pointer inactivity (None disables)
|
/// When the cursor hides itself: idle, fullscreen idle, typing, touch
|
||||||
pub cursor_hide_timeout: Option<u32>,
|
pub cursor_hide: CursorHideConfig,
|
||||||
/// Briefly magnify the cursor when the pointer is shaken, to help locate it
|
/// Briefly magnify the cursor when the pointer is shaken, to help locate it
|
||||||
pub cursor_shake_to_find: bool,
|
pub cursor_shake_to_find: bool,
|
||||||
pub activation_policy: ActivationPolicy,
|
pub activation_policy: ActivationPolicy,
|
||||||
|
|
@ -148,7 +149,7 @@ impl Default for CosmicCompConfig {
|
||||||
edge_snap_threshold: 0,
|
edge_snap_threshold: 0,
|
||||||
accessibility_zoom: ZoomConfig::default(),
|
accessibility_zoom: ZoomConfig::default(),
|
||||||
appearance_settings: AppearanceConfig::default(),
|
appearance_settings: AppearanceConfig::default(),
|
||||||
cursor_hide_timeout: None,
|
cursor_hide: CursorHideConfig::default(),
|
||||||
cursor_shake_to_find: true,
|
cursor_shake_to_find: true,
|
||||||
activation_policy: ActivationPolicy::default(),
|
activation_policy: ActivationPolicy::default(),
|
||||||
decoration_preference: DecorationPreference::default(),
|
decoration_preference: DecorationPreference::default(),
|
||||||
|
|
@ -198,6 +199,81 @@ fn default_repeat_delay() -> u32 {
|
||||||
600
|
600
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What the cursor idle timer should do when it fires.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum HideDecision {
|
||||||
|
/// Nothing applies in this context; drop the timer until the next activity.
|
||||||
|
Drop,
|
||||||
|
/// Hide the cursor now.
|
||||||
|
Hide,
|
||||||
|
/// Not yet — re-arm for this long.
|
||||||
|
RearmAfter(Duration),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When the cursor hides itself. Every trigger is revealed by pointer input, so
|
||||||
|
/// these differ only in what arms the hide and after how long.
|
||||||
|
// `default`: a trigger added later must not make every existing file fail to
|
||||||
|
// deserialize, which `get_entry` would swallow as "reset everyone to defaults".
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct CursorHideConfig {
|
||||||
|
/// Seconds of pointer inactivity before hiding, anywhere. `None` disables.
|
||||||
|
pub idle_timeout: Option<u32>,
|
||||||
|
/// Seconds of pointer inactivity before hiding over a fullscreen window.
|
||||||
|
/// Shortens `idle_timeout` in fullscreen; it never lengthens it.
|
||||||
|
pub fullscreen_idle_timeout: Option<u32>,
|
||||||
|
/// Hide as soon as a key is pressed.
|
||||||
|
pub while_typing: bool,
|
||||||
|
/// Hide on touch input, until the pointer next moves.
|
||||||
|
pub after_touch: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursorHideConfig {
|
||||||
|
/// The shortest timeout that applies right now, if any.
|
||||||
|
pub fn effective_timeout(&self, fullscreen: bool) -> Option<Duration> {
|
||||||
|
let secs = if fullscreen {
|
||||||
|
match (self.idle_timeout, self.fullscreen_idle_timeout) {
|
||||||
|
(Some(a), Some(b)) => Some(a.min(b)),
|
||||||
|
(a, b) => a.or(b),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.idle_timeout
|
||||||
|
}?;
|
||||||
|
Some(Duration::from_secs(secs as u64))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What to do when the idle timer fires after `elapsed` without pointer input.
|
||||||
|
pub fn resolve(&self, elapsed: Duration, fullscreen: bool) -> HideDecision {
|
||||||
|
match self.effective_timeout(fullscreen) {
|
||||||
|
None => HideDecision::Drop,
|
||||||
|
Some(timeout) if elapsed >= timeout => HideDecision::Hide,
|
||||||
|
Some(timeout) => HideDecision::RearmAfter(timeout - elapsed),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How long to wait before the idle timer should next fire, given how long
|
||||||
|
/// it has been since the last pointer event. Pessimistic: fullscreen only
|
||||||
|
/// ever shortens a timeout, so arming as if fullscreen cannot fire too
|
||||||
|
/// late, and `resolve` corrects an early fire once the context is known.
|
||||||
|
pub fn arm_delay(&self, since_pointer_activity: Duration) -> Option<Duration> {
|
||||||
|
Some(
|
||||||
|
self.effective_timeout(true)?
|
||||||
|
.saturating_sub(since_pointer_activity),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CursorHideConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
CursorHideConfig {
|
||||||
|
idle_timeout: None,
|
||||||
|
fullscreen_idle_timeout: Some(3),
|
||||||
|
while_typing: false,
|
||||||
|
after_touch: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
pub struct ZoomConfig {
|
pub struct ZoomConfig {
|
||||||
pub start_on_login: bool,
|
pub start_on_login: bool,
|
||||||
|
|
@ -263,3 +339,109 @@ pub enum XwaylandDescaling {
|
||||||
#[default]
|
#[default]
|
||||||
Fractional,
|
Fractional,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::{CursorHideConfig, HideDecision};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const OFF: CursorHideConfig = CursorHideConfig {
|
||||||
|
idle_timeout: None,
|
||||||
|
fullscreen_idle_timeout: None,
|
||||||
|
while_typing: false,
|
||||||
|
after_touch: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn secs(n: u64) -> Duration {
|
||||||
|
Duration::from_secs(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_timeout_never_hides() {
|
||||||
|
assert_eq!(OFF.resolve(secs(600), false), HideDecision::Drop);
|
||||||
|
assert_eq!(OFF.resolve(secs(600), true), HideDecision::Drop);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn global_timeout_applies_everywhere() {
|
||||||
|
let cfg = CursorHideConfig {
|
||||||
|
idle_timeout: Some(10),
|
||||||
|
..OFF
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
cfg.resolve(secs(4), false),
|
||||||
|
HideDecision::RearmAfter(secs(6))
|
||||||
|
);
|
||||||
|
assert_eq!(cfg.resolve(secs(10), false), HideDecision::Hide);
|
||||||
|
// A fullscreen window must not suppress an enabled global timeout.
|
||||||
|
assert_eq!(cfg.resolve(secs(10), true), HideDecision::Hide);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fullscreen_timeout_only_applies_in_fullscreen() {
|
||||||
|
let cfg = CursorHideConfig {
|
||||||
|
fullscreen_idle_timeout: Some(3),
|
||||||
|
..OFF
|
||||||
|
};
|
||||||
|
assert_eq!(cfg.resolve(secs(3), true), HideDecision::Hide);
|
||||||
|
assert_eq!(cfg.resolve(secs(600), false), HideDecision::Drop);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fullscreen_shortens_but_never_lengthens() {
|
||||||
|
let short_fs = CursorHideConfig {
|
||||||
|
idle_timeout: Some(10),
|
||||||
|
fullscreen_idle_timeout: Some(3),
|
||||||
|
..OFF
|
||||||
|
};
|
||||||
|
assert_eq!(short_fs.resolve(secs(3), true), HideDecision::Hide);
|
||||||
|
assert_eq!(
|
||||||
|
short_fs.resolve(secs(3), false),
|
||||||
|
HideDecision::RearmAfter(secs(7))
|
||||||
|
);
|
||||||
|
|
||||||
|
let long_fs = CursorHideConfig {
|
||||||
|
idle_timeout: Some(3),
|
||||||
|
fullscreen_idle_timeout: Some(30),
|
||||||
|
..OFF
|
||||||
|
};
|
||||||
|
assert_eq!(long_fs.resolve(secs(3), true), HideDecision::Hide);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn boundary_is_inclusive() {
|
||||||
|
let cfg = CursorHideConfig {
|
||||||
|
idle_timeout: Some(5),
|
||||||
|
..OFF
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
cfg.resolve(Duration::from_millis(4999), false),
|
||||||
|
HideDecision::RearmAfter(Duration::from_millis(1))
|
||||||
|
);
|
||||||
|
assert_eq!(cfg.resolve(secs(5), false), HideDecision::Hide);
|
||||||
|
assert_eq!(cfg.resolve(secs(6), false), HideDecision::Hide);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arm_delay_measures_from_the_last_pointer_event() {
|
||||||
|
let cfg = CursorHideConfig {
|
||||||
|
idle_timeout: Some(10),
|
||||||
|
..OFF
|
||||||
|
};
|
||||||
|
assert_eq!(cfg.arm_delay(Duration::ZERO), Some(secs(10)));
|
||||||
|
assert_eq!(cfg.arm_delay(secs(4)), Some(secs(6)));
|
||||||
|
// Exactly due, and past due: fire immediately rather than deferring.
|
||||||
|
assert_eq!(cfg.arm_delay(secs(10)), Some(Duration::ZERO));
|
||||||
|
assert_eq!(cfg.arm_delay(secs(600)), Some(Duration::ZERO));
|
||||||
|
|
||||||
|
// Armed against the shortest timeout that any context could want.
|
||||||
|
let fs = CursorHideConfig {
|
||||||
|
idle_timeout: Some(10),
|
||||||
|
fullscreen_idle_timeout: Some(3),
|
||||||
|
..OFF
|
||||||
|
};
|
||||||
|
assert_eq!(fs.arm_delay(secs(1)), Some(secs(2)));
|
||||||
|
|
||||||
|
assert_eq!(OFF.arm_delay(Duration::ZERO), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
1
debian/control
vendored
1
debian/control
vendored
|
|
@ -6,6 +6,7 @@ Build-Depends:
|
||||||
cargo,
|
cargo,
|
||||||
cmake,
|
cmake,
|
||||||
debhelper-compat (=10),
|
debhelper-compat (=10),
|
||||||
|
just,
|
||||||
libegl1-mesa-dev,
|
libegl1-mesa-dev,
|
||||||
libfontconfig-dev,
|
libfontconfig-dev,
|
||||||
libgbm-dev,
|
libgbm-dev,
|
||||||
|
|
|
||||||
12
debian/rules
vendored
12
debian/rules
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/make -f
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
export sysconfdir = "/usr/share"
|
DESTDIR = debian/cosmic-comp
|
||||||
export VENDOR ?= 1
|
export VENDOR ?= 1
|
||||||
CLEAN ?= 1
|
CLEAN ?= 1
|
||||||
|
|
||||||
|
|
@ -9,12 +9,18 @@ CLEAN ?= 1
|
||||||
|
|
||||||
override_dh_auto_clean:
|
override_dh_auto_clean:
|
||||||
ifeq ($(CLEAN),1)
|
ifeq ($(CLEAN),1)
|
||||||
ischroot && make clean || make distclean
|
ischroot && just clean || just clean-dist
|
||||||
endif
|
endif
|
||||||
ifeq ($(VENDOR),1)
|
ifeq ($(VENDOR),1)
|
||||||
ischroot || make vendor
|
ischroot || just vendor
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
override_dh_auto_build:
|
||||||
|
just build-vendored
|
||||||
|
|
||||||
|
override_dh_auto_install:
|
||||||
|
just rootdir=$(DESTDIR) install
|
||||||
|
|
||||||
override_dh_installinit:
|
override_dh_installinit:
|
||||||
dh_installinit -r
|
dh_installinit -r
|
||||||
|
|
||||||
|
|
|
||||||
64
justfile
Normal file
64
justfile
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
name := 'cosmic-comp'
|
||||||
|
|
||||||
|
rootdir := ''
|
||||||
|
prefix := '/usr'
|
||||||
|
sysconfdir := '/etc'
|
||||||
|
|
||||||
|
base-dir := absolute_path(clean(rootdir / prefix))
|
||||||
|
sysconfdir-dir := absolute_path(clean(rootdir / sysconfdir))
|
||||||
|
|
||||||
|
bin-dir := base-dir / 'bin'
|
||||||
|
lib-dir := base-dir / 'lib'
|
||||||
|
share-dir := base-dir / 'share'
|
||||||
|
|
||||||
|
bin-src := cargo-target-dir / 'release' / name
|
||||||
|
bin-dst := bin-dir / name
|
||||||
|
|
||||||
|
keybindings-src := 'data' / 'keybindings.ron'
|
||||||
|
keybindings-dst := share-dir / 'cosmic' / 'com.system76.CosmicSettings.Shortcuts' / 'v1' / 'defaults'
|
||||||
|
|
||||||
|
tiling-exceptions-src := 'data' / 'tiling-exceptions.ron'
|
||||||
|
tiling-exceptions-dst := share-dir / 'cosmic' / 'com.system76.CosmicSettings.WindowRules' / 'v1' / 'tiling_exception_defaults'
|
||||||
|
|
||||||
|
session-desktop-src := 'data' / 'cosmic.desktop'
|
||||||
|
session-desktop-dst := share-dir / 'wayland-sessions' / 'cosmic.desktop'
|
||||||
|
|
||||||
|
systemd-session-target-src := 'data' / 'cosmic-session.target'
|
||||||
|
systemd-session-target-dst := lib-dir / 'systemd' / 'user' / 'cosmic-session.target'
|
||||||
|
|
||||||
|
systemd-session-pre-target-src := 'data' / 'cosmic-session-pre.target'
|
||||||
|
systemd-session-pre-target-dst := lib-dir / 'systemd' / 'user' / 'cosmic-session-pre.target'
|
||||||
|
|
||||||
|
systemd-comp-service-src := 'data' / 'cosmic-comp.service'
|
||||||
|
systemd-comp-service-dst := lib-dir / 'systemd' / 'user' / 'cosmic-comp.service'
|
||||||
|
|
||||||
|
cosmic-service-src := 'data' / 'cosmic-service'
|
||||||
|
cosmic-service-dst := bin-dir / 'cosmic-service'
|
||||||
|
|
||||||
|
import 'cargo.just'
|
||||||
|
|
||||||
|
# Default recipe which runs `just build-release`
|
||||||
|
[private]
|
||||||
|
default: build-release
|
||||||
|
|
||||||
|
# Installs files
|
||||||
|
install:
|
||||||
|
install -Dm0755 {{bin-src}} {{bin-dst}}
|
||||||
|
install -Dm0644 {{keybindings-src}} {{keybindings-dst}}
|
||||||
|
install -Dm0644 {{tiling-exceptions-src}} {{tiling-exceptions-dst}}
|
||||||
|
|
||||||
|
# Installs files including bare session files
|
||||||
|
install-bare-session: install
|
||||||
|
install -Dm0644 {{session-desktop-src}} {{session-desktop-dst}}
|
||||||
|
install -Dm0644 {{systemd-session-target-src}} {{systemd-session-target-dst}}
|
||||||
|
install -Dm0644 {{systemd-session-pre-target-src}} {{systemd-session-pre-target-dst}}
|
||||||
|
install -Dm0644 {{systemd-comp-service-src}} {{systemd-comp-service-dst}}
|
||||||
|
install -Dm0755 {{cosmic-service-src}} {{cosmic-service-dst}}
|
||||||
|
|
||||||
|
# Uninstalls installed files
|
||||||
|
uninstall:
|
||||||
|
rm {{bin-dst}} {{keybindings-dst}} {{tiling-exceptions-dst}}
|
||||||
|
|
||||||
|
# Uninstalls installed files including bare session files
|
||||||
|
uninstall-bare-session: uninstall
|
||||||
|
rm {{session-desktop-dst}} {{systemd-session-target-dst}} {{systemd-session-pre-target-dst}} {{systemd-comp-service-dst}} {{cosmic-service-dst}}
|
||||||
|
|
@ -615,7 +615,13 @@ impl State {
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
let mut feedback =
|
let mut feedback =
|
||||||
DmabufFeedbackBuilder::new(primary_node.dev_id(), primary_formats.clone());
|
DmabufFeedbackBuilder::new(primary_node.dev_id(), primary_formats.clone())
|
||||||
|
.add_preference_tranche(
|
||||||
|
primary_node.dev_id(),
|
||||||
|
TrancheFlags::Sampling,
|
||||||
|
primary_formats.iter().cloned(),
|
||||||
|
3..=6,
|
||||||
|
);
|
||||||
for dev in kms
|
for dev in kms
|
||||||
.drm_devices
|
.drm_devices
|
||||||
.values()
|
.values()
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ use crate::{
|
||||||
element::AsGlowRenderer,
|
element::AsGlowRenderer,
|
||||||
wayland::{SurfaceRenderElement, push_render_elements_from_surface_tree},
|
wayland::{SurfaceRenderElement, push_render_elements_from_surface_tree},
|
||||||
},
|
},
|
||||||
|
shell::focus::target::KeyboardFocusTarget,
|
||||||
utils::prelude::*,
|
utils::prelude::*,
|
||||||
wayland::handlers::compositor::FRAME_TIME_FILTER,
|
wayland::handlers::compositor::FRAME_TIME_FILTER,
|
||||||
};
|
};
|
||||||
|
use cosmic_comp_config::HideDecision;
|
||||||
use keyframe::{ease, functions::EaseInOutCubic};
|
use keyframe::{ease, functions::EaseInOutCubic};
|
||||||
use resvg::{tiny_skia, usvg};
|
use resvg::{tiny_skia, usvg};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
@ -451,6 +453,32 @@ pub fn draw_dnd_icon<R>(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Why the cursor is currently hidden. Determines what brings it back.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum HideReason {
|
||||||
|
/// No pointer activity for the configured timeout.
|
||||||
|
Idle,
|
||||||
|
/// A key was pressed while `while_typing` is enabled.
|
||||||
|
Typing,
|
||||||
|
/// Touch input arrived while `after_touch` is enabled.
|
||||||
|
Touch,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a pointer event carries movement. A touch-hidden cursor comes back
|
||||||
|
/// only on real movement, so the distinction has to reach the call site.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PointerEventKind {
|
||||||
|
Motion,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HideReason {
|
||||||
|
/// Whether an event of this kind should reveal the cursor.
|
||||||
|
fn revealed_by(self, kind: PointerEventKind) -> bool {
|
||||||
|
matches!(kind, PointerEventKind::Motion) || !matches!(self, HideReason::Touch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type CursorState = Mutex<CursorStateInner>;
|
pub type CursorState = Mutex<CursorStateInner>;
|
||||||
pub struct CursorStateInner {
|
pub struct CursorStateInner {
|
||||||
current_cursor: Option<CursorIcon>,
|
current_cursor: Option<CursorIcon>,
|
||||||
|
|
@ -462,9 +490,10 @@ pub struct CursorStateInner {
|
||||||
current_image: Option<Image>,
|
current_image: Option<Image>,
|
||||||
image_cache: Vec<CachedFrame>,
|
image_cache: Vec<CachedFrame>,
|
||||||
|
|
||||||
hidden: bool,
|
hidden: Option<HideReason>,
|
||||||
idle_timer: Option<RegistrationToken>,
|
idle_timer: Option<RegistrationToken>,
|
||||||
last_armed: Option<Instant>,
|
last_armed: Option<Instant>,
|
||||||
|
last_pointer_activity: Instant,
|
||||||
|
|
||||||
// shake-to-find
|
// shake-to-find
|
||||||
shake_path: VecDeque<PathSample>,
|
shake_path: VecDeque<PathSample>,
|
||||||
|
|
@ -694,7 +723,8 @@ impl Default for CursorStateInner {
|
||||||
current_image: None,
|
current_image: None,
|
||||||
image_cache: Vec::new(),
|
image_cache: Vec::new(),
|
||||||
|
|
||||||
hidden: false,
|
hidden: None,
|
||||||
|
last_pointer_activity: Instant::now(),
|
||||||
idle_timer: None,
|
idle_timer: None,
|
||||||
last_armed: None,
|
last_armed: None,
|
||||||
|
|
||||||
|
|
@ -747,7 +777,7 @@ pub fn draw_cursor<R>(
|
||||||
let mut state_ref = seat_userdata.get::<CursorState>().unwrap().lock().unwrap();
|
let mut state_ref = seat_userdata.get::<CursorState>().unwrap().lock().unwrap();
|
||||||
let state = &mut *state_ref;
|
let state = &mut *state_ref;
|
||||||
|
|
||||||
if state.hidden {
|
if state.hidden.is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -837,68 +867,175 @@ pub fn draw_cursor<R>(
|
||||||
|
|
||||||
const ACTIVITY_THROTTLE: Duration = Duration::from_millis(100);
|
const ACTIVITY_THROTTLE: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
/// Reveal the cursor and (re)arm the idle-hide timer; returns true if it was previously hidden
|
/// Reveal the cursor if this event should, and (re)arm the idle-hide timer.
|
||||||
pub fn notify_cursor_activity(state: &State, seat: &Seat<State>) -> bool {
|
/// Returns true if the cursor was previously hidden and is now visible.
|
||||||
let timeout = state.common.config.cosmic_conf.cursor_hide_timeout;
|
pub fn notify_cursor_activity(state: &State, seat: &Seat<State>, kind: PointerEventKind) -> bool {
|
||||||
|
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
||||||
|
|
||||||
|
let revealed = {
|
||||||
|
let mut inner = cursor_state.lock().unwrap();
|
||||||
|
inner.last_pointer_activity = Instant::now();
|
||||||
|
match inner.hidden {
|
||||||
|
Some(reason) if reason.revealed_by(kind) => {
|
||||||
|
inner.hidden = None;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
// Still hidden on purpose: leave the timer alone.
|
||||||
|
Some(_) => return false,
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
refresh_idle_timer(state, seat);
|
||||||
|
revealed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (Re)arm the idle-hide timer without counting as pointer activity.
|
||||||
|
///
|
||||||
|
/// The deadline is measured from the last *pointer* event, not from now, so a
|
||||||
|
/// caller that does not stamp activity — a keystroke, a config reload — cannot
|
||||||
|
/// postpone the hide by re-arming.
|
||||||
|
///
|
||||||
|
/// Armed with the shortest timeout any context could want: the input that arms
|
||||||
|
/// the timer is usually what goes on to make a window fullscreen, so the
|
||||||
|
/// context is not yet knowable here. `on_idle_timer` evaluates it on fire and
|
||||||
|
/// re-arms for the remainder if nothing applied yet.
|
||||||
|
pub fn refresh_idle_timer(state: &State, seat: &Seat<State>) {
|
||||||
|
let config = state.common.config.cosmic_conf.cursor_hide;
|
||||||
let loop_handle = &state.common.event_loop_handle;
|
let loop_handle = &state.common.event_loop_handle;
|
||||||
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
|
||||||
let (was_hidden, old_token) = {
|
let (old_token, since_activity) = {
|
||||||
let mut inner = cursor_state.lock().unwrap();
|
let mut inner = cursor_state.lock().unwrap();
|
||||||
let was_hidden = inner.hidden;
|
if inner.hidden.is_some() {
|
||||||
inner.hidden = false;
|
return;
|
||||||
|
}
|
||||||
let throttled = timeout.is_some()
|
// Coalesce bursts from high-frequency pointers.
|
||||||
&& !was_hidden
|
let throttled = inner.idle_timer.is_some()
|
||||||
&& inner.idle_timer.is_some()
|
|
||||||
&& inner
|
&& inner
|
||||||
.last_armed
|
.last_armed
|
||||||
.is_some_and(|t| now.duration_since(t) < ACTIVITY_THROTTLE);
|
.is_some_and(|t| now.duration_since(t) < ACTIVITY_THROTTLE);
|
||||||
if throttled {
|
if throttled {
|
||||||
return was_hidden;
|
return;
|
||||||
}
|
}
|
||||||
|
let since_activity = now.duration_since(inner.last_pointer_activity);
|
||||||
let old_token = inner.idle_timer.take();
|
|
||||||
inner.last_armed = None;
|
inner.last_armed = None;
|
||||||
(was_hidden, old_token)
|
(inner.idle_timer.take(), since_activity)
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(token) = old_token {
|
if let Some(token) = old_token {
|
||||||
loop_handle.remove(token);
|
loop_handle.remove(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(secs) = timeout {
|
let Some(delay) = config.arm_delay(since_activity) else {
|
||||||
let timer = Timer::from_duration(Duration::from_secs(secs as u64));
|
return;
|
||||||
let seat = seat.clone();
|
};
|
||||||
if let Ok(token) = loop_handle.insert_source(timer, move |_, _, state| {
|
|
||||||
hide_cursor(state, &seat);
|
let timer = Timer::from_duration(delay);
|
||||||
TimeoutAction::Drop
|
let timer_seat = seat.clone();
|
||||||
}) {
|
match loop_handle.insert_source(timer, move |_, _, state| on_idle_timer(state, &timer_seat)) {
|
||||||
|
Ok(token) => {
|
||||||
let mut inner = cursor_state.lock().unwrap();
|
let mut inner = cursor_state.lock().unwrap();
|
||||||
inner.idle_timer = Some(token);
|
inner.idle_timer = Some(token);
|
||||||
inner.last_armed = Some(now);
|
inner.last_armed = Some(now);
|
||||||
}
|
}
|
||||||
|
// `idle_timer` stays `None`, so the next activity is unthrottled and retries.
|
||||||
|
Err(err) => warn!(?err, "Failed to arm the cursor idle-hide timer"),
|
||||||
}
|
}
|
||||||
|
|
||||||
was_hidden
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hide_cursor(state: &mut State, seat: &Seat<State>) {
|
/// Whether the seat is focusing a fullscreen surface. A fullscreen window that
|
||||||
if let Some(ptr) = seat.get_pointer()
|
/// is not focused can be stacked behind others, so its presence on the output
|
||||||
&& ptr.is_grabbed()
|
/// says nothing about what the user is looking at.
|
||||||
{
|
fn fullscreen_focused(seat: &Seat<State>) -> bool {
|
||||||
return;
|
matches!(
|
||||||
|
seat.get_keyboard().and_then(|kbd| kbd.current_focus()),
|
||||||
|
Some(KeyboardFocusTarget::Fullscreen(_))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_idle_timer(state: &mut State, seat: &Seat<State>) -> TimeoutAction {
|
||||||
|
let config = state.common.config.cosmic_conf.cursor_hide;
|
||||||
|
let fullscreen = fullscreen_focused(seat);
|
||||||
|
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
||||||
|
|
||||||
|
let elapsed = {
|
||||||
|
let inner = cursor_state.lock().unwrap();
|
||||||
|
Instant::now().duration_since(inner.last_pointer_activity)
|
||||||
|
};
|
||||||
|
|
||||||
|
match config.resolve(elapsed, fullscreen) {
|
||||||
|
HideDecision::Hide => {
|
||||||
|
hide_cursor(state, seat, HideReason::Idle);
|
||||||
|
TimeoutAction::Drop
|
||||||
|
}
|
||||||
|
HideDecision::Drop => {
|
||||||
|
let mut inner = cursor_state.lock().unwrap();
|
||||||
|
inner.idle_timer = None;
|
||||||
|
inner.last_armed = None;
|
||||||
|
TimeoutAction::Drop
|
||||||
|
}
|
||||||
|
HideDecision::RearmAfter(delay) => {
|
||||||
|
cursor_state.lock().unwrap().last_armed = Some(Instant::now());
|
||||||
|
TimeoutAction::ToDuration(delay)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hide the cursor immediately, for a trigger that does not wait on the timer.
|
||||||
|
pub fn hide_cursor_now(state: &mut State, seat: &Seat<State>, reason: HideReason) {
|
||||||
|
let loop_handle = state.common.event_loop_handle.clone();
|
||||||
|
let token = {
|
||||||
|
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
||||||
|
let mut inner = cursor_state.lock().unwrap();
|
||||||
|
inner.idle_timer.take()
|
||||||
|
};
|
||||||
|
if let Some(token) = token {
|
||||||
|
loop_handle.remove(token);
|
||||||
|
}
|
||||||
|
hide_cursor(state, seat, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hide_cursor(state: &mut State, seat: &Seat<State>, reason: HideReason) {
|
||||||
|
let grabbed = seat.get_pointer().is_some_and(|ptr| ptr.is_grabbed());
|
||||||
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
||||||
{
|
{
|
||||||
let mut inner = cursor_state.lock().unwrap();
|
let mut inner = cursor_state.lock().unwrap();
|
||||||
inner.hidden = true;
|
// Every caller has already disposed of the timer source, so clear the
|
||||||
|
// bookkeeping before the grab check rather than only on success.
|
||||||
inner.idle_timer = None;
|
inner.idle_timer = None;
|
||||||
inner.last_armed = None;
|
inner.last_armed = None;
|
||||||
|
// The reason only ever tightens: `Touch` waits for real movement, so a
|
||||||
|
// later trigger must not relax it back to click-or-scroll.
|
||||||
|
if grabbed || inner.hidden == Some(reason) || inner.hidden == Some(HideReason::Touch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inner.hidden = Some(reason);
|
||||||
}
|
}
|
||||||
let outputs: Vec<_> = state.common.shell.read().outputs().cloned().collect();
|
let outputs: Vec<_> = state.common.shell.read().outputs().cloned().collect();
|
||||||
for output in outputs {
|
for output in outputs {
|
||||||
state.backend.schedule_render(&output);
|
state.backend.schedule_render(&output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::{HideReason, PointerEventKind};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn touch_hidden_cursor_waits_for_movement() {
|
||||||
|
assert!(HideReason::Touch.revealed_by(PointerEventKind::Motion));
|
||||||
|
assert!(!HideReason::Touch.revealed_by(PointerEventKind::Other));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn idle_and_typing_hidden_cursors_are_revealed_by_any_pointer_event() {
|
||||||
|
for reason in [HideReason::Idle, HideReason::Typing] {
|
||||||
|
assert!(reason.revealed_by(PointerEventKind::Motion));
|
||||||
|
// A click or a scroll must rescue the cursor rather than forcing a
|
||||||
|
// blind click.
|
||||||
|
assert!(reason.revealed_by(PointerEventKind::Other));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use cosmic_config::{ConfigGet, CosmicConfigEntry};
|
use cosmic_config::{ConfigGet, ConfigSet, CosmicConfigEntry};
|
||||||
use cosmic_settings_config::window_rules::ApplicationException;
|
use cosmic_settings_config::window_rules::ApplicationException;
|
||||||
use cosmic_settings_config::{Shortcuts, shortcuts, window_rules};
|
use cosmic_settings_config::{Shortcuts, shortcuts, window_rules};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -49,8 +49,8 @@ mod types;
|
||||||
use cosmic::config::CosmicTk;
|
use cosmic::config::CosmicTk;
|
||||||
pub use cosmic_comp_config::EdidProduct;
|
pub use cosmic_comp_config::EdidProduct;
|
||||||
use cosmic_comp_config::{
|
use cosmic_comp_config::{
|
||||||
ActivationPolicy, AppearanceConfig, CosmicCompConfig, DecorationPreference, KeyboardConfig,
|
ActivationPolicy, AppearanceConfig, CosmicCompConfig, CursorHideConfig, DecorationPreference,
|
||||||
TileBehavior, XkbConfig, XwaylandDescaling, XwaylandEavesdropping, ZoomConfig,
|
KeyboardConfig, TileBehavior, XkbConfig, XwaylandDescaling, XwaylandEavesdropping, ZoomConfig,
|
||||||
input::{DeviceState as InputDeviceState, InputConfig, TouchpadOverride},
|
input::{DeviceState as InputDeviceState, InputConfig, TouchpadOverride},
|
||||||
output::comp::{
|
output::comp::{
|
||||||
OutputConfig, OutputInfo, OutputState, OutputsConfig, TransformDef, load_outputs,
|
OutputConfig, OutputInfo, OutputState, OutputsConfig, TransformDef, load_outputs,
|
||||||
|
|
@ -172,7 +172,7 @@ pub enum ColorFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load(loop_handle: &LoopHandle<'_, State>) -> Config {
|
pub fn load(loop_handle: &LoopHandle<'_, State>, kiosk_mode: bool) -> Config {
|
||||||
let config = cosmic_config::Config::new("com.system76.CosmicComp", 1).unwrap();
|
let config = cosmic_config::Config::new("com.system76.CosmicComp", 1).unwrap();
|
||||||
let source = cosmic_config::calloop::ConfigWatchSource::new(&config).unwrap();
|
let source = cosmic_config::calloop::ConfigWatchSource::new(&config).unwrap();
|
||||||
loop_handle
|
loop_handle
|
||||||
|
|
@ -182,7 +182,7 @@ impl Config {
|
||||||
.expect("Failed to add cosmic-config to the event loop");
|
.expect("Failed to add cosmic-config to the event loop");
|
||||||
let xdg = xdg::BaseDirectories::new();
|
let xdg = xdg::BaseDirectories::new();
|
||||||
|
|
||||||
let cosmic_comp_config =
|
let mut cosmic_comp_config =
|
||||||
CosmicCompConfig::get_entry(&config).unwrap_or_else(|(errs, c)| {
|
CosmicCompConfig::get_entry(&config).unwrap_or_else(|(errs, c)| {
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
for err in errs {
|
for err in errs {
|
||||||
|
|
@ -192,6 +192,18 @@ impl Config {
|
||||||
c
|
c
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// `cursor_hide_timeout` was replaced by the grouped `cursor_hide` key.
|
||||||
|
// Seed the new key once so configs hand-edited before the rename keep
|
||||||
|
// working; the old file is left in place so a downgrade still reads it.
|
||||||
|
if config.get::<CursorHideConfig>("cursor_hide").is_err()
|
||||||
|
&& let Ok(legacy) = config.get::<Option<u32>>("cursor_hide_timeout")
|
||||||
|
{
|
||||||
|
cosmic_comp_config.cursor_hide.idle_timeout = legacy;
|
||||||
|
if let Err(err) = config.set("cursor_hide", cosmic_comp_config.cursor_hide) {
|
||||||
|
warn!(?err, "Failed to migrate cursor_hide_timeout to cursor_hide");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Listen for updates to the toolkit config
|
// Listen for updates to the toolkit config
|
||||||
if let Ok(tk_config) = cosmic_config::Config::new("com.system76.CosmicTk", 1) {
|
if let Ok(tk_config) = cosmic_config::Config::new("com.system76.CosmicTk", 1) {
|
||||||
fn handle_new_toolkit_config(config: CosmicTk, state: &mut State) {
|
fn handle_new_toolkit_config(config: CosmicTk, state: &mut State) {
|
||||||
|
|
@ -248,40 +260,48 @@ impl Config {
|
||||||
|
|
||||||
// Source key bindings from com.system76.CosmicSettings.Shortcuts
|
// Source key bindings from com.system76.CosmicSettings.Shortcuts
|
||||||
let settings_context = shortcuts::context().expect("Failed to load shortcuts config");
|
let settings_context = shortcuts::context().expect("Failed to load shortcuts config");
|
||||||
let system_actions = shortcuts::system_actions(&settings_context);
|
let mut system_actions = Default::default();
|
||||||
let shortcuts = shortcuts::shortcuts(&settings_context);
|
let mut shortcuts = Default::default();
|
||||||
|
// Kiosk mode disables shortcuts
|
||||||
|
if !kiosk_mode {
|
||||||
|
system_actions = shortcuts::system_actions(&settings_context);
|
||||||
|
shortcuts = shortcuts::shortcuts(&settings_context);
|
||||||
|
|
||||||
// Listen for updates to the keybindings config.
|
// Listen for updates to the keybindings config.
|
||||||
match cosmic_config::calloop::ConfigWatchSource::new(&settings_context) {
|
match cosmic_config::calloop::ConfigWatchSource::new(&settings_context) {
|
||||||
Ok(source) => {
|
Ok(source) => {
|
||||||
if let Err(err) = loop_handle.insert_source(source, |(config, keys), (), state| {
|
if let Err(err) =
|
||||||
for key in keys {
|
loop_handle.insert_source(source, |(config, keys), (), state| {
|
||||||
match key.as_str() {
|
for key in keys {
|
||||||
// Reload the keyboard shortcuts config.
|
match key.as_str() {
|
||||||
"custom" | "defaults" => {
|
// Reload the keyboard shortcuts config.
|
||||||
state.common.config.shortcuts = shortcuts::shortcuts(&config);
|
"custom" | "defaults" => {
|
||||||
|
state.common.config.shortcuts =
|
||||||
|
shortcuts::shortcuts(&config);
|
||||||
|
}
|
||||||
|
|
||||||
|
"system_actions" => {
|
||||||
|
state.common.config.system_actions =
|
||||||
|
shortcuts::system_actions(&config);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
"system_actions" => {
|
{
|
||||||
state.common.config.system_actions =
|
warn!(
|
||||||
shortcuts::system_actions(&config);
|
?err,
|
||||||
}
|
"Failed to watch com.system76.CosmicSettings.Shortcuts config"
|
||||||
|
);
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}) {
|
|
||||||
warn!(
|
|
||||||
?err,
|
|
||||||
"Failed to watch com.system76.CosmicSettings.Shortcuts config"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
Err(err) => warn!(
|
||||||
Err(err) => warn!(
|
?err,
|
||||||
?err,
|
"failed to create config watch source for com.system76.CosmicSettings.Shortcuts"
|
||||||
"failed to create config watch source for com.system76.CosmicSettings.Shortcuts"
|
),
|
||||||
),
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
let window_rules_context =
|
let window_rules_context =
|
||||||
window_rules::context().expect("Failed to load window rules config");
|
window_rules::context().expect("Failed to load window rules config");
|
||||||
|
|
@ -983,15 +1003,21 @@ fn config_changed(config: cosmic_config::Config, keys: Vec<String>, state: &mut
|
||||||
let new = get_config::<bool>(&config, "cursor_shake_to_find");
|
let new = get_config::<bool>(&config, "cursor_shake_to_find");
|
||||||
state.common.config.cosmic_conf.cursor_shake_to_find = new;
|
state.common.config.cosmic_conf.cursor_shake_to_find = new;
|
||||||
}
|
}
|
||||||
"cursor_hide_timeout" => {
|
"cursor_hide" => {
|
||||||
let new = get_config::<Option<u32>>(&config, "cursor_hide_timeout");
|
let new = get_config::<CursorHideConfig>(&config, "cursor_hide");
|
||||||
if new != state.common.config.cosmic_conf.cursor_hide_timeout {
|
if new != state.common.config.cosmic_conf.cursor_hide {
|
||||||
state.common.config.cosmic_conf.cursor_hide_timeout = new;
|
state.common.config.cosmic_conf.cursor_hide = new;
|
||||||
|
// Reveal on every change: a visible cursor is the safe state,
|
||||||
|
// and it avoids stranding a hidden cursor when the trigger
|
||||||
|
// that hid it is switched off.
|
||||||
let seats: Vec<_> = state.common.shell.read().seats.iter().cloned().collect();
|
let seats: Vec<_> = state.common.shell.read().seats.iter().cloned().collect();
|
||||||
let mut needs_render = false;
|
let mut needs_render = false;
|
||||||
for seat in seats {
|
for seat in seats {
|
||||||
needs_render |=
|
needs_render |= crate::backend::render::cursor::notify_cursor_activity(
|
||||||
crate::backend::render::cursor::notify_cursor_activity(state, &seat);
|
state,
|
||||||
|
&seat,
|
||||||
|
crate::backend::render::cursor::PointerEventKind::Motion,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if needs_render {
|
if needs_render {
|
||||||
let outputs: Vec<_> =
|
let outputs: Vec<_> =
|
||||||
|
|
|
||||||
104
src/input/mod.rs
104
src/input/mod.rs
|
|
@ -1,7 +1,10 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::render::{ElementFilter, cursor::notify_cursor_activity},
|
backend::render::{
|
||||||
|
ElementFilter,
|
||||||
|
cursor::{PointerEventKind, notify_cursor_activity},
|
||||||
|
},
|
||||||
config::{
|
config::{
|
||||||
Action, Config, PrivateAction,
|
Action, Config, PrivateAction,
|
||||||
key_bindings::{
|
key_bindings::{
|
||||||
|
|
@ -19,7 +22,7 @@ use crate::{
|
||||||
Stage, render_input_order,
|
Stage, render_input_order,
|
||||||
target::{KeyboardFocusTarget, PointerFocusTarget},
|
target::{KeyboardFocusTarget, PointerFocusTarget},
|
||||||
},
|
},
|
||||||
grabs::{ReleaseMode, ResizeEdge},
|
grabs::{MenuGrab, MoveGrab, ReleaseMode, ResizeEdge, ResizeGrab},
|
||||||
layout::{
|
layout::{
|
||||||
floating::ResizeGrabMarker,
|
floating::ResizeGrabMarker,
|
||||||
tiling::{NodeDesc, SwapWindowGrab, TilingLayout},
|
tiling::{NodeDesc, SwapWindowGrab, TilingLayout},
|
||||||
|
|
@ -49,7 +52,7 @@ use smithay::{
|
||||||
PointerAxisEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent,
|
PointerAxisEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent,
|
||||||
TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent,
|
TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent,
|
||||||
},
|
},
|
||||||
desktop::{PopupKeyboardGrab, WindowSurfaceType, utils::under_from_surface_tree},
|
desktop::{WindowSurfaceType, utils::under_from_surface_tree},
|
||||||
input::{
|
input::{
|
||||||
Seat,
|
Seat,
|
||||||
keyboard::KeyboardHandle,
|
keyboard::KeyboardHandle,
|
||||||
|
|
@ -220,7 +223,16 @@ impl State {
|
||||||
) where
|
) where
|
||||||
<B as InputBackend>::Device: 'static,
|
<B as InputBackend>::Device: 'static,
|
||||||
{
|
{
|
||||||
crate::wayland::handlers::output_power::set_all_surfaces_dpms_on(self);
|
// An input device going away is not user activity, e.g. a wireless keyboard
|
||||||
|
// dropping its link when it sleeps, so it must not power displays back on.
|
||||||
|
// Device additions still wake: after a system resume the re-enumerated
|
||||||
|
// devices may be the only input seen, and the wake resets idle anyway.
|
||||||
|
if !matches!(
|
||||||
|
&event,
|
||||||
|
InputEvent::DeviceRemoved { .. } | InputEvent::Special(_)
|
||||||
|
) {
|
||||||
|
crate::wayland::handlers::output_power::set_all_surfaces_dpms_on(self);
|
||||||
|
}
|
||||||
|
|
||||||
use smithay::backend::input::Event;
|
use smithay::backend::input::Event;
|
||||||
match event {
|
match event {
|
||||||
|
|
@ -327,6 +339,26 @@ impl State {
|
||||||
keyboard.modifier_state().num_lock;
|
keyboard.modifier_state().num_lock;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A bare modifier press changes the modifier state; a real
|
||||||
|
// keystroke does not. Super+drag moves windows, so hiding on
|
||||||
|
// Super-down would take the cursor away exactly as the user
|
||||||
|
// reaches for it. Super+1 still counts as typing.
|
||||||
|
let bare_modifier = previous_modifiers != keyboard.modifier_state();
|
||||||
|
if self.common.config.cosmic_conf.cursor_hide.while_typing
|
||||||
|
&& state == KeyState::Pressed
|
||||||
|
&& !bare_modifier
|
||||||
|
{
|
||||||
|
crate::backend::render::cursor::hide_cursor_now(
|
||||||
|
self,
|
||||||
|
&seat,
|
||||||
|
crate::backend::render::cursor::HideReason::Typing,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Still refresh: this is how entering fullscreen by
|
||||||
|
// keyboard arms the fullscreen timeout.
|
||||||
|
crate::backend::render::cursor::refresh_idle_timer(self, &seat);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,7 +372,7 @@ impl State {
|
||||||
.cloned()
|
.cloned()
|
||||||
{
|
{
|
||||||
self.common.idle_notifier_state.notify_activity(&seat);
|
self.common.idle_notifier_state.notify_activity(&seat);
|
||||||
notify_cursor_activity(self, &seat);
|
notify_cursor_activity(self, &seat, PointerEventKind::Motion);
|
||||||
let current_output = seat.active_output();
|
let current_output = seat.active_output();
|
||||||
|
|
||||||
if self.common.config.cosmic_conf.cursor_shake_to_find
|
if self.common.config.cosmic_conf.cursor_shake_to_find
|
||||||
|
|
@ -714,7 +746,7 @@ impl State {
|
||||||
.cloned();
|
.cloned();
|
||||||
if let Some(seat) = maybe_seat {
|
if let Some(seat) = maybe_seat {
|
||||||
self.common.idle_notifier_state.notify_activity(&seat);
|
self.common.idle_notifier_state.notify_activity(&seat);
|
||||||
notify_cursor_activity(self, &seat);
|
notify_cursor_activity(self, &seat, PointerEventKind::Motion);
|
||||||
let (output, position) = if matches!(&backend_id, InputBackendId::Ei(_)) {
|
let (output, position) = if matches!(&backend_id, InputBackendId::Ei(_)) {
|
||||||
// EI absolute coordinates are in the compositor's *global*
|
// EI absolute coordinates are in the compositor's *global*
|
||||||
// logical space: each advertised region carries its output's
|
// logical space: each advertised region carries its output's
|
||||||
|
|
@ -809,7 +841,7 @@ impl State {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.common.idle_notifier_state.notify_activity(&seat);
|
self.common.idle_notifier_state.notify_activity(&seat);
|
||||||
notify_cursor_activity(self, &seat);
|
notify_cursor_activity(self, &seat, PointerEventKind::Other);
|
||||||
|
|
||||||
let current_focus = seat.get_keyboard().unwrap().current_focus();
|
let current_focus = seat.get_keyboard().unwrap().current_focus();
|
||||||
let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| {
|
let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| {
|
||||||
|
|
@ -1060,7 +1092,7 @@ impl State {
|
||||||
.cloned();
|
.cloned();
|
||||||
if let Some(seat) = maybe_seat {
|
if let Some(seat) = maybe_seat {
|
||||||
self.common.idle_notifier_state.notify_activity(&seat);
|
self.common.idle_notifier_state.notify_activity(&seat);
|
||||||
notify_cursor_activity(self, &seat);
|
notify_cursor_activity(self, &seat, PointerEventKind::Other);
|
||||||
|
|
||||||
if self.source_modifiers(&backend_id, &seat).logo
|
if self.source_modifiers(&backend_id, &seat).logo
|
||||||
&& self
|
&& self
|
||||||
|
|
@ -1462,11 +1494,18 @@ impl State {
|
||||||
};
|
};
|
||||||
let under = State::surface_under(position, &output, &shell)
|
let under = State::surface_under(position, &output, &shell)
|
||||||
.map(|(target, pos)| (target, pos.as_logical()));
|
.map(|(target, pos)| (target, pos.as_logical()));
|
||||||
|
let focus_target = State::element_under(position, &output, &shell, &seat);
|
||||||
|
|
||||||
std::mem::drop(shell);
|
std::mem::drop(shell);
|
||||||
|
|
||||||
let serial = SERIAL_COUNTER.next_serial();
|
let serial = SERIAL_COUNTER.next_serial();
|
||||||
let touch = seat.get_touch().unwrap();
|
let touch = seat.get_touch().unwrap();
|
||||||
|
// change the keyboard focus unless the touch is grabbed, like pointer buttons
|
||||||
|
if !touch.is_grabbed()
|
||||||
|
&& let Some(target) = focus_target.as_ref()
|
||||||
|
{
|
||||||
|
Shell::set_focus(self, Some(target), &seat, Some(serial), false);
|
||||||
|
}
|
||||||
touch.down(
|
touch.down(
|
||||||
self,
|
self,
|
||||||
under,
|
under,
|
||||||
|
|
@ -1477,6 +1516,14 @@ impl State {
|
||||||
time: event.time(),
|
time: event.time(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if self.common.config.cosmic_conf.cursor_hide.after_touch {
|
||||||
|
crate::backend::render::cursor::hide_cursor_now(
|
||||||
|
self,
|
||||||
|
&seat,
|
||||||
|
crate::backend::render::cursor::HideReason::Touch,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InputEvent::TouchMotion { event, .. } => {
|
InputEvent::TouchMotion { event, .. } => {
|
||||||
|
|
@ -1592,7 +1639,7 @@ impl State {
|
||||||
.cloned()
|
.cloned()
|
||||||
{
|
{
|
||||||
self.common.idle_notifier_state.notify_activity(&seat);
|
self.common.idle_notifier_state.notify_activity(&seat);
|
||||||
notify_cursor_activity(self, &seat);
|
notify_cursor_activity(self, &seat, PointerEventKind::Motion);
|
||||||
let Some(output) =
|
let Some(output) =
|
||||||
mapped_output_for_device(&self.common.config, &shell, &event.device())
|
mapped_output_for_device(&self.common.config, &shell, &event.device())
|
||||||
.cloned()
|
.cloned()
|
||||||
|
|
@ -1696,7 +1743,7 @@ impl State {
|
||||||
.cloned()
|
.cloned()
|
||||||
{
|
{
|
||||||
self.common.idle_notifier_state.notify_activity(&seat);
|
self.common.idle_notifier_state.notify_activity(&seat);
|
||||||
notify_cursor_activity(self, &seat);
|
notify_cursor_activity(self, &seat, PointerEventKind::Motion);
|
||||||
let Some(output) =
|
let Some(output) =
|
||||||
mapped_output_for_device(&self.common.config, &shell, &event.device())
|
mapped_output_for_device(&self.common.config, &shell, &event.device())
|
||||||
.cloned()
|
.cloned()
|
||||||
|
|
@ -1845,7 +1892,7 @@ impl State {
|
||||||
.cloned();
|
.cloned();
|
||||||
if let Some(seat) = maybe_seat {
|
if let Some(seat) = maybe_seat {
|
||||||
self.common.idle_notifier_state.notify_activity(&seat);
|
self.common.idle_notifier_state.notify_activity(&seat);
|
||||||
notify_cursor_activity(self, &seat);
|
notify_cursor_activity(self, &seat, PointerEventKind::Other);
|
||||||
|
|
||||||
let serial = SERIAL_COUNTER.next_serial();
|
let serial = SERIAL_COUNTER.next_serial();
|
||||||
let output = seat.active_output();
|
let output = seat.active_output();
|
||||||
|
|
@ -1898,7 +1945,7 @@ impl State {
|
||||||
.cloned();
|
.cloned();
|
||||||
if let Some(seat) = maybe_seat {
|
if let Some(seat) = maybe_seat {
|
||||||
self.common.idle_notifier_state.notify_activity(&seat);
|
self.common.idle_notifier_state.notify_activity(&seat);
|
||||||
notify_cursor_activity(self, &seat);
|
notify_cursor_activity(self, &seat, PointerEventKind::Other);
|
||||||
if let Some(tool) = seat.tablet_seat().get_tool(&event.tool()) {
|
if let Some(tool) = seat.tablet_seat().get_tool(&event.tool()) {
|
||||||
tool.button(
|
tool.button(
|
||||||
self,
|
self,
|
||||||
|
|
@ -2096,6 +2143,21 @@ impl State {
|
||||||
backend_id, seat, modifiers, handle, serial, keycode, key_state, time,
|
backend_id, seat, modifiers, handle, serial, keycode, key_state, time,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// A modifier-only binding fires on the modifier's release, but its press was already
|
||||||
|
// forwarded. Forward the release too.
|
||||||
|
if key_state == KeyState::Released
|
||||||
|
&& matches!(&result, FilterResult::Intercept(Some((_, binding))) if binding.key.is_none())
|
||||||
|
{
|
||||||
|
seat.get_keyboard().unwrap().input_forward(
|
||||||
|
self,
|
||||||
|
keycode,
|
||||||
|
key_state,
|
||||||
|
serial,
|
||||||
|
time,
|
||||||
|
previous_modifiers != *modifiers,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (matches!(result, FilterResult::Forward)
|
if (matches!(result, FilterResult::Forward)
|
||||||
&& !seat.get_keyboard().unwrap().is_grabbed()
|
&& !seat.get_keyboard().unwrap().is_grabbed()
|
||||||
&& !shortcuts_inhibited
|
&& !shortcuts_inhibited
|
||||||
|
|
@ -2291,17 +2353,15 @@ impl State {
|
||||||
|
|
||||||
let keyboard = seat.get_keyboard().unwrap();
|
let keyboard = seat.get_keyboard().unwrap();
|
||||||
let pointer = seat.get_pointer().unwrap();
|
let pointer = seat.get_pointer().unwrap();
|
||||||
// We're only interested in filtering keyboard grabs if we initiated them.
|
// Escape only cancels grabs the compositor started for its own interactions
|
||||||
// The easiest way to check that is to check the type of the grab.
|
// For keyboard: SwapWindowGrab
|
||||||
let keyboard_grabbed = keyboard.with_grab(|_serial, grab| {
|
// For pointer: MoveGrab, ResizeGrab, and MenuGrab
|
||||||
grab.is::<SwapWindowGrab>() || grab.is::<PopupKeyboardGrab<State>>()
|
let keyboard_grabbed =
|
||||||
|
keyboard.with_grab(|_serial, grab| grab.is::<SwapWindowGrab>()) == Some(true);
|
||||||
|
let pointer_grabbed = pointer.with_grab(|_serial, grab| {
|
||||||
|
grab.is::<MoveGrab>() || grab.is::<ResizeGrab>() || grab.is::<MenuGrab>()
|
||||||
}) == Some(true);
|
}) == Some(true);
|
||||||
// A virtual-keyboard key can arrive while the seat's pointer is grabbed by that
|
let is_grabbed = keyboard_grabbed || pointer_grabbed;
|
||||||
// same on-screen keyboard's own button press (the implicit grab from clicking an OSK
|
|
||||||
// key). That pointer grab must not capture the injected key, otherwise e.g.
|
|
||||||
// pressing esc on a virtual keyboard gets swallowed here
|
|
||||||
let from_vk = matches!(backend_id, InputBackendId::VirtualKeyboard);
|
|
||||||
let is_grabbed = keyboard_grabbed || (pointer.is_grabbed() && !from_vk);
|
|
||||||
|
|
||||||
let current_focus = keyboard.current_focus();
|
let current_focus = keyboard.current_focus();
|
||||||
//this should fall back to active output since there may not be a focused output
|
//this should fall back to active output since there may not be a focused output
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,7 @@ Project home page: https://github.com/pop-os/cosmic-comp
|
||||||
Options:
|
Options:
|
||||||
-h, --help Show this message
|
-h, --help Show this message
|
||||||
--no-xwayland Run without Xwayland
|
--no-xwayland Run without Xwayland
|
||||||
-v, --version Show the version of cosmic-comp"#
|
-V, --version Show the version of cosmic-comp"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use tracing::{debug, info, warn};
|
#[cfg(feature = "systemd")]
|
||||||
|
use tracing::warn;
|
||||||
|
use tracing::{debug, info};
|
||||||
|
#[cfg(feature = "systemd")]
|
||||||
use tracing_journald as journald;
|
use tracing_journald as journald;
|
||||||
use tracing_subscriber::{EnvFilter, filter::Directive, fmt, prelude::*};
|
use tracing_subscriber::{EnvFilter, filter::Directive, fmt, prelude::*};
|
||||||
|
|
||||||
|
|
@ -29,6 +32,7 @@ pub fn init_logger() -> Result<()> {
|
||||||
|
|
||||||
let fmt_layer = fmt::layer().compact();
|
let fmt_layer = fmt::layer().compact();
|
||||||
|
|
||||||
|
#[cfg(feature = "systemd")]
|
||||||
match journald::layer() {
|
match journald::layer() {
|
||||||
Ok(journald_layer) => tracing_subscriber::registry()
|
Ok(journald_layer) => tracing_subscriber::registry()
|
||||||
.with(fmt_layer)
|
.with(fmt_layer)
|
||||||
|
|
@ -43,6 +47,11 @@ pub fn init_logger() -> Result<()> {
|
||||||
warn!(?err, "Failed to init journald logging.");
|
warn!(?err, "Failed to init journald logging.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
#[cfg(not(feature = "systemd"))]
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(fmt_layer)
|
||||||
|
.with(filter)
|
||||||
|
.init();
|
||||||
log_panics::init();
|
log_panics::init();
|
||||||
|
|
||||||
info!("Version: {}", std::env!("CARGO_PKG_VERSION"));
|
info!("Version: {}", std::env!("CARGO_PKG_VERSION"));
|
||||||
|
|
|
||||||
|
|
@ -430,6 +430,10 @@ impl CosmicMapped {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_sticky(&self) -> bool {
|
||||||
|
self.active_window().is_sticky()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn pending_size(&self) -> Option<Size<i32, Logical>> {
|
pub fn pending_size(&self) -> Option<Size<i32, Logical>> {
|
||||||
match &self.element {
|
match &self.element {
|
||||||
CosmicMappedInternal::Stack(s) => s.pending_size(),
|
CosmicMappedInternal::Stack(s) => s.pending_size(),
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,18 @@ use crate::{
|
||||||
shell::{CosmicSurface, MinimizedWindow, Shell, Trigger, element::CosmicMapped},
|
shell::{CosmicSurface, MinimizedWindow, Shell, Trigger, element::CosmicMapped},
|
||||||
state::{Common, State},
|
state::{Common, State},
|
||||||
utils::prelude::*,
|
utils::prelude::*,
|
||||||
wayland::handlers::{xdg_shell::PopupGrabData, xwayland_keyboard_grab::XWaylandGrabSeatData},
|
wayland::{
|
||||||
|
handlers::{xdg_shell::PopupGrabData, xwayland_keyboard_grab::XWaylandGrabSeatData},
|
||||||
|
protocols::session_lock_layer::layer_show_on_lock,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::input::InputTime,
|
backend::input::InputTime,
|
||||||
desktop::{PopupUngrabStrategy, layer_map_for_output},
|
desktop::{
|
||||||
|
LayerSurface, PopupUngrabStrategy, WindowSurfaceType, find_popup_root_surface,
|
||||||
|
layer_map_for_output,
|
||||||
|
},
|
||||||
input::{Seat, pointer::MotionEvent},
|
input::{Seat, pointer::MotionEvent},
|
||||||
output::Output,
|
output::Output,
|
||||||
reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface},
|
reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface},
|
||||||
|
|
@ -249,8 +255,17 @@ impl Shell {
|
||||||
let workspace = if let Some(workspace) = workspace {
|
let workspace = if let Some(workspace) = workspace {
|
||||||
self.workspaces.space_for_handle_mut(&workspace.0).unwrap()
|
self.workspaces.space_for_handle_mut(&workspace.0).unwrap()
|
||||||
} else {
|
} else {
|
||||||
//should this be the active output or the focused output?
|
let output = match &target {
|
||||||
self.active_space_mut(&seat.focused_or_active_output())
|
FocusTarget::Window(mapped) => self
|
||||||
|
.workspaces
|
||||||
|
.sets
|
||||||
|
.iter()
|
||||||
|
.find(|(_, set)| set.sticky_layer.mapped().any(|m| m == mapped))
|
||||||
|
.map(|(output, _)| output.clone()),
|
||||||
|
FocusTarget::Fullscreen(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.active_space_mut(&output.unwrap_or_else(|| seat.active_output()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -627,19 +642,42 @@ fn focus_target_is_valid(
|
||||||
output: &Output,
|
output: &Output,
|
||||||
target: KeyboardFocusTarget,
|
target: KeyboardFocusTarget,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// If a session lock is active, only lock surfaces can be focused
|
// If a session lock is active, only lock surfaces and lock layers can be focused
|
||||||
if shell.session_lock.is_some() {
|
if shell.session_lock.is_some() {
|
||||||
|
if let KeyboardFocusTarget::LayerSurface(layer) = &target {
|
||||||
|
return layer_show_on_lock(layer.wl_surface());
|
||||||
|
} else if let KeyboardFocusTarget::Popup(popup) = &target
|
||||||
|
&& let Ok(root) = find_popup_root_surface(popup)
|
||||||
|
{
|
||||||
|
return layer_show_on_lock(&root);
|
||||||
|
}
|
||||||
return matches!(target, KeyboardFocusTarget::LockSurface(_));
|
return matches!(target, KeyboardFocusTarget::LockSurface(_));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If an exclusive layer shell surface exists (on any output), only exclusive
|
// If an exclusive layer shell surface exists (on any output), only exclusive
|
||||||
// shell surfaces can have focus, on the highest layer with exclusive surfaces.
|
// shell surfaces can have focus, on the highest layer with exclusive surfaces.
|
||||||
|
// Popups are judged by their root surface, so an exclusive surface can
|
||||||
|
// still open grabbing popups (menus, dropdowns, context menus).
|
||||||
if let Some(layer) = exclusive_layer_surface_layer(shell) {
|
if let Some(layer) = exclusive_layer_surface_layer(shell) {
|
||||||
return if let KeyboardFocusTarget::LayerSurface(layer_surface) = target {
|
let is_exclusive_on_layer = |layer_surface: &LayerSurface| {
|
||||||
let data = layer_surface.cached_state();
|
let data = layer_surface.cached_state();
|
||||||
(data.keyboard_interactivity, data.layer) == (KeyboardInteractivity::Exclusive, layer)
|
(data.keyboard_interactivity, data.layer) == (KeyboardInteractivity::Exclusive, layer)
|
||||||
} else {
|
};
|
||||||
false
|
return match target {
|
||||||
|
KeyboardFocusTarget::LayerSurface(layer_surface) => {
|
||||||
|
is_exclusive_on_layer(&layer_surface)
|
||||||
|
}
|
||||||
|
KeyboardFocusTarget::Popup(popup) => find_popup_root_surface(&popup)
|
||||||
|
.ok()
|
||||||
|
.and_then(|root| {
|
||||||
|
shell.outputs().find_map(|o| {
|
||||||
|
layer_map_for_output(o)
|
||||||
|
.layer_for_surface(&root, WindowSurfaceType::ALL)
|
||||||
|
.map(&is_exclusive_on_layer)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or(false),
|
||||||
|
_ => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ use crate::{
|
||||||
prelude::OutputExt,
|
prelude::OutputExt,
|
||||||
quirks::{WORKSPACE_OVERVIEW_NAMESPACE, workspace_overview_is_open},
|
quirks::{WORKSPACE_OVERVIEW_NAMESPACE, workspace_overview_is_open},
|
||||||
},
|
},
|
||||||
wayland::protocols::workspace::WorkspaceHandle,
|
wayland::protocols::{session_lock_layer::layer_show_on_lock, workspace::WorkspaceHandle},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum Stage<'a> {
|
pub enum Stage<'a> {
|
||||||
|
|
@ -77,6 +77,8 @@ fn render_input_order_internal<R: 'static>(
|
||||||
element_filter: ElementFilter,
|
element_filter: ElementFilter,
|
||||||
mut callback: impl FnMut(Stage) -> ControlFlow<Result<R, OutputNoMode>, ()>,
|
mut callback: impl FnMut(Stage) -> ControlFlow<Result<R, OutputNoMode>, ()>,
|
||||||
) -> ControlFlow<Result<R, OutputNoMode>, ()> {
|
) -> ControlFlow<Result<R, OutputNoMode>, ()> {
|
||||||
|
// NOTE: Keep in sync with other surface iteration functions
|
||||||
|
|
||||||
if shell
|
if shell
|
||||||
.zoom_state
|
.zoom_state
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -87,7 +89,92 @@ fn render_input_order_internal<R: 'static>(
|
||||||
|
|
||||||
// Session Lock
|
// Session Lock
|
||||||
if let Some(session_lock) = &shell.session_lock {
|
if let Some(session_lock) = &shell.session_lock {
|
||||||
return callback(Stage::SessionLock(session_lock.surfaces.get(output)));
|
for (layer, popup, location) in layer_popups(output, Layer::Overlay, element_filter) {
|
||||||
|
if !layer_show_on_lock(layer.wl_surface()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
callback(Stage::LayerPopup {
|
||||||
|
layer,
|
||||||
|
popup: &popup,
|
||||||
|
location,
|
||||||
|
workspace_idx: current.1,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
for (layer, location) in layer_surfaces(output, Layer::Overlay, element_filter) {
|
||||||
|
if !layer_show_on_lock(layer.wl_surface()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
callback(Stage::LayerSurface {
|
||||||
|
layer,
|
||||||
|
location,
|
||||||
|
workspace_idx: current.1,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
for (layer, popup, location) in layer_popups(output, Layer::Top, element_filter) {
|
||||||
|
if !layer_show_on_lock(layer.wl_surface()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
callback(Stage::LayerPopup {
|
||||||
|
layer,
|
||||||
|
popup: &popup,
|
||||||
|
location,
|
||||||
|
workspace_idx: current.1,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
for (layer, location) in layer_surfaces(output, Layer::Top, element_filter) {
|
||||||
|
if !layer_show_on_lock(layer.wl_surface()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
callback(Stage::LayerSurface {
|
||||||
|
layer,
|
||||||
|
location,
|
||||||
|
workspace_idx: current.1,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
callback(Stage::SessionLock(session_lock.surfaces.get(output)))?;
|
||||||
|
for (layer, popup, location) in layer_popups(output, Layer::Bottom, element_filter) {
|
||||||
|
if !layer_show_on_lock(layer.wl_surface()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
callback(Stage::LayerPopup {
|
||||||
|
layer,
|
||||||
|
popup: &popup,
|
||||||
|
location,
|
||||||
|
workspace_idx: current.1,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
for (layer, popup, location) in layer_popups(output, Layer::Background, element_filter) {
|
||||||
|
if !layer_show_on_lock(layer.wl_surface()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
callback(Stage::LayerPopup {
|
||||||
|
layer,
|
||||||
|
popup: &popup,
|
||||||
|
location,
|
||||||
|
workspace_idx: current.1,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
for (layer, location) in layer_surfaces(output, Layer::Bottom, element_filter) {
|
||||||
|
if !layer_show_on_lock(layer.wl_surface()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
callback(Stage::LayerSurface {
|
||||||
|
layer,
|
||||||
|
location,
|
||||||
|
workspace_idx: current.1,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
for (layer, location) in layer_surfaces(output, Layer::Background, element_filter) {
|
||||||
|
if !layer_show_on_lock(layer.wl_surface()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
callback(Stage::LayerSurface {
|
||||||
|
layer,
|
||||||
|
location,
|
||||||
|
workspace_idx: current.1,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
return ControlFlow::Continue(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overlay-level layer shell
|
// Overlay-level layer shell
|
||||||
|
|
|
||||||
|
|
@ -579,10 +579,12 @@ impl PointerGrab<State> for MenuGrab {
|
||||||
PointerTarget::motion(&element.iced, &self.seat, state, &new_event);
|
PointerTarget::motion(&element.iced, &self.seat, state, &new_event);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Keep the root menu hovered while a submenu is open
|
||||||
|
let keep_root = usize::from(elements.len() > 1);
|
||||||
elements
|
elements
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.filter(|element| element.pointer_entered)
|
.filter(|element| element.pointer_entered)
|
||||||
.skip(1)
|
.skip(keep_root)
|
||||||
.for_each(|element| {
|
.for_each(|element| {
|
||||||
PointerTarget::leave(
|
PointerTarget::leave(
|
||||||
&element.iced,
|
&element.iced,
|
||||||
|
|
@ -1020,10 +1022,11 @@ impl TabletToolGrab<State> for MenuGrab {
|
||||||
);
|
);
|
||||||
self.last_tablet_idx = Some(i);
|
self.last_tablet_idx = Some(i);
|
||||||
} else {
|
} else {
|
||||||
|
let keep_root = usize::from(elements.len() > 1);
|
||||||
elements
|
elements
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.filter(|element| element.tablet_entered.is_some())
|
.filter(|element| element.tablet_entered.is_some())
|
||||||
.skip(1)
|
.skip(keep_root)
|
||||||
.for_each(|element| {
|
.for_each(|element| {
|
||||||
TabletToolTarget::proximity_out(
|
TabletToolTarget::proximity_out(
|
||||||
&element.iced,
|
&element.iced,
|
||||||
|
|
|
||||||
|
|
@ -3271,12 +3271,34 @@ impl TilingLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (_, inner) = self.gaps();
|
||||||
let mut result = None;
|
let mut result = None;
|
||||||
|
let mut fork = None;
|
||||||
let mut lookup = Some(root.clone());
|
let mut lookup = Some(root.clone());
|
||||||
while let Some(node) = lookup {
|
while let Some(node) = lookup {
|
||||||
let data = tree.get(&node).unwrap().data();
|
let data = tree.get(&node).unwrap().data();
|
||||||
if data.geometry().contains(location) {
|
if data.geometry().contains(location) {
|
||||||
result = Some(node.clone());
|
result = Some(node.clone());
|
||||||
|
if let Data::Group {
|
||||||
|
orientation,
|
||||||
|
sizes,
|
||||||
|
last_geometry,
|
||||||
|
..
|
||||||
|
} = data
|
||||||
|
{
|
||||||
|
let (pos, mut split) = match orientation {
|
||||||
|
Orientation::Vertical => (location.x, last_geometry.loc.x),
|
||||||
|
Orientation::Horizontal => (location.y, last_geometry.loc.y),
|
||||||
|
};
|
||||||
|
for (idx, size) in
|
||||||
|
sizes.iter().take(sizes.len().saturating_sub(1)).enumerate()
|
||||||
|
{
|
||||||
|
split += size;
|
||||||
|
if (pos - split).abs() <= inner / 2 {
|
||||||
|
fork = Some((node.clone(), idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lookup = None;
|
lookup = None;
|
||||||
|
|
@ -3323,25 +3345,16 @@ impl TilingLayout {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Some((
|
Some((_, Data::Group { .. })) => {
|
||||||
id,
|
let (id, idx) = fork?;
|
||||||
Data::Group {
|
let Data::Group {
|
||||||
orientation,
|
orientation,
|
||||||
last_geometry,
|
last_geometry,
|
||||||
..
|
..
|
||||||
},
|
} = tree.get(&id).unwrap().data().clone()
|
||||||
)) => {
|
else {
|
||||||
let idx = tree
|
unreachable!()
|
||||||
.children(&id)
|
};
|
||||||
.unwrap()
|
|
||||||
.position(|node| {
|
|
||||||
let data = node.data();
|
|
||||||
match orientation {
|
|
||||||
Orientation::Vertical => location.x < data.geometry().loc.x,
|
|
||||||
Orientation::Horizontal => location.y < data.geometry().loc.y,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.and_then(|x| x.checked_sub(1))?;
|
|
||||||
Some((
|
Some((
|
||||||
ResizeForkTarget {
|
ResizeForkTarget {
|
||||||
node: id.clone(),
|
node: id.clone(),
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,10 @@ use crate::{
|
||||||
utils,
|
utils,
|
||||||
wayland::{
|
wayland::{
|
||||||
handlers::data_device::{self, get_dnd_icon},
|
handlers::data_device::{self, get_dnd_icon},
|
||||||
protocols::workspace::{State as WState, WorkspaceCapabilities},
|
protocols::{
|
||||||
|
session_lock_layer::layer_show_on_lock,
|
||||||
|
workspace::{State as WState, WorkspaceCapabilities},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use cosmic_comp_config::{
|
use cosmic_comp_config::{
|
||||||
|
|
@ -513,6 +516,12 @@ impl WorkspaceSet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn refresh_focus_stacks(&mut self) {
|
||||||
|
for workspace in &mut self.workspaces {
|
||||||
|
workspace.refresh_focus_stack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn activate(
|
fn activate(
|
||||||
&mut self,
|
&mut self,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
|
|
@ -617,6 +626,7 @@ impl WorkspaceSet {
|
||||||
self.workspaces[self.active].refresh();
|
self.workspaces[self.active].refresh();
|
||||||
}
|
}
|
||||||
self.sticky_layer.refresh();
|
self.sticky_layer.refresh();
|
||||||
|
self.refresh_focus_stacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_empty_workspace(&mut self, state: &mut WorkspaceUpdateGuard<State>) {
|
fn add_empty_workspace(&mut self, state: &mut WorkspaceUpdateGuard<State>) {
|
||||||
|
|
@ -2046,12 +2056,27 @@ impl Shell {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn visible_output_for_surface(&self, surface: &WlSurface) -> Option<&Output> {
|
pub fn visible_output_for_surface(&self, surface: &WlSurface) -> Option<&Output> {
|
||||||
|
// NOTE: Keep in sync with surface iteration in `render_input_order_internal`
|
||||||
|
|
||||||
if let Some(session_lock) = &self.session_lock {
|
if let Some(session_lock) = &self.session_lock {
|
||||||
return session_lock
|
if let Some((output, _)) = session_lock
|
||||||
.surfaces
|
.surfaces
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, v)| v.wl_surface() == surface)
|
.find(|(_, v)| v.wl_surface() == surface)
|
||||||
.map(|(k, _)| k);
|
{
|
||||||
|
return Some(output);
|
||||||
|
}
|
||||||
|
for o in self.outputs() {
|
||||||
|
let map = layer_map_for_output(o);
|
||||||
|
if let Some(layer_surface) = map.layer_for_surface(surface, WindowSurfaceType::ALL)
|
||||||
|
{
|
||||||
|
if layer_show_on_lock(layer_surface.wl_surface()) {
|
||||||
|
return Some(o);
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.outputs()
|
self.outputs()
|
||||||
|
|
|
||||||
|
|
@ -526,10 +526,16 @@ impl Workspace {
|
||||||
.mapped()
|
.mapped()
|
||||||
.chain(self.tiling_layer.mapped().map(|(w, _)| w))
|
.chain(self.tiling_layer.mapped().map(|(w, _)| w))
|
||||||
.chain(move_mapped.iter())
|
.chain(move_mapped.iter())
|
||||||
|
.chain(
|
||||||
|
self.minimized_windows
|
||||||
|
.iter()
|
||||||
|
.flat_map(MinimizedWindow::mapped),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
stack.retain(|w| match w {
|
stack.retain(|w| match w {
|
||||||
FocusTarget::Fullscreen(s) => fullscreen_surfaces.contains(&s),
|
FocusTarget::Fullscreen(s) => fullscreen_surfaces.contains(&s),
|
||||||
FocusTarget::Window(w) => mapped().any(|m| w == m),
|
// Sticky windows live in the `WorkspaceSet` but are shown on whichever workspace is current
|
||||||
|
FocusTarget::Window(w) => w.is_sticky() || mapped().any(|m| w == m),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1767,13 +1773,17 @@ impl Workspace {
|
||||||
|
|
||||||
self.floating_layer.render(
|
self.floating_layer.render(
|
||||||
renderer,
|
renderer,
|
||||||
focused.as_ref().and_then(|target| {
|
render_focus
|
||||||
if let FocusTarget::Window(mapped) = target {
|
.then(|| {
|
||||||
Some(mapped)
|
focused.as_ref().and_then(|target| {
|
||||||
} else {
|
if let FocusTarget::Window(mapped) = target {
|
||||||
None
|
Some(mapped)
|
||||||
}
|
} else {
|
||||||
}),
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.flatten(),
|
||||||
resize_indicator.clone(),
|
resize_indicator.clone(),
|
||||||
indicator_thickness,
|
indicator_thickness,
|
||||||
alpha,
|
alpha,
|
||||||
|
|
|
||||||
14
src/state.rs
14
src/state.rs
|
|
@ -23,6 +23,7 @@ use crate::{
|
||||||
output_configuration::OutputConfigurationState,
|
output_configuration::OutputConfigurationState,
|
||||||
output_power::OutputPowerState,
|
output_power::OutputPowerState,
|
||||||
overlap_notify::OverlapNotifyState,
|
overlap_notify::OverlapNotifyState,
|
||||||
|
session_lock_layer::SessionLockLayerState,
|
||||||
toplevel_info::ToplevelInfoState,
|
toplevel_info::ToplevelInfoState,
|
||||||
toplevel_management::{ManagementCapabilities, ToplevelManagementState},
|
toplevel_management::{ManagementCapabilities, ToplevelManagementState},
|
||||||
workspace::{WorkspaceState, WorkspaceUpdateGuard},
|
workspace::{WorkspaceState, WorkspaceUpdateGuard},
|
||||||
|
|
@ -320,6 +321,7 @@ pub struct Common {
|
||||||
pub xwayland_state: Option<XWaylandState>,
|
pub xwayland_state: Option<XWaylandState>,
|
||||||
pub xwayland_shell_state: XWaylandShellState,
|
pub xwayland_shell_state: XWaylandShellState,
|
||||||
pub pointer_focus_state: Option<PointerFocusState>,
|
pub pointer_focus_state: Option<PointerFocusState>,
|
||||||
|
pub session_lock_layer_state: SessionLockLayerState,
|
||||||
|
|
||||||
#[cfg(feature = "logind")]
|
#[cfg(feature = "logind")]
|
||||||
pub inhibit_lid_fd: Option<OwnedFd>,
|
pub inhibit_lid_fd: Option<OwnedFd>,
|
||||||
|
|
@ -665,7 +667,7 @@ impl State {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let clock = Clock::new();
|
let clock = Clock::new();
|
||||||
let config = Config::load(&handle);
|
let config = Config::load(&handle, kiosk_command.is_some());
|
||||||
let compositor_state = CompositorState::new::<Self>(dh);
|
let compositor_state = CompositorState::new::<Self>(dh);
|
||||||
let corner_radius_state = CornerRadiusState::new::<Self>(dh);
|
let corner_radius_state = CornerRadiusState::new::<Self>(dh);
|
||||||
let data_device_state = DataDeviceState::new::<Self>(dh);
|
let data_device_state = DataDeviceState::new::<Self>(dh);
|
||||||
|
|
@ -763,6 +765,9 @@ impl State {
|
||||||
|
|
||||||
let dbus_state = DBusState::init(&handle);
|
let dbus_state = DBusState::init(&handle);
|
||||||
|
|
||||||
|
let session_lock_layer_state =
|
||||||
|
SessionLockLayerState::new::<State, _>(dh, client_not_sandboxed);
|
||||||
|
|
||||||
State {
|
State {
|
||||||
common: Common {
|
common: Common {
|
||||||
config,
|
config,
|
||||||
|
|
@ -830,6 +835,7 @@ impl State {
|
||||||
pointer_focus_state: None,
|
pointer_focus_state: None,
|
||||||
dbus_state,
|
dbus_state,
|
||||||
keyboard_layout_state,
|
keyboard_layout_state,
|
||||||
|
session_lock_layer_state,
|
||||||
|
|
||||||
#[cfg(feature = "logind")]
|
#[cfg(feature = "logind")]
|
||||||
inhibit_lid_fd: None,
|
inhibit_lid_fd: None,
|
||||||
|
|
@ -968,6 +974,8 @@ impl Common {
|
||||||
output: &Output,
|
output: &Output,
|
||||||
render_element_states: &RenderElementStates,
|
render_element_states: &RenderElementStates,
|
||||||
) {
|
) {
|
||||||
|
// NOTE: Keep in sync with surface iteration in `render_input_order_internal`
|
||||||
|
|
||||||
let shell = self.shell.read();
|
let shell = self.shell.read();
|
||||||
let processor = |namespace: Option<usize>| {
|
let processor = |namespace: Option<usize>| {
|
||||||
move |surface: &WlSurface, states: &SurfaceData| {
|
move |surface: &WlSurface, states: &SurfaceData| {
|
||||||
|
|
@ -1074,6 +1082,8 @@ impl Common {
|
||||||
render_element_states: &RenderElementStates,
|
render_element_states: &RenderElementStates,
|
||||||
mut dmabuf_feedback: impl FnMut(DrmNode) -> Option<SurfaceDmabufFeedback>,
|
mut dmabuf_feedback: impl FnMut(DrmNode) -> Option<SurfaceDmabufFeedback>,
|
||||||
) {
|
) {
|
||||||
|
// NOTE: Keep in sync with surface iteration in `render_input_order_internal`
|
||||||
|
|
||||||
let shell = self.shell.read();
|
let shell = self.shell.read();
|
||||||
|
|
||||||
if let Some(session_lock) = shell.session_lock.as_ref()
|
if let Some(session_lock) = shell.session_lock.as_ref()
|
||||||
|
|
@ -1287,6 +1297,8 @@ impl Common {
|
||||||
|
|
||||||
#[profiling::function]
|
#[profiling::function]
|
||||||
pub fn send_frames(&self, output: &Output, sequence: Option<usize>) {
|
pub fn send_frames(&self, output: &Output, sequence: Option<usize>) {
|
||||||
|
// NOTE: Keep in sync with surface iteration in `render_input_order_internal`
|
||||||
|
|
||||||
let time = self.clock.now();
|
let time = self.clock.now();
|
||||||
let should_send = |surface: &WlSurface, states: &SurfaceData| {
|
let should_send = |surface: &WlSurface, states: &SurfaceData| {
|
||||||
// Do the standard primary scanout output check. For pointer surfaces it deduplicates
|
// Do the standard primary scanout output check. For pointer surfaces it deduplicates
|
||||||
|
|
|
||||||
|
|
@ -519,6 +519,7 @@ impl<P: Program + Send + 'static> TabletToolTarget<crate::state::State> for Iced
|
||||||
_tool_descriptor: &TabletToolDescriptor,
|
_tool_descriptor: &TabletToolDescriptor,
|
||||||
) {
|
) {
|
||||||
let mut internal = self.0.lock().unwrap();
|
let mut internal = self.0.lock().unwrap();
|
||||||
|
internal.cursor_pos = None;
|
||||||
internal
|
internal
|
||||||
.state
|
.state
|
||||||
.queue_event(Event::Mouse(MouseEvent::CursorLeft));
|
.queue_event(Event::Mouse(MouseEvent::CursorLeft));
|
||||||
|
|
@ -728,6 +729,7 @@ impl<P: Program + Send + 'static> PointerTarget<crate::state::State> for IcedEle
|
||||||
_time: InputTime,
|
_time: InputTime,
|
||||||
) {
|
) {
|
||||||
let mut internal = self.0.lock().unwrap();
|
let mut internal = self.0.lock().unwrap();
|
||||||
|
internal.cursor_pos = None;
|
||||||
internal
|
internal
|
||||||
.state
|
.state
|
||||||
.queue_event(Event::Mouse(MouseEvent::CursorLeft));
|
.queue_event(Event::Mouse(MouseEvent::CursorLeft));
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use std::{borrow::Borrow, collections::HashMap, sync::Mutex};
|
use std::{borrow::Borrow, sync::Mutex};
|
||||||
|
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::{
|
backend::{
|
||||||
|
|
@ -452,23 +454,28 @@ fn constraints_for_renderer(
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|device| device.try_get_render_node().ok().flatten())
|
.and_then(|device| device.try_get_render_node().ok().flatten())
|
||||||
{
|
{
|
||||||
constraints.dma = Some(DmabufConstraints {
|
let mut formats = renderer
|
||||||
node,
|
.egl_context()
|
||||||
formats: renderer
|
.dmabuf_render_formats()
|
||||||
.egl_context()
|
.iter()
|
||||||
.dmabuf_render_formats()
|
.fold(
|
||||||
.iter()
|
IndexMap::<Fourcc, Vec<Modifier>>::new(),
|
||||||
.fold(
|
|mut map, format| {
|
||||||
HashMap::<Fourcc, Vec<Modifier>>::new(),
|
map.entry(format.code).or_default().push(format.modifier);
|
||||||
|mut map, format| {
|
map
|
||||||
map.entry(format.code).or_default().push(format.modifier);
|
},
|
||||||
map
|
)
|
||||||
},
|
.into_iter()
|
||||||
)
|
.collect::<Vec<_>>();
|
||||||
.into_iter()
|
formats.sort_by_key(|(code, _)| !is_preferred_capture_format(*code));
|
||||||
.collect::<Vec<_>>(),
|
constraints.dma = Some(DmabufConstraints { node, formats });
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constraints
|
constraints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Formats most screencast consumers handle natively, offered first. WebRTC, for
|
||||||
|
/// example, swaps RGBA frames to BGRA on the CPU for every captured frame.
|
||||||
|
fn is_preferred_capture_format(code: Fourcc) -> bool {
|
||||||
|
matches!(code, Fourcc::Argb8888 | Fourcc::Xrgb8888)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,11 @@ pub fn set_all_surfaces_dpms_on(state: &mut State) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if changed {
|
if changed {
|
||||||
|
// Powering an output on because of input is activity. Without this the idle
|
||||||
|
// notifier stays idled, the idle daemon never sees `resumed`, and nothing
|
||||||
|
// powers the output off again until real input arrives.
|
||||||
|
let seat = state.common.shell.read().seats.last_active().clone();
|
||||||
|
state.common.idle_notifier_state.notify_activity(&seat);
|
||||||
OutputPowerState::refresh(state);
|
OutputPowerState::refresh(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,7 @@ impl State {
|
||||||
{
|
{
|
||||||
for mapped in current_workspace
|
for mapped in current_workspace
|
||||||
.mapped()
|
.mapped()
|
||||||
.filter(|m| m.maximized_state.lock().unwrap().is_some())
|
.filter(|m| m.maximized_state.lock().unwrap().is_some() && *m != &element)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ pub mod keyboard_layout;
|
||||||
pub mod output_configuration;
|
pub mod output_configuration;
|
||||||
pub mod output_power;
|
pub mod output_power;
|
||||||
pub mod overlap_notify;
|
pub mod overlap_notify;
|
||||||
|
pub mod session_lock_layer;
|
||||||
pub mod toplevel_info;
|
pub mod toplevel_info;
|
||||||
pub mod toplevel_management;
|
pub mod toplevel_management;
|
||||||
pub mod workspace;
|
pub mod workspace;
|
||||||
|
|
|
||||||
136
src/wayland/protocols/session_lock_layer.rs
Normal file
136
src/wayland/protocols/session_lock_layer.rs
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
use cosmic_protocols::session_lock_layer::v1::server::cosmic_session_lock_layer_manager_v1;
|
||||||
|
use smithay::{
|
||||||
|
reexports::wayland_server::{
|
||||||
|
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, backend::GlobalId,
|
||||||
|
protocol::wl_surface,
|
||||||
|
},
|
||||||
|
wayland::{
|
||||||
|
Dispatch2, GlobalDispatch2,
|
||||||
|
compositor::{Cacheable, with_states},
|
||||||
|
shell::wlr_layer::WlrLayerShellHandler,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SessionLockLayerData;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SessionLockLayerState {
|
||||||
|
global: GlobalId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionLockLayerState {
|
||||||
|
pub fn new<D, F>(dh: &DisplayHandle, client_filter: F) -> Self
|
||||||
|
where
|
||||||
|
F: for<'a> Fn(&'a Client) -> bool + Clone + Send + Sync + 'static,
|
||||||
|
D: GlobalDispatch<
|
||||||
|
cosmic_session_lock_layer_manager_v1::CosmicSessionLockLayerManagerV1,
|
||||||
|
SessionLockLayerGlobalData,
|
||||||
|
> + 'static,
|
||||||
|
{
|
||||||
|
let global = dh
|
||||||
|
.create_global::<D, cosmic_session_lock_layer_manager_v1::CosmicSessionLockLayerManagerV1, _>(
|
||||||
|
1,
|
||||||
|
SessionLockLayerGlobalData {
|
||||||
|
filter: Box::new(client_filter.clone()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Self { global }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn global_id(&self) -> GlobalId {
|
||||||
|
self.global.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub struct SessionLockLayerGlobalData {
|
||||||
|
filter: Box<dyn for<'a> Fn(&'a Client) -> bool + Send + Sync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> GlobalDispatch2<cosmic_session_lock_layer_manager_v1::CosmicSessionLockLayerManagerV1, D>
|
||||||
|
for SessionLockLayerGlobalData
|
||||||
|
where
|
||||||
|
D: Dispatch<
|
||||||
|
cosmic_session_lock_layer_manager_v1::CosmicSessionLockLayerManagerV1,
|
||||||
|
SessionLockLayerData,
|
||||||
|
>,
|
||||||
|
{
|
||||||
|
fn bind(
|
||||||
|
&self,
|
||||||
|
_state: &mut D,
|
||||||
|
_handle: &DisplayHandle,
|
||||||
|
_client: &Client,
|
||||||
|
resource: New<cosmic_session_lock_layer_manager_v1::CosmicSessionLockLayerManagerV1>,
|
||||||
|
data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
data_init.init(resource, SessionLockLayerData);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_view(&self, client: &Client) -> bool {
|
||||||
|
(self.filter)(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default)]
|
||||||
|
struct SessionLockLayerSurfaceData {
|
||||||
|
show_on_lock: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cacheable for SessionLockLayerSurfaceData {
|
||||||
|
fn commit(&mut self, _dh: &DisplayHandle) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) {
|
||||||
|
*into = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn layer_show_on_lock(wl_surface: &wl_surface::WlSurface) -> bool {
|
||||||
|
with_states(wl_surface, |states| {
|
||||||
|
if states.cached_state.has::<SessionLockLayerSurfaceData>() {
|
||||||
|
let mut state = states.cached_state.get::<SessionLockLayerSurfaceData>();
|
||||||
|
state.current().show_on_lock
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> Dispatch2<cosmic_session_lock_layer_manager_v1::CosmicSessionLockLayerManagerV1, D>
|
||||||
|
for SessionLockLayerData
|
||||||
|
where
|
||||||
|
D: WlrLayerShellHandler,
|
||||||
|
{
|
||||||
|
fn request(
|
||||||
|
&self,
|
||||||
|
state: &mut D,
|
||||||
|
_: &Client,
|
||||||
|
_: &cosmic_session_lock_layer_manager_v1::CosmicSessionLockLayerManagerV1,
|
||||||
|
request: cosmic_session_lock_layer_manager_v1::Request,
|
||||||
|
_: &DisplayHandle,
|
||||||
|
_: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
let (layer, value) = match request {
|
||||||
|
cosmic_session_lock_layer_manager_v1::Request::SetShowOnLock { layer } => (layer, true),
|
||||||
|
cosmic_session_lock_layer_manager_v1::Request::UnsetShowOnLock { layer } => {
|
||||||
|
(layer, false)
|
||||||
|
}
|
||||||
|
cosmic_session_lock_layer_manager_v1::Request::Destroy => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(layer) = state
|
||||||
|
.shell_state()
|
||||||
|
.layer_surfaces()
|
||||||
|
.find(|surface| surface.shell_surface() == &layer)
|
||||||
|
{
|
||||||
|
with_states(layer.wl_surface(), |states| {
|
||||||
|
let mut state = states.cached_state.get::<SessionLockLayerSurfaceData>();
|
||||||
|
state.pending().show_on_lock = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue