Compare commits

...

33 commits

Author SHA1 Message Date
2f3a82beb4 chore: fige Cargo.lock (epoch-1.10.0, [patch] locaux)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:02:21 +02:00
11f60acc94 merge: epoch-1.10.0 d'upstream
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:00:05 +02:00
mikairyuu
41497b42d9 backend: dmabuf: ensure primary render node is preffered 2026-10-05 21:13:31 +02:00
Anthony T. Lannutti
3d55cba06c feat: make tracing-journald dependency optional
Fixes #2466
2026-10-01 19:04:53 +02:00
Niklas Herder
895ce91543 cursor: let the hide reason tighten, and clear timer state when suppressed
The hidden.is_some() guard dropped a hide-reason upgrade: an Idle-hidden
cursor that received a touch stayed Idle, so a later click or scroll
revealed it where the spec says a touch-hidden cursor waits for real
movement. Reachable on the default config — fullscreen video, cursor
idle-hidden after 3s, tap the screen, nudge the scroll wheel. The guard
itself has to stay, or every keystroke with while_typing on reschedules a
render on every output; so it now returns early only when the reason is
unchanged or already Touch.

hide_cursor also returned before clearing idle_timer and last_armed when
the pointer was grabbed, leaving both stale while on_idle_timer's
neighbouring Drop arm cleared them carefully. Clearing in hide_cursor
before the grab check fixes both call sites at once — hide_cursor_now
removes the source itself, and the Hide arm returns TimeoutAction::Drop —
rather than restating the cleanup in one match arm.

Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-10-01 16:55:18 +02:00
Niklas Herder
115cdac039 cursor: measure the idle-hide deadline from the last pointer event
refresh_idle_timer armed for the full timeout measured from now, which is
right only for notify_cursor_activity — it stamps activity immediately
before arming. The other callers deliberately do not stamp, so with
idle_timeout: Some(10) a user typing continuously never saw the cursor
hide: every key press and release pushed the deadline out by a fresh ten
seconds. That inverts the feature and regresses the shipped
cursor_hide_timeout behaviour.

resolve's RearmAfter cannot recover it — it only corrects a timer that
fires too early. So the delay is now shortest_timeout minus the time
already elapsed, extracted as CursorHideConfig::arm_delay and table
tested; the arithmetic handed to Timer::from_duration was the one part of
this path with no coverage. A zero delay is fine: the timer fires on the
next loop iteration and resolve decides.

Also rename fullscreen_focused to fullscreen_on_active_output, which is
what seat.active_output() actually reports, and log an arming failure
rather than silently disabling cursor hiding.

Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-10-01 16:55:18 +02:00
Niklas Herder
7596cf7f2f config: deserialize cursor_hide with defaults for missing fields
Serde ignores unknown fields but errors on missing ones, so adding a
fifth trigger later would make every existing cursor_hide file fail to
deserialize. get_entry would then substitute the struct default and
Config::load swallows the error — its warn! loop is behind
cfg!(debug_assertions) — silently resetting everyone's settings on
upgrade. That is the exact failure this key was grouped to avoid.

It also has a present-day effect: the migration shim cannot tell "absent"
from "present but unparseable", so a partial hand-written cursor_hide was
treated as absent and overwritten — and hand-editing is what the shim's
audience does.

Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-10-01 16:55:18 +02:00
Niklas Herder
295eaa8f63 cursor: hide while typing and after touch input
Typing hides on a real keystroke only — a bare modifier press is excluded
so Super+drag keeps its cursor. Touch hides until the pointer next moves,
which the reveal rule enforces via HideReason::Touch.

Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-10-01 16:55:18 +02:00
Niklas Herder
569cfa6442 cursor: re-evaluate the idle timer in context, add fullscreen hiding
The timer now decides on fire rather than at arm time, so a shorter
fullscreen timeout applies without hooking any of the six fullscreen
transition sites in shell/mod.rs. Arming still touches no shell lock,
which the PointerMotion call site requires.

hidden becomes Option<HideReason> so the reveal rule can depend on why
the cursor went away.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-10-01 16:55:18 +02:00
Niklas Herder
4e72d2ce98 config: migrate cursor_hide_timeout to the grouped cursor_hide key
Reads the legacy key once at startup when the new one is absent, so the
handful of people who set it by hand from the cosmic-settings#603 thread
do not silently lose auto-hide. The old file is left on disk so a
downgrade still finds it.

Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-10-01 16:55:18 +02:00
Niklas Herder
6e0aea0eb2 config: group cursor hiding into CursorHideConfig
Replaces the flat cursor_hide_timeout key with a grouped cursor_hide key
carrying the idle, fullscreen-idle, typing and touch triggers, plus the
pure decision function the compositor's idle timer evaluates on fire.

The fullscreen timeout shortens the global one rather than replacing it,
so an unset fullscreen timeout cannot suppress an enabled global one.

Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-10-01 16:55:18 +02:00
Frederic Laing
b975370968 debian: add just to Build-Depends 2026-10-01 15:14:34 +02:00
Frederic Laing
af025856c7 build: migrate from make to just 2026-10-01 15:14:34 +02:00
Hojjat
d8e4683be8 fix(ci): use the correct toolchain version 2026-10-01 12:12:49 +02:00
Hojjat
ed7814e51e fix: set keyboard focus on touch target (similar to tablet/pointer) 2026-10-01 11:40:53 +02:00
Netflate
467d1fbc7f image-copy: Prioritize ARGB/XRGB dmabuf formats
Co-authored-by: back-design <271796486+back-design@users.noreply.github.com>
2026-10-01 11:33:32 +02:00
Netflate
e7b65b1fc8 image-copy: Fix non-deterministic dmabuf format order 2026-10-01 11:33:32 +02:00
Jeremy Soller
4d854ecb94 COSMIC policy update
Generated by cosmic-epoch scripts/policy-update.sh. Uses `policy_nobuild` branch to avoid package builds as it contains no code changes.
2026-09-30 09:55:29 -06:00
Hojjat
9f746ea3ff fix: the logic for resize direction in tiling mode 2026-09-25 17:01:54 +02:00
30d8868cca chore: fige Cargo.lock (epoch-1.9.0, [patch] locaux)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 21:03:54 +02:00
51c6cfedf6 merge: epoch-1.9.0 d'upstream
# Conflicts:
#	Cargo.lock
2026-09-24 20:48:00 +02:00
dev-goalz
e5010c23c9 fix: don't wake displays on input device removal, count input wakes as activity
A sleeping wireless keyboard relit blanked displays and idle never re-armed.

Written with assistance from Claude Opus 5.0. Reviewed, built and tested by the
author, who understands the changes in full.
2026-09-24 15:56:32 +02:00
Hojjat
f60d2c8436 fix: remove the hovered state from menu item on pointer leave 2026-09-24 15:47:37 +02:00
Hojjat
58837e27b8 fix: send pointer leave to root menu if no sub menu is open 2026-09-24 15:47:37 +02:00
Hojjat
6645edff64 fix: clear sticky windows, or floating windows focus hint if focus moves 2026-09-24 15:47:22 +02:00
Ashley Wulber
0fbd4574ef fix: skip loading shortcuts for kiosk mode 2026-09-22 17:12:28 -06:00
Hojjat
d36480c1b5 fix: only unmaximize on activation if maximized != target 2026-09-22 16:55:54 -06:00
Hojjat
50c170c355 fix: only cancel grabs the compositor started for its own interactions 2026-09-22 16:10:23 -04:00
Hojjat
26daf75d93 fix: forward release for modifier-only bindings 2026-09-22 14:22:06 +02:00
Flori Ava Star Weber
cd881a4df2 cli: change incorrect text -v to -V 2026-09-21 13:17:44 +02:00
Hojjat
651f701f84 shell/focus: Don't dismiss grabbing popups of exclusive layer surfaces 2026-09-16 11:04:07 -06:00
Ian Douglas Scott
a7349b72e3 Add cosmic-session-lock-layer-v1 protocol
https://github.com/pop-os/cosmic-protocols/pull/88
2026-09-11 14:38:07 -06:00
Ian Douglas Scott
f9e02f4321 Add comments to functions that iterate over surfaces
These should be kept in sync with `render_input_order_internal`.
2026-09-11 14:38:07 -06:00
32 changed files with 1139 additions and 298 deletions

View file

@ -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).

View file

@ -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
View 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
View file

@ -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]]

View file

@ -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"]

View file

@ -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
View 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
View 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

View file

@ -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
View file

@ -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
View file

@ -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
View 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}}

View file

@ -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()

View file

@ -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));
}
}
}

View file

@ -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<_> =

View file

@ -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

View file

@ -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"#
); );
} }

View file

@ -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"));

View file

@ -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(),

View file

@ -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,
}; };
} }

View file

@ -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

View file

@ -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,

View file

@ -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(),

View file

@ -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()

View file

@ -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,

View file

@ -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

View file

@ -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));

View file

@ -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)
}

View file

@ -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);
} }
} }

View file

@ -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()

View file

@ -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;

View 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;
});
}
}
}