diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e6ca28bc..63abe526 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,8 +1,10 @@ -- [ ] I have disclosed use of any AI generated code in my commit messages. - - 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. +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. +--- + +- 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). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc455356..e9277414 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,18 +13,12 @@ jobs: - name: Checkout sources uses: actions/checkout@v6 - name: Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 with: components: rustfmt - - name: Cargo cache - uses: actions/cache@v5 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} + rustflags: "" - name: Format - run: cargo +stable fmt --all -- --check + run: cargo fmt --all -- --check clippy-check: runs-on: ubuntu-latest @@ -32,20 +26,14 @@ jobs: - name: Checkout sources uses: actions/checkout@v6 - name: Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 with: components: clippy - - name: Cargo cache - uses: actions/cache@v5 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} + rustflags: "" - 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 - name: Clippy - run: cargo +stable clippy --all-features -- -D warnings + run: cargo clippy --all-features -- -D warnings check-features: runs-on: ubuntu-latest @@ -53,20 +41,15 @@ jobs: - name: Checkout sources uses: actions/checkout@v6 - name: Rust toolchain - uses: dtolnay/rust-toolchain@stable - - name: Cargo cache - uses: actions/cache@v5 + uses: actions-rust-lang/setup-rust-toolchain@v1 with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} + rustflags: "" - 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: cargo +stable check --no-default-features - - run: cargo +stable check --features debug - - run: cargo +stable check --features profile-with-tracy - - run: cargo +stable check --features profile-with-tracy-gpu + - run: cargo check --no-default-features + - run: cargo check --features debug + - run: cargo check --features profile-with-tracy + - run: cargo check --features profile-with-tracy-gpu test: runs-on: ubuntu-latest @@ -74,21 +57,11 @@ jobs: - name: Checkout sources uses: actions/checkout@v6 - name: Rust toolchain - uses: dtolnay/rust-toolchain@stable - - name: Cargo cache - uses: actions/cache@v5 + uses: actions-rust-lang/setup-rust-toolchain@v1 with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} + rustflags: "" - 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 - - name: Build cache - uses: actions/cache@v5 - with: - path: target - key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.toml') }} - name: Build and install Libseat run: | wget https://git.sr.ht/~kennylevinsen/seatd/archive/0.9.1.tar.gz -O libseat-source.tar.gz diff --git a/.github/workflows/require-checklist.yml b/.github/workflows/require-checklist.yml new file mode 100644 index 00000000..fd4ca8c6 --- /dev/null +++ b/.github/workflows/require-checklist.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index a53353a7..715836fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,7 +21,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "accesskit" 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 = [ "uuid", ] @@ -29,7 +29,7 @@ dependencies = [ [[package]] name = "accesskit_consumer" 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 = [ "accesskit", "hashbrown 0.16.1", @@ -38,7 +38,7 @@ dependencies = [ [[package]] name = "accesskit_macos" 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 = [ "accesskit", "accesskit_consumer", @@ -51,7 +51,7 @@ dependencies = [ [[package]] name = "accesskit_windows" 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 = [ "accesskit", "accesskit_consumer", @@ -64,7 +64,7 @@ dependencies = [ [[package]] name = "accesskit_winit" 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 = [ "accesskit", "accesskit_macos", @@ -1012,7 +1012,7 @@ dependencies = [ [[package]] name = "cosmic-randr-shell" -version = "1.3.0" +version = "1.10.0" dependencies = [ "kdl", "slotmap", @@ -1021,7 +1021,7 @@ dependencies = [ [[package]] name = "cosmic-settings-config" -version = "0.1.0" +version = "1.10.0" dependencies = [ "cosmic-config", "ron 0.11.0", @@ -1040,7 +1040,7 @@ dependencies = [ [[package]] name = "cosmic-settings-daemon-config" -version = "0.1.0" +version = "1.10.0" dependencies = [ "cosmic-config", "cosmic-theme", @@ -1564,7 +1564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1963,8 +1963,8 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link 0.2.1", - "windows-result 0.4.1", + "windows-link 0.1.3", + "windows-result 0.3.4", ] [[package]] @@ -2329,7 +2329,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -2833,7 +2833,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3072,6 +3072,7 @@ dependencies = [ "log", "palette", "phf 0.13.1", + "roxmltree 0.20.0", "rust-embed", "serde", "slotmap", @@ -3135,9 +3136,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.23" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" dependencies = [ "bitflags 2.13.2", "libc", @@ -4810,7 +4811,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4823,7 +4824,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5432,7 +5433,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6546,7 +6547,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 11001e83..49cfb455 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,7 +65,7 @@ tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_info", ] } -tracing-journald = "0.3.2" +tracing-journald = { version = "0.3.2", optional = true } tracing-subscriber = { version = "0.3.22", features = [ "env-filter", "tracing-log", @@ -125,7 +125,7 @@ optional = true debug = ["egui", "egui_plot", "smithay-egui", "anyhow/backtrace"] default = ["systemd"] logind = ["logind-zbus"] -systemd = ["libsystemd", "logind"] +systemd = ["libsystemd", "logind", "tracing-journald"] profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"] profile-with-tracy-gpu = ["profile-with-tracy", "smithay/tracy_gpu_profiling"] diff --git a/Makefile b/Makefile deleted file mode 100644 index 16f59ee6..00000000 --- a/Makefile +++ /dev/null @@ -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" diff --git a/README.md b/README.md new file mode 100644 index 00000000..0465d1a9 --- /dev/null +++ b/README.md @@ -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 diff --git a/cargo.just b/cargo.just new file mode 100644 index 00000000..c05f7fff --- /dev/null +++ b/cargo.just @@ -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 diff --git a/cosmic-comp-config/src/lib.rs b/cosmic-comp-config/src/lib.rs index 510927f9..193fd0db 100644 --- a/cosmic-comp-config/src/lib.rs +++ b/cosmic-comp-config/src/lib.rs @@ -3,6 +3,7 @@ use cosmic_config::{CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::time::Duration; use crate::input::TouchpadOverride; @@ -106,8 +107,8 @@ pub struct CosmicCompConfig { pub edge_snap_threshold: u32, pub accessibility_zoom: ZoomConfig, pub appearance_settings: AppearanceConfig, - /// Hide the cursor after this many seconds of pointer inactivity (None disables) - pub cursor_hide_timeout: Option, + /// When the cursor hides itself: idle, fullscreen idle, typing, touch + pub cursor_hide: CursorHideConfig, /// Briefly magnify the cursor when the pointer is shaken, to help locate it pub cursor_shake_to_find: bool, pub activation_policy: ActivationPolicy, @@ -148,7 +149,7 @@ impl Default for CosmicCompConfig { edge_snap_threshold: 0, accessibility_zoom: ZoomConfig::default(), appearance_settings: AppearanceConfig::default(), - cursor_hide_timeout: None, + cursor_hide: CursorHideConfig::default(), cursor_shake_to_find: true, activation_policy: ActivationPolicy::default(), decoration_preference: DecorationPreference::default(), @@ -198,6 +199,81 @@ fn default_repeat_delay() -> u32 { 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, + /// Seconds of pointer inactivity before hiding over a fullscreen window. + /// Shortens `idle_timeout` in fullscreen; it never lengthens it. + pub fullscreen_idle_timeout: Option, + /// 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 { + 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 { + 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)] pub struct ZoomConfig { pub start_on_login: bool, @@ -263,3 +339,109 @@ pub enum XwaylandDescaling { #[default] 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); + } +} diff --git a/debian/control b/debian/control index a3baf0eb..18c26482 100644 --- a/debian/control +++ b/debian/control @@ -6,6 +6,7 @@ Build-Depends: cargo, cmake, debhelper-compat (=10), + just, libegl1-mesa-dev, libfontconfig-dev, libgbm-dev, diff --git a/debian/rules b/debian/rules index 5a266947..8bd7c8d4 100755 --- a/debian/rules +++ b/debian/rules @@ -1,6 +1,6 @@ #!/usr/bin/make -f -export sysconfdir = "/usr/share" +DESTDIR = debian/cosmic-comp export VENDOR ?= 1 CLEAN ?= 1 @@ -9,12 +9,18 @@ CLEAN ?= 1 override_dh_auto_clean: ifeq ($(CLEAN),1) - ischroot && make clean || make distclean + ischroot && just clean || just clean-dist endif ifeq ($(VENDOR),1) - ischroot || make vendor + ischroot || just vendor endif +override_dh_auto_build: + just build-vendored + +override_dh_auto_install: + just rootdir=$(DESTDIR) install + override_dh_installinit: dh_installinit -r diff --git a/justfile b/justfile new file mode 100644 index 00000000..05db4b52 --- /dev/null +++ b/justfile @@ -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}} diff --git a/src/backend/kms/mod.rs b/src/backend/kms/mod.rs index 36c6407c..4dcffc75 100644 --- a/src/backend/kms/mod.rs +++ b/src/backend/kms/mod.rs @@ -615,7 +615,13 @@ impl State { .clone(); 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 .drm_devices .values() diff --git a/src/backend/render/cursor.rs b/src/backend/render/cursor.rs index 8e14d1be..02da0685 100644 --- a/src/backend/render/cursor.rs +++ b/src/backend/render/cursor.rs @@ -5,9 +5,11 @@ use crate::{ element::AsGlowRenderer, wayland::{SurfaceRenderElement, push_render_elements_from_surface_tree}, }, + shell::focus::target::KeyboardFocusTarget, utils::prelude::*, wayland::handlers::compositor::FRAME_TIME_FILTER, }; +use cosmic_comp_config::HideDecision; use keyframe::{ease, functions::EaseInOutCubic}; use resvg::{tiny_skia, usvg}; use serde::Deserialize; @@ -451,6 +453,32 @@ pub fn draw_dnd_icon( ); } +/// 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; pub struct CursorStateInner { current_cursor: Option, @@ -462,9 +490,10 @@ pub struct CursorStateInner { current_image: Option, image_cache: Vec, - hidden: bool, + hidden: Option, idle_timer: Option, last_armed: Option, + last_pointer_activity: Instant, // shake-to-find shake_path: VecDeque, @@ -694,7 +723,8 @@ impl Default for CursorStateInner { current_image: None, image_cache: Vec::new(), - hidden: false, + hidden: None, + last_pointer_activity: Instant::now(), idle_timer: None, last_armed: None, @@ -747,7 +777,7 @@ pub fn draw_cursor( let mut state_ref = seat_userdata.get::().unwrap().lock().unwrap(); let state = &mut *state_ref; - if state.hidden { + if state.hidden.is_some() { return; } @@ -837,68 +867,175 @@ pub fn draw_cursor( 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 -pub fn notify_cursor_activity(state: &State, seat: &Seat) -> bool { - let timeout = state.common.config.cosmic_conf.cursor_hide_timeout; +/// Reveal the cursor if this event should, and (re)arm the idle-hide timer. +/// Returns true if the cursor was previously hidden and is now visible. +pub fn notify_cursor_activity(state: &State, seat: &Seat, kind: PointerEventKind) -> bool { + let cursor_state = seat.user_data().get::().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) { + let config = state.common.config.cosmic_conf.cursor_hide; let loop_handle = &state.common.event_loop_handle; let cursor_state = seat.user_data().get::().unwrap(); let now = Instant::now(); - let (was_hidden, old_token) = { + let (old_token, since_activity) = { let mut inner = cursor_state.lock().unwrap(); - let was_hidden = inner.hidden; - inner.hidden = false; - - let throttled = timeout.is_some() - && !was_hidden - && inner.idle_timer.is_some() + if inner.hidden.is_some() { + return; + } + // Coalesce bursts from high-frequency pointers. + let throttled = inner.idle_timer.is_some() && inner .last_armed .is_some_and(|t| now.duration_since(t) < ACTIVITY_THROTTLE); if throttled { - return was_hidden; + return; } - - let old_token = inner.idle_timer.take(); + let since_activity = now.duration_since(inner.last_pointer_activity); inner.last_armed = None; - (was_hidden, old_token) + (inner.idle_timer.take(), since_activity) }; if let Some(token) = old_token { loop_handle.remove(token); } - if let Some(secs) = timeout { - let timer = Timer::from_duration(Duration::from_secs(secs as u64)); - let seat = seat.clone(); - if let Ok(token) = loop_handle.insert_source(timer, move |_, _, state| { - hide_cursor(state, &seat); - TimeoutAction::Drop - }) { + let Some(delay) = config.arm_delay(since_activity) else { + return; + }; + + let timer = Timer::from_duration(delay); + 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(); inner.idle_timer = Some(token); 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) { - if let Some(ptr) = seat.get_pointer() - && ptr.is_grabbed() - { - return; +/// Whether the seat is focusing a fullscreen surface. A fullscreen window that +/// is not focused can be stacked behind others, so its presence on the output +/// says nothing about what the user is looking at. +fn fullscreen_focused(seat: &Seat) -> bool { + matches!( + seat.get_keyboard().and_then(|kbd| kbd.current_focus()), + Some(KeyboardFocusTarget::Fullscreen(_)) + ) +} + +fn on_idle_timer(state: &mut State, seat: &Seat) -> TimeoutAction { + let config = state.common.config.cosmic_conf.cursor_hide; + let fullscreen = fullscreen_focused(seat); + let cursor_state = seat.user_data().get::().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, reason: HideReason) { + let loop_handle = state.common.event_loop_handle.clone(); + let token = { + let cursor_state = seat.user_data().get::().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, reason: HideReason) { + let grabbed = seat.get_pointer().is_some_and(|ptr| ptr.is_grabbed()); let cursor_state = seat.user_data().get::().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.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(); for output in outputs { 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)); + } + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 4d2fad9b..9bdef7c1 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -10,7 +10,7 @@ use crate::{ }, }; 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::{Shortcuts, shortcuts, window_rules}; use serde::{Deserialize, Serialize}; @@ -49,8 +49,8 @@ mod types; use cosmic::config::CosmicTk; pub use cosmic_comp_config::EdidProduct; use cosmic_comp_config::{ - ActivationPolicy, AppearanceConfig, CosmicCompConfig, DecorationPreference, KeyboardConfig, - TileBehavior, XkbConfig, XwaylandDescaling, XwaylandEavesdropping, ZoomConfig, + ActivationPolicy, AppearanceConfig, CosmicCompConfig, CursorHideConfig, DecorationPreference, + KeyboardConfig, TileBehavior, XkbConfig, XwaylandDescaling, XwaylandEavesdropping, ZoomConfig, input::{DeviceState as InputDeviceState, InputConfig, TouchpadOverride}, output::comp::{ OutputConfig, OutputInfo, OutputState, OutputsConfig, TransformDef, load_outputs, @@ -172,7 +172,7 @@ pub enum ColorFilter { } 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 source = cosmic_config::calloop::ConfigWatchSource::new(&config).unwrap(); loop_handle @@ -182,7 +182,7 @@ impl Config { .expect("Failed to add cosmic-config to the event loop"); let xdg = xdg::BaseDirectories::new(); - let cosmic_comp_config = + let mut cosmic_comp_config = CosmicCompConfig::get_entry(&config).unwrap_or_else(|(errs, c)| { if cfg!(debug_assertions) { for err in errs { @@ -192,6 +192,18 @@ impl Config { 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::("cursor_hide").is_err() + && let Ok(legacy) = config.get::>("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 if let Ok(tk_config) = cosmic_config::Config::new("com.system76.CosmicTk", 1) { fn handle_new_toolkit_config(config: CosmicTk, state: &mut State) { @@ -248,40 +260,48 @@ impl Config { // Source key bindings from com.system76.CosmicSettings.Shortcuts let settings_context = shortcuts::context().expect("Failed to load shortcuts config"); - let system_actions = shortcuts::system_actions(&settings_context); - let shortcuts = shortcuts::shortcuts(&settings_context); + let mut system_actions = Default::default(); + 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. - match cosmic_config::calloop::ConfigWatchSource::new(&settings_context) { - Ok(source) => { - if let Err(err) = loop_handle.insert_source(source, |(config, keys), (), state| { - for key in keys { - match key.as_str() { - // Reload the keyboard shortcuts config. - "custom" | "defaults" => { - state.common.config.shortcuts = shortcuts::shortcuts(&config); + // Listen for updates to the keybindings config. + match cosmic_config::calloop::ConfigWatchSource::new(&settings_context) { + Ok(source) => { + if let Err(err) = + loop_handle.insert_source(source, |(config, keys), (), state| { + for key in keys { + match key.as_str() { + // Reload the keyboard 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 = - shortcuts::system_actions(&config); - } - - _ => (), - } + }) + { + warn!( + ?err, + "Failed to watch com.system76.CosmicSettings.Shortcuts config" + ); } - }) { - warn!( - ?err, - "Failed to watch com.system76.CosmicSettings.Shortcuts config" - ); } - } - Err(err) => warn!( - ?err, - "failed to create config watch source for com.system76.CosmicSettings.Shortcuts" - ), - }; + Err(err) => warn!( + ?err, + "failed to create config watch source for com.system76.CosmicSettings.Shortcuts" + ), + }; + } let window_rules_context = window_rules::context().expect("Failed to load window rules config"); @@ -983,15 +1003,21 @@ fn config_changed(config: cosmic_config::Config, keys: Vec, state: &mut let new = get_config::(&config, "cursor_shake_to_find"); state.common.config.cosmic_conf.cursor_shake_to_find = new; } - "cursor_hide_timeout" => { - let new = get_config::>(&config, "cursor_hide_timeout"); - if new != state.common.config.cosmic_conf.cursor_hide_timeout { - state.common.config.cosmic_conf.cursor_hide_timeout = new; + "cursor_hide" => { + let new = get_config::(&config, "cursor_hide"); + if new != state.common.config.cosmic_conf.cursor_hide { + 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 mut needs_render = false; for seat in seats { - needs_render |= - crate::backend::render::cursor::notify_cursor_activity(state, &seat); + needs_render |= crate::backend::render::cursor::notify_cursor_activity( + state, + &seat, + crate::backend::render::cursor::PointerEventKind::Motion, + ); } if needs_render { let outputs: Vec<_> = diff --git a/src/input/mod.rs b/src/input/mod.rs index 45586b03..6cac7cdf 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1,7 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::{ - backend::render::{ElementFilter, cursor::notify_cursor_activity}, + backend::render::{ + ElementFilter, + cursor::{PointerEventKind, notify_cursor_activity}, + }, config::{ Action, Config, PrivateAction, key_bindings::{ @@ -19,7 +22,7 @@ use crate::{ Stage, render_input_order, target::{KeyboardFocusTarget, PointerFocusTarget}, }, - grabs::{ReleaseMode, ResizeEdge}, + grabs::{MenuGrab, MoveGrab, ReleaseMode, ResizeEdge, ResizeGrab}, layout::{ floating::ResizeGrabMarker, tiling::{NodeDesc, SwapWindowGrab, TilingLayout}, @@ -49,7 +52,7 @@ use smithay::{ PointerAxisEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent, TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent, }, - desktop::{PopupKeyboardGrab, WindowSurfaceType, utils::under_from_surface_tree}, + desktop::{WindowSurfaceType, utils::under_from_surface_tree}, input::{ Seat, keyboard::KeyboardHandle, @@ -220,7 +223,16 @@ impl State { ) where ::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; match event { @@ -327,6 +339,26 @@ impl State { 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() { 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(); if self.common.config.cosmic_conf.cursor_shake_to_find @@ -714,7 +746,7 @@ impl State { .cloned(); if let Some(seat) = maybe_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(_)) { // EI absolute coordinates are in the compositor's *global* // logical space: each advertised region carries its output's @@ -809,7 +841,7 @@ impl State { return; }; 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 shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| { @@ -1060,7 +1092,7 @@ impl State { .cloned(); if let Some(seat) = maybe_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 && self @@ -1462,11 +1494,18 @@ impl State { }; let under = State::surface_under(position, &output, &shell) .map(|(target, pos)| (target, pos.as_logical())); + let focus_target = State::element_under(position, &output, &shell, &seat); std::mem::drop(shell); let serial = SERIAL_COUNTER.next_serial(); 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( self, under, @@ -1477,6 +1516,14 @@ impl State { 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, .. } => { @@ -1592,7 +1639,7 @@ impl State { .cloned() { self.common.idle_notifier_state.notify_activity(&seat); - notify_cursor_activity(self, &seat); + notify_cursor_activity(self, &seat, PointerEventKind::Motion); let Some(output) = mapped_output_for_device(&self.common.config, &shell, &event.device()) .cloned() @@ -1696,7 +1743,7 @@ impl State { .cloned() { self.common.idle_notifier_state.notify_activity(&seat); - notify_cursor_activity(self, &seat); + notify_cursor_activity(self, &seat, PointerEventKind::Motion); let Some(output) = mapped_output_for_device(&self.common.config, &shell, &event.device()) .cloned() @@ -1845,7 +1892,7 @@ impl State { .cloned(); if let Some(seat) = maybe_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 output = seat.active_output(); @@ -1898,7 +1945,7 @@ impl State { .cloned(); if let Some(seat) = maybe_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()) { tool.button( self, @@ -2096,6 +2143,21 @@ impl State { 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) && !seat.get_keyboard().unwrap().is_grabbed() && !shortcuts_inhibited @@ -2291,17 +2353,15 @@ impl State { let keyboard = seat.get_keyboard().unwrap(); let pointer = seat.get_pointer().unwrap(); - // We're only interested in filtering keyboard grabs if we initiated them. - // The easiest way to check that is to check the type of the grab. - let keyboard_grabbed = keyboard.with_grab(|_serial, grab| { - grab.is::() || grab.is::>() + // Escape only cancels grabs the compositor started for its own interactions + // For keyboard: SwapWindowGrab + // For pointer: MoveGrab, ResizeGrab, and MenuGrab + let keyboard_grabbed = + keyboard.with_grab(|_serial, grab| grab.is::()) == Some(true); + let pointer_grabbed = pointer.with_grab(|_serial, grab| { + grab.is::() || grab.is::() || grab.is::() }) == Some(true); - // A virtual-keyboard key can arrive while the seat's pointer is grabbed by that - // 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 is_grabbed = keyboard_grabbed || pointer_grabbed; let current_focus = keyboard.current_focus(); //this should fall back to active output since there may not be a focused output diff --git a/src/lib.rs b/src/lib.rs index aa31a464..98bfa2a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -289,7 +289,7 @@ Project home page: https://github.com/pop-os/cosmic-comp Options: -h, --help Show this message --no-xwayland Run without Xwayland - -v, --version Show the version of cosmic-comp"# + -V, --version Show the version of cosmic-comp"# ); } diff --git a/src/logger/mod.rs b/src/logger/mod.rs index 3e5347df..34baf42d 100644 --- a/src/logger/mod.rs +++ b/src/logger/mod.rs @@ -4,7 +4,10 @@ use std::str::FromStr; 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_subscriber::{EnvFilter, filter::Directive, fmt, prelude::*}; @@ -29,6 +32,7 @@ pub fn init_logger() -> Result<()> { let fmt_layer = fmt::layer().compact(); + #[cfg(feature = "systemd")] match journald::layer() { Ok(journald_layer) => tracing_subscriber::registry() .with(fmt_layer) @@ -43,6 +47,11 @@ pub fn init_logger() -> Result<()> { warn!(?err, "Failed to init journald logging."); } }; + #[cfg(not(feature = "systemd"))] + tracing_subscriber::registry() + .with(fmt_layer) + .with(filter) + .init(); log_panics::init(); info!("Version: {}", std::env!("CARGO_PKG_VERSION")); diff --git a/src/shell/element/mod.rs b/src/shell/element/mod.rs index d4743b7f..c7e799be 100644 --- a/src/shell/element/mod.rs +++ b/src/shell/element/mod.rs @@ -430,6 +430,10 @@ impl CosmicMapped { } } + pub fn is_sticky(&self) -> bool { + self.active_window().is_sticky() + } + pub fn pending_size(&self) -> Option> { match &self.element { CosmicMappedInternal::Stack(s) => s.pending_size(), diff --git a/src/shell/focus/mod.rs b/src/shell/focus/mod.rs index f631372e..8b2348eb 100644 --- a/src/shell/focus/mod.rs +++ b/src/shell/focus/mod.rs @@ -2,12 +2,18 @@ use crate::{ shell::{CosmicSurface, MinimizedWindow, Shell, Trigger, element::CosmicMapped}, state::{Common, State}, 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 smithay::{ 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}, output::Output, reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface}, @@ -249,8 +255,17 @@ impl Shell { let workspace = if let Some(workspace) = workspace { self.workspaces.space_for_handle_mut(&workspace.0).unwrap() } else { - //should this be the active output or the focused output? - self.active_space_mut(&seat.focused_or_active_output()) + let output = match &target { + 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() }; @@ -627,19 +642,42 @@ fn focus_target_is_valid( output: &Output, target: KeyboardFocusTarget, ) -> 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 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(_)); } // If an exclusive layer shell surface exists (on any output), only exclusive // 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) { - return if let KeyboardFocusTarget::LayerSurface(layer_surface) = target { + let is_exclusive_on_layer = |layer_surface: &LayerSurface| { let data = layer_surface.cached_state(); (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, }; } diff --git a/src/shell/focus/order.rs b/src/shell/focus/order.rs index fb904c01..937fa311 100644 --- a/src/shell/focus/order.rs +++ b/src/shell/focus/order.rs @@ -22,7 +22,7 @@ use crate::{ prelude::OutputExt, 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> { @@ -77,6 +77,8 @@ fn render_input_order_internal( element_filter: ElementFilter, mut callback: impl FnMut(Stage) -> ControlFlow, ()>, ) -> ControlFlow, ()> { + // NOTE: Keep in sync with other surface iteration functions + if shell .zoom_state .as_ref() @@ -87,7 +89,92 @@ fn render_input_order_internal( // 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 diff --git a/src/shell/grabs/menu/mod.rs b/src/shell/grabs/menu/mod.rs index 9ea37a31..37755027 100644 --- a/src/shell/grabs/menu/mod.rs +++ b/src/shell/grabs/menu/mod.rs @@ -579,10 +579,12 @@ impl PointerGrab for MenuGrab { PointerTarget::motion(&element.iced, &self.seat, state, &new_event); } } else { + // Keep the root menu hovered while a submenu is open + let keep_root = usize::from(elements.len() > 1); elements .iter_mut() .filter(|element| element.pointer_entered) - .skip(1) + .skip(keep_root) .for_each(|element| { PointerTarget::leave( &element.iced, @@ -1020,10 +1022,11 @@ impl TabletToolGrab for MenuGrab { ); self.last_tablet_idx = Some(i); } else { + let keep_root = usize::from(elements.len() > 1); elements .iter_mut() .filter(|element| element.tablet_entered.is_some()) - .skip(1) + .skip(keep_root) .for_each(|element| { TabletToolTarget::proximity_out( &element.iced, diff --git a/src/shell/layout/tiling/mod.rs b/src/shell/layout/tiling/mod.rs index 8706dc59..20e11b71 100644 --- a/src/shell/layout/tiling/mod.rs +++ b/src/shell/layout/tiling/mod.rs @@ -3271,12 +3271,34 @@ impl TilingLayout { } } + let (_, inner) = self.gaps(); let mut result = None; + let mut fork = None; let mut lookup = Some(root.clone()); while let Some(node) = lookup { let data = tree.get(&node).unwrap().data(); if data.geometry().contains(location) { 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; @@ -3323,25 +3345,16 @@ impl TilingLayout { ) }) } - Some(( - id, - Data::Group { + Some((_, Data::Group { .. })) => { + let (id, idx) = fork?; + let Data::Group { orientation, last_geometry, .. - }, - )) => { - let idx = tree - .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))?; + } = tree.get(&id).unwrap().data().clone() + else { + unreachable!() + }; Some(( ResizeForkTarget { node: id.clone(), diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 47d78851..10b40b59 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -19,7 +19,10 @@ use crate::{ utils, wayland::{ 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::{ @@ -513,6 +516,12 @@ impl WorkspaceSet { } } + fn refresh_focus_stacks(&mut self) { + for workspace in &mut self.workspaces { + workspace.refresh_focus_stack(); + } + } + fn activate( &mut self, idx: usize, @@ -617,6 +626,7 @@ impl WorkspaceSet { self.workspaces[self.active].refresh(); } self.sticky_layer.refresh(); + self.refresh_focus_stacks(); } fn add_empty_workspace(&mut self, state: &mut WorkspaceUpdateGuard) { @@ -2046,12 +2056,27 @@ impl Shell { } 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 { - return session_lock + if let Some((output, _)) = session_lock .surfaces .iter() .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() diff --git a/src/shell/workspace.rs b/src/shell/workspace.rs index 84a020ff..fd63dcb1 100644 --- a/src/shell/workspace.rs +++ b/src/shell/workspace.rs @@ -526,10 +526,16 @@ impl Workspace { .mapped() .chain(self.tiling_layer.mapped().map(|(w, _)| w)) .chain(move_mapped.iter()) + .chain( + self.minimized_windows + .iter() + .flat_map(MinimizedWindow::mapped), + ) }; stack.retain(|w| match w { 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( renderer, - focused.as_ref().and_then(|target| { - if let FocusTarget::Window(mapped) = target { - Some(mapped) - } else { - None - } - }), + render_focus + .then(|| { + focused.as_ref().and_then(|target| { + if let FocusTarget::Window(mapped) = target { + Some(mapped) + } else { + None + } + }) + }) + .flatten(), resize_indicator.clone(), indicator_thickness, alpha, diff --git a/src/state.rs b/src/state.rs index 28ed8fd4..86f5e23e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -23,6 +23,7 @@ use crate::{ output_configuration::OutputConfigurationState, output_power::OutputPowerState, overlap_notify::OverlapNotifyState, + session_lock_layer::SessionLockLayerState, toplevel_info::ToplevelInfoState, toplevel_management::{ManagementCapabilities, ToplevelManagementState}, workspace::{WorkspaceState, WorkspaceUpdateGuard}, @@ -320,6 +321,7 @@ pub struct Common { pub xwayland_state: Option, pub xwayland_shell_state: XWaylandShellState, pub pointer_focus_state: Option, + pub session_lock_layer_state: SessionLockLayerState, #[cfg(feature = "logind")] pub inhibit_lid_fd: Option, @@ -665,7 +667,7 @@ impl State { .unwrap(); let clock = Clock::new(); - let config = Config::load(&handle); + let config = Config::load(&handle, kiosk_command.is_some()); let compositor_state = CompositorState::new::(dh); let corner_radius_state = CornerRadiusState::new::(dh); let data_device_state = DataDeviceState::new::(dh); @@ -763,6 +765,9 @@ impl State { let dbus_state = DBusState::init(&handle); + let session_lock_layer_state = + SessionLockLayerState::new::(dh, client_not_sandboxed); + State { common: Common { config, @@ -830,6 +835,7 @@ impl State { pointer_focus_state: None, dbus_state, keyboard_layout_state, + session_lock_layer_state, #[cfg(feature = "logind")] inhibit_lid_fd: None, @@ -968,6 +974,8 @@ impl Common { output: &Output, render_element_states: &RenderElementStates, ) { + // NOTE: Keep in sync with surface iteration in `render_input_order_internal` + let shell = self.shell.read(); let processor = |namespace: Option| { move |surface: &WlSurface, states: &SurfaceData| { @@ -1074,6 +1082,8 @@ impl Common { render_element_states: &RenderElementStates, mut dmabuf_feedback: impl FnMut(DrmNode) -> Option, ) { + // NOTE: Keep in sync with surface iteration in `render_input_order_internal` + let shell = self.shell.read(); if let Some(session_lock) = shell.session_lock.as_ref() @@ -1287,6 +1297,8 @@ impl Common { #[profiling::function] pub fn send_frames(&self, output: &Output, sequence: Option) { + // NOTE: Keep in sync with surface iteration in `render_input_order_internal` + let time = self.clock.now(); let should_send = |surface: &WlSurface, states: &SurfaceData| { // Do the standard primary scanout output check. For pointer surfaces it deduplicates diff --git a/src/utils/iced/mod.rs b/src/utils/iced/mod.rs index 4522644b..8baf87f9 100644 --- a/src/utils/iced/mod.rs +++ b/src/utils/iced/mod.rs @@ -519,6 +519,7 @@ impl TabletToolTarget for Iced _tool_descriptor: &TabletToolDescriptor, ) { let mut internal = self.0.lock().unwrap(); + internal.cursor_pos = None; internal .state .queue_event(Event::Mouse(MouseEvent::CursorLeft)); @@ -728,6 +729,7 @@ impl PointerTarget for IcedEle _time: InputTime, ) { let mut internal = self.0.lock().unwrap(); + internal.cursor_pos = None; internal .state .queue_event(Event::Mouse(MouseEvent::CursorLeft)); diff --git a/src/wayland/handlers/image_copy_capture/mod.rs b/src/wayland/handlers/image_copy_capture/mod.rs index 85a01c9a..57ef9500 100644 --- a/src/wayland/handlers/image_copy_capture/mod.rs +++ b/src/wayland/handlers/image_copy_capture/mod.rs @@ -1,6 +1,8 @@ // 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::{ backend::{ @@ -452,23 +454,28 @@ fn constraints_for_renderer( .ok() .and_then(|device| device.try_get_render_node().ok().flatten()) { - constraints.dma = Some(DmabufConstraints { - node, - formats: renderer - .egl_context() - .dmabuf_render_formats() - .iter() - .fold( - HashMap::>::new(), - |mut map, format| { - map.entry(format.code).or_default().push(format.modifier); - map - }, - ) - .into_iter() - .collect::>(), - }); + let mut formats = renderer + .egl_context() + .dmabuf_render_formats() + .iter() + .fold( + IndexMap::>::new(), + |mut map, format| { + map.entry(format.code).or_default().push(format.modifier); + map + }, + ) + .into_iter() + .collect::>(); + formats.sort_by_key(|(code, _)| !is_preferred_capture_format(*code)); + constraints.dma = Some(DmabufConstraints { node, formats }); } 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) +} diff --git a/src/wayland/handlers/output_power.rs b/src/wayland/handlers/output_power.rs index 2675009a..333aacc6 100644 --- a/src/wayland/handlers/output_power.rs +++ b/src/wayland/handlers/output_power.rs @@ -21,6 +21,11 @@ pub fn set_all_surfaces_dpms_on(state: &mut State) { } 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); } } diff --git a/src/wayland/handlers/xdg_activation.rs b/src/wayland/handlers/xdg_activation.rs index ad8ce0d0..38ba6219 100644 --- a/src/wayland/handlers/xdg_activation.rs +++ b/src/wayland/handlers/xdg_activation.rs @@ -243,7 +243,7 @@ impl State { { for mapped in current_workspace .mapped() - .filter(|m| m.maximized_state.lock().unwrap().is_some()) + .filter(|m| m.maximized_state.lock().unwrap().is_some() && *m != &element) .cloned() .collect::>() .into_iter() diff --git a/src/wayland/protocols/mod.rs b/src/wayland/protocols/mod.rs index c6c0234a..48aa8337 100644 --- a/src/wayland/protocols/mod.rs +++ b/src/wayland/protocols/mod.rs @@ -8,6 +8,7 @@ pub mod keyboard_layout; pub mod output_configuration; pub mod output_power; pub mod overlap_notify; +pub mod session_lock_layer; pub mod toplevel_info; pub mod toplevel_management; pub mod workspace; diff --git a/src/wayland/protocols/session_lock_layer.rs b/src/wayland/protocols/session_lock_layer.rs new file mode 100644 index 00000000..85edda50 --- /dev/null +++ b/src/wayland/protocols/session_lock_layer.rs @@ -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(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::( + 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 Fn(&'a Client) -> bool + Send + Sync>, +} + +impl GlobalDispatch2 + 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, + 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::() { + let mut state = states.cached_state.get::(); + state.current().show_on_lock + } else { + false + } + }) +} + +impl Dispatch2 + 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::(); + state.pending().show_on_lock = value; + }); + } + } +}