merge: epoch-1.10.0 d'upstream

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lionel DARNIS 2026-10-11 07:00:05 +02:00
commit 11f60acc94
25 changed files with 758 additions and 222 deletions

View file

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

View file

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

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

View file

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

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 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<u32>,
/// 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<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)]
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);
}
}

1
debian/control vendored
View file

@ -6,6 +6,7 @@ Build-Depends:
cargo,
cmake,
debhelper-compat (=10),
just,
libegl1-mesa-dev,
libfontconfig-dev,
libgbm-dev,

12
debian/rules vendored
View file

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

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

View file

@ -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<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 struct CursorStateInner {
current_cursor: Option<CursorIcon>,
@ -462,9 +490,10 @@ pub struct CursorStateInner {
current_image: Option<Image>,
image_cache: Vec<CachedFrame>,
hidden: bool,
hidden: Option<HideReason>,
idle_timer: Option<RegistrationToken>,
last_armed: Option<Instant>,
last_pointer_activity: Instant,
// shake-to-find
shake_path: VecDeque<PathSample>,
@ -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<R>(
let mut state_ref = seat_userdata.get::<CursorState>().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<R>(
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<State>) -> 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<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 cursor_state = seat.user_data().get::<CursorState>().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<State>) {
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<State>) -> bool {
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 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));
}
}
}

View file

@ -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,
@ -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::<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
if let Ok(tk_config) = cosmic_config::Config::new("com.system76.CosmicTk", 1) {
fn handle_new_toolkit_config(config: CosmicTk, state: &mut State) {
@ -991,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");
state.common.config.cosmic_conf.cursor_shake_to_find = new;
}
"cursor_hide_timeout" => {
let new = get_config::<Option<u32>>(&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::<CursorHideConfig>(&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<_> =

View file

@ -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::{
@ -220,7 +223,16 @@ impl State {
) where
<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;
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,

View file

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

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>> {
match &self.element {
CosmicMappedInternal::Stack(s) => s.pending_size(),

View file

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

View file

@ -579,10 +579,12 @@ impl PointerGrab<State> 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<State> 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,

View file

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

View file

@ -516,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,
@ -620,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<State>) {

View file

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

View file

@ -519,6 +519,7 @@ impl<P: Program + Send + 'static> TabletToolTarget<crate::state::State> 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<P: Program + Send + 'static> PointerTarget<crate::state::State> for IcedEle
_time: InputTime,
) {
let mut internal = self.0.lock().unwrap();
internal.cursor_pos = None;
internal
.state
.queue_event(Event::Mouse(MouseEvent::CursorLeft));

View file

@ -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::<Fourcc, Vec<Modifier>>::new(),
|mut map, format| {
map.entry(format.code).or_default().push(format.modifier);
map
},
)
.into_iter()
.collect::<Vec<_>>(),
});
let mut formats = renderer
.egl_context()
.dmabuf_render_formats()
.iter()
.fold(
IndexMap::<Fourcc, Vec<Modifier>>::new(),
|mut map, format| {
map.entry(format.code).or_default().push(format.modifier);
map
},
)
.into_iter()
.collect::<Vec<_>>();
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)
}

View file

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