Compare commits

...

11 commits

Author SHA1 Message Date
934db82775 chore: align compositor with local cosmic stack 2026-05-24 11:40:51 +02:00
Niklas Herder
28258e5a5f cursor: Add idle-hide timeout
Adds a cursor_hide_timeout config key (Option<u32> seconds) to
CosmicCompConfig. When set, the cursor is hidden after the configured
period of pointer inactivity and revealed again by any pointer event.
Touch input does not count as activity (no visible cursor to surface).

Implementation:
- Per-seat hidden flag, calloop timer token, and last-armed Instant on
  CursorStateInner.
- notify_cursor_activity called from each pointer-related input branch
  (motion, button, axis, tablet) resets the flag and reschedules the
  timer; rapid successive calls are coalesced behind a 100ms throttle
  so high-frequency mice don't churn the calloop timer source.
- On timer fire, the hidden flag is set, draw_cursor short-circuits to
  an empty element list, and a render is scheduled. Active pointer
  grabs (drags, resizes) suppress the hide.
- Config reload arms or cancels the timer immediately; None as the
  configured value collapses the cancel path into the same function.

Closes #2231.

Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-05-22 14:14:17 +02:00
Levi Portenier
6ebe2a1f04
Merge pull request #2395 from hojjatabdollahi/hojjat/fix-fullscreen
fix: fullscreen x11 games opening as floating/tiled windows
2026-05-21 10:27:06 -06:00
Hojjat
451b8319d6 fix: ignore configure requests from fullscreen windows
Fixes steam big picture mode full screen not taking the whole screen
2026-05-20 19:19:21 -06:00
Hojjat
200074e580 fix: fullscreen x11 games opening as floating/tiled windows 2026-05-20 18:30:03 -06:00
Levi Portenier
8450e89de4
Merge pull request #2381 from dra11y/fix-magnifier-scroll
fix(magnifier): smooth mouse wheel zoom and respect natural scroll direction
2026-05-20 11:39:42 -06:00
Tom Grushka
c776eeac03 change a11y zoom easing function from EaseInOutCubic to Linear to avoid lagging mouse wheel scroll 2026-05-19 17:46:21 -06:00
Tom Grushka
657f0aeddf revert self.update_zoom to update with wheel 2026-05-19 16:30:46 -06:00
Victoria Brekenfeld
61e677e39f kms: Try to re-open the device on permission errors after resume 2026-05-19 19:26:25 +02:00
Victoria Brekenfeld
23b198f30b kms: Fix surface drop_and_join 2026-05-19 19:26:25 +02:00
Tom Grushka
92fcceba54 fix(magnifier): smooth mouse wheel zoom and respect natural scroll direction
- Disable animation for mouse wheel zoom to eliminate the timing race
  that causes lag and sudden jumps. Each wheel tick now zooms instantly.
- Invert zoom direction when natural scrolling is enabled so that
  forward scroll zooms in, matching macOS/Windows behavior.
2026-05-15 15:51:24 -06:00
13 changed files with 1786 additions and 361 deletions

1317
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -17,18 +17,18 @@ cosmic-comp-config = { path = "cosmic-comp-config", features = [
"libdisplay-info",
"output",
] }
cosmic-config = { git = "https://github.com/pop-os/libcosmic", features = [
cosmic-config = { path = "../libcosmic/cosmic-config", features = [
"calloop",
"macro",
] }
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", rev = "160b086", default-features = false, features = [
cosmic-protocols = { path = "../cosmic-protocols", default-features = false, features = [
"server",
] }
cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon" }
cosmic-settings-daemon-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", features = [
cosmic-settings-config = { path = "../cosmic-settings-daemon/config" }
cosmic-settings-daemon-config = { path = "../cosmic-settings-daemon/cosmic-settings-daemon-config", features = [
"greeter",
] }
cosmic-text = { git = "https://github.com/pop-os/cosmic-text.git", features = [
cosmic-text = { git = "https://forge.aditua.com/leyoda/cosmic-text.git", branch = "local/pr-503", features = [
"shape-run-cache",
] }
libdisplay-info = "0.3.0"
@ -39,10 +39,14 @@ i18n-embed = { version = "0.16", features = [
"desktop-requester",
] }
i18n-embed-fl = "0.10"
iced_tiny_skia = { git = "https://github.com/pop-os/libcosmic" }
iced_tiny_skia = { path = "../libcosmic/iced/tiny_skia" }
indexmap = "2.13"
keyframe = "1.1.1"
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false }
cosmic = { package = "libcosmic-yoda", path = "../libcosmic", default-features = false, features = [
"tokio",
"wayland",
"multi-window",
] }
libsystemd = { version = "0.7", optional = true }
log-panics = { version = "2", features = ["with-backtrace"] }
ordered-float = "5.1"
@ -138,9 +142,36 @@ inherits = "release"
[profile.release]
lto = "fat"
[patch."https://github.com/pop-os/cosmic-protocols"]
cosmic-protocols = { git = "https://github.com/pop-os//cosmic-protocols", branch = "main" }
cosmic-client-toolkit = { git = "https://github.com/pop-os//cosmic-protocols", branch = "main" }
[patch.'https://github.com/pop-os/libcosmic']
cosmic-config = { path = "../libcosmic/cosmic-config" }
cosmic-theme = { path = "../libcosmic/cosmic-theme" }
iced = { path = "../libcosmic/iced" }
iced_accessibility = { path = "../libcosmic/iced/accessibility" }
iced_core = { path = "../libcosmic/iced/core" }
iced_futures = { path = "../libcosmic/iced/futures" }
iced_graphics = { path = "../libcosmic/iced/graphics" }
iced_renderer = { path = "../libcosmic/iced/renderer" }
iced_runtime = { path = "../libcosmic/iced/runtime" }
iced_tiny_skia = { path = "../libcosmic/iced/tiny_skia" }
iced_wgpu = { path = "../libcosmic/iced/wgpu" }
iced_widget = { path = "../libcosmic/iced/widget" }
iced_winit = { path = "../libcosmic/iced/winit" }
[patch.'https://github.com/pop-os/cosmic-protocols']
cosmic-protocols = { path = "../cosmic-protocols" }
cosmic-client-toolkit = { path = "../cosmic-protocols/client-toolkit" }
[patch.'https://github.com/pop-os/cosmic-settings-daemon']
cosmic-settings-config = { path = "../cosmic-settings-daemon/config" }
cosmic-settings-daemon-config = { path = "../cosmic-settings-daemon/cosmic-settings-daemon-config" }
[patch.'https://github.com/pop-os/cosmic-text.git']
cosmic-text = { git = "https://forge.aditua.com/leyoda/cosmic-text.git", branch = "local/pr-503" }
[patch.'https://forge.aditua.com/leyoda/window_clipboard.git']
window_clipboard = { path = "../window_clipboard" }
dnd = { path = "../window_clipboard/dnd" }
mime = { path = "../window_clipboard/mime" }
[patch.crates-io]
smithay = { git = "https://github.com/smithay/smithay.git", rev = "774f2ab" }

View file

@ -4,8 +4,8 @@ version = "1.0.0"
edition = "2024"
[dependencies]
cosmic-config = { git = "https://github.com/pop-os/libcosmic/" }
cosmic-randr-shell = { git = "https://github.com/pop-os/cosmic-randr/", optional = true }
cosmic-config = { path = "../../libcosmic/cosmic-config" }
cosmic-randr-shell = { path = "../../cosmic-randr/shell", optional = true }
input = "0.10.0"
libdisplay-info = { version = "0.3.0", optional = true }
serde = { version = "1", features = ["derive"] }

View file

@ -99,6 +99,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>,
}
impl Default for CosmicCompConfig {
@ -135,6 +137,7 @@ impl Default for CosmicCompConfig {
edge_snap_threshold: 0,
accessibility_zoom: ZoomConfig::default(),
appearance_settings: AppearanceConfig::default(),
cursor_hide_timeout: None,
}
}
}

View file

@ -28,7 +28,7 @@ use smithay::{
},
egl::{EGLContext, EGLDevice, EGLDisplay, context::ContextPriority},
renderer::glow::GlowRenderer,
session::Session,
session::{Session, libseat::LibSeatSession},
},
desktop::utils::OutputPresentationFeedback,
output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel},
@ -40,7 +40,10 @@ use smithay::{
wayland_server::DisplayHandle,
},
utils::{Clock, DevPath, DeviceFd, Monotonic, Point, Transform},
wayland::drm_lease::{DrmLease, DrmLeaseState},
wayland::{
drm_lease::{DrmLease, DrmLeaseState},
drm_syncobj::supports_syncobj_eventfd,
},
};
use tracing::{error, info, warn};
use wayland_backend::server::ClientId;
@ -99,6 +102,20 @@ pub struct Device {
pub socket: Option<Socket>,
}
#[derive(Debug)]
struct ReusableDevice {
leasing_global: Option<DrmLeaseState>,
active_clients: HashSet<ClientId>,
socket: Option<Socket>,
}
#[derive(Debug)]
struct OldDeviceState {
fd: DeviceFd,
outputs: HashMap<connector::Handle, Output>,
leased_connectors: HashSet<connector::Handle>,
}
#[derive(Debug)]
pub struct LockedDevice<'a> {
pub inner: &'a mut InnerDevice,
@ -222,138 +239,14 @@ impl State {
}
}
let fd = DrmDeviceFd::new(DeviceFd::from(
self.backend
.kms()
.session
.open(
path,
OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK,
)
.with_context(|| {
format!(
"Failed to optain file descriptor for drm device: {}",
path.display()
)
})?,
));
let (drm, notifier) = DrmDevice::new(fd.clone(), false)
.with_context(|| format!("Failed to initialize drm device for: {}", path.display()))?;
let drm_node = DrmNode::from_dev_id(dev)?;
let supports_atomic = drm.is_atomic();
let gbm = GbmDevice::new(fd)
.with_context(|| format!("Failed to initialize GBM device for {}", path.display()))?;
let (render_node, render_formats, texture_formats, is_software) = {
let egl = init_egl(&gbm)?;
let render_node = egl
.device
.try_get_render_node()
.ok()
.and_then(std::convert::identity)
.unwrap_or(drm_node);
let render_formats = egl.context.dmabuf_render_formats().clone();
let texture_formats = egl.context.dmabuf_texture_formats().clone();
(
render_node,
render_formats,
texture_formats,
egl.device.is_software(),
)
};
let token = self
.common
.event_loop_handle
.insert_source(
notifier,
move |event, metadata, state: &mut State| match event {
DrmEvent::VBlank(crtc) => {
if let Some(device) = state.backend.kms().drm_devices.get_mut(&drm_node)
&& let Some(surface) = device.inner.surfaces.get_mut(&crtc)
{
surface.on_vblank(metadata.take());
}
}
DrmEvent::Error(err) => {
warn!(?err, "Failed to read events of device {:?}.", dev);
}
},
)
.with_context(|| format!("Failed to add drm device to event loop: {}", dev))?;
let socket = match (!is_software)
.then(|| self.create_socket(dh, render_node, texture_formats.clone()))
.transpose()
{
Ok(socket) => socket,
Err(err) => {
warn!(
?err,
"Failed to initialize hardware-acceleration for clients on {}.", render_node,
);
None
}
};
let leasing_global = match (!is_software)
.then(|| DrmLeaseState::new::<State>(dh, &drm_node))
.transpose()
{
Ok(global) => global,
Err(err) => {
// TODO: replace with inspect_err, once stable
warn!(
?err,
"Failed to initialize drm lease global for: {}", drm_node
);
None
}
};
let drm = GbmDrmOutputManager::new(
drm,
GbmAllocator::new(
gbm.clone(),
GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
),
GbmFramebufferExporter::new(gbm.clone(), render_node.into()),
Some(gbm.clone()),
[
Fourcc::Abgr2101010,
Fourcc::Argb2101010,
Fourcc::Abgr8888,
Fourcc::Argb8888,
],
render_formats,
);
let mut device = Device {
drm,
inner: InnerDevice {
dev_node: drm_node,
render_node,
is_software,
egl: None,
outputs: HashMap::new(),
surfaces: HashMap::new(),
gbm,
leased_connectors: Vec::new(),
leasing_global,
active_leases: Vec::new(),
active_clients: HashSet::new(),
},
supports_atomic,
texture_formats,
event_token: Some(token),
socket,
};
let mut device = Device::new(
dev,
path,
&mut self.backend.kms().session,
&mut self.common,
dh,
None,
)?;
let connectors = device.enumerate_surfaces()?.added; // There are no removed outputs on newly added devices
let mut wl_outputs = Vec::new();
let mut w = self.common.shell.read().global_space().size.w as u32;
@ -387,7 +280,7 @@ impl State {
// TODO atomic commit all surfaces together and drop surfaces, if it fails due to bandwidth
let kms = self.backend.kms();
kms.drm_devices.insert(drm_node, device);
kms.drm_devices.insert(device.inner.dev_node, device);
}
self.common
@ -489,6 +382,218 @@ impl State {
Ok(outputs_added)
}
pub fn reopen_device(
&mut self,
dev: dev_t,
path: &Path,
dh: &DisplayHandle,
) -> Result<Vec<Output>> {
let backend = self.backend.kms();
let drm_node = backend
.drm_devices
.values()
.find_map(|device| {
(device.inner.dev_node.dev_id() == dev).then_some(device.inner.dev_node)
})
.with_context(|| format!("Couldn't find drm node for {}", dev))?;
if let Some(mut device) = backend.drm_devices.shift_remove(&drm_node) {
let is_primary = backend
.primary_node
.read()
.unwrap()
.is_some_and(|node| node == device.inner.render_node);
// all drm resources become inert
if let Some(token) = device.event_token.take() {
self.common.event_loop_handle.remove(token);
}
for (_, surface) in device.inner.surfaces.drain() {
surface.drop_and_join();
}
if let Some(leasing_global) = device.inner.leasing_global.as_mut() {
for (connector, _) in &device.inner.leased_connectors {
leasing_global.withdraw_connector(*connector);
}
device.inner.active_leases.clear();
}
// These contain a reference to the file descriptor
backend.api.as_mut().remove_node(&device.inner.render_node);
// trigger enumeration
let _ = backend.api.devices();
for surface in backend
.drm_devices
.values_mut()
.flat_map(|device| device.inner.surfaces.values_mut())
{
surface.remove_node(device.inner.render_node);
}
let syncobj_guard =
if is_primary && let Some(syncobj_state) = backend.syncobj_state.as_mut() {
Some(syncobj_state.close_device())
} else {
None
};
let (reusable, mut old_state) = device.reuse();
match TryInto::<OwnedFd>::try_into(old_state.fd) {
Ok(fd) => {
if let Err(err) = backend.session.close(fd) {
warn!("Failed to close drm device fd: {}", err);
}
}
Err(_) => {
warn!(?drm_node, "Unable to close drm device fd cleanly.");
}
};
let mut new_device = Device::new(
dev,
path,
&mut backend.session,
&mut self.common,
dh,
Some(reusable),
)?;
if let Some(guard) = syncobj_guard {
let import_device = new_device.drm.device().device_fd().clone();
if supports_syncobj_eventfd(&import_device) {
guard.update_device(import_device);
} else {
drop(guard);
dh.remove_global::<State>(backend.syncobj_state.take().unwrap().into_global());
}
}
let connectors = new_device.enumerate_surfaces()?.added;
let mut outputs_added = Vec::new();
let mut w = self.common.shell.read().global_space().size.w as u32;
{
for (conn, maybe_crtc) in connectors {
if let Some(output) = old_state.outputs.remove(&conn) {
let has_surface = if old_state.leased_connectors.contains(&conn) {
if let Some(crtc) = maybe_crtc {
new_device.inner.leased_connectors.push((conn, crtc));
info!(
"Connector {} is non-desktop, setting up for leasing",
output.name()
);
if let Some(lease_state) = new_device.inner.leasing_global.as_mut()
{
let physical = output.physical_properties();
lease_state.add_connector::<State>(
conn,
output.name(),
format!("{} {}", physical.make, physical.model),
);
}
} else {
warn!(
"Connector {} is non-desktop, but we don't have a free crtc: not leasing",
output.name()
);
}
false
} else {
let loc = output.current_location();
populate_modes(
new_device.drm.device_mut(),
&output,
conn,
true,
(loc.x as u32, loc.y as u32),
)
.with_context(|| "Failed to enumerate connector modes")?;
if let Some(crtc) = maybe_crtc {
match Surface::new(
&output,
crtc,
conn,
backend.primary_node.clone(),
new_device.inner.dev_node,
new_device.inner.render_node,
&self.common.event_loop_handle,
self.common.config.dynamic_conf.screen_filter().clone(),
self.common.shell.clone(),
self.common.startup_done.clone(),
) {
Ok(data) => {
new_device.inner.surfaces.insert(crtc, data);
true
}
Err(err) => {
error!(?crtc, "Failed to initialize surface: {}", err);
false
}
}
} else {
false
}
};
if !has_surface {
output
.user_data()
.get::<RefCell<OutputConfig>>()
.unwrap()
.borrow_mut()
.enabled = OutputState::Disabled;
}
new_device.inner.outputs.insert(conn, output);
} else {
match new_device.inner.connector_added(
new_device.drm.device_mut(),
backend.primary_node.clone(),
conn,
maybe_crtc,
(w, 0),
&self.common.event_loop_handle,
self.common.config.dynamic_conf.screen_filter().clone(),
self.common.shell.clone(),
self.common.startup_done.clone(),
) {
Ok((output, should_expose)) => {
if should_expose {
w += output.geometry().size.w as u32;
outputs_added.push(output.clone());
}
new_device.inner.outputs.insert(conn, output);
}
Err(err) => {
warn!(?err, "Failed to initialize output, skipping");
}
}
}
}
}
self.common
.output_configuration_state
.remove_heads(old_state.outputs.values());
self.common
.output_configuration_state
.add_heads(outputs_added.iter());
for output in old_state.outputs.values() {
self.common.remove_output(output);
}
backend.drm_devices.insert(drm_node, new_device);
backend.refresh_used_devices()?;
Ok(outputs_added)
} else {
Ok(Vec::new())
}
}
pub fn device_removed(&mut self, dev: dev_t, dh: &DisplayHandle) -> Result<()> {
let backend = self.backend.kms();
// we can't use DrmNode::from_node_id, because that assumes the node is still on sysfs
@ -581,6 +686,158 @@ pub struct OutputChanges {
}
impl Device {
fn new(
dev: dev_t,
path: impl AsRef<Path>,
session: &mut LibSeatSession,
common: &mut Common,
dh: &DisplayHandle,
reuse: Option<ReusableDevice>,
) -> Result<Self> {
let path = path.as_ref();
let fd = DrmDeviceFd::new(DeviceFd::from(
session
.open(
path,
OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK,
)
.with_context(|| {
format!(
"Failed to optain file descriptor for drm device: {}",
path.display()
)
})?,
));
let (drm, notifier) = DrmDevice::new(fd.clone(), false)
.with_context(|| format!("Failed to initialize drm device for: {}", path.display()))?;
let dev_node = DrmNode::from_dev_id(dev)?;
let supports_atomic = drm.is_atomic();
let gbm = GbmDevice::new(fd)
.with_context(|| format!("Failed to initialize GBM device for {}", path.display()))?;
let (render_node, render_formats, texture_formats, is_software) = {
let egl = init_egl(&gbm)?;
let render_node = egl
.device
.try_get_render_node()
.ok()
.and_then(std::convert::identity)
.unwrap_or(dev_node);
let render_formats = egl.context.dmabuf_render_formats().clone();
let texture_formats = egl.context.dmabuf_texture_formats().clone();
(
render_node,
render_formats,
texture_formats,
egl.device.is_software(),
)
};
let token = common
.event_loop_handle
.insert_source(
notifier,
move |event, metadata, state: &mut State| match event {
DrmEvent::VBlank(crtc) => {
if let Some(device) = state.backend.kms().drm_devices.get_mut(&dev_node)
&& let Some(surface) = device.inner.surfaces.get_mut(&crtc)
{
surface.on_vblank(metadata.take());
}
}
DrmEvent::Error(err) => {
warn!(?err, "Failed to read events of device {:?}.", dev);
}
},
)
.with_context(|| format!("Failed to add drm device to event loop: {}", dev))?;
let ReusableDevice {
leasing_global,
active_clients,
socket,
} = reuse.unwrap_or_else(|| {
let socket = match (!is_software)
.then(|| common.create_socket(dh, render_node, texture_formats.clone()))
.transpose()
{
Ok(socket) => socket,
Err(err) => {
warn!(
?err,
"Failed to initialize hardware-acceleration for clients on {}.",
render_node,
);
None
}
};
let leasing_global = match (!is_software)
.then(|| DrmLeaseState::new::<State>(dh, &dev_node))
.transpose()
{
Ok(global) => global,
Err(err) => {
// TODO: replace with inspect_err, once stable
warn!(
?err,
"Failed to initialize drm lease global for: {}", dev_node
);
None
}
};
ReusableDevice {
leasing_global,
active_clients: HashSet::new(),
socket,
}
});
let drm = GbmDrmOutputManager::new(
drm,
GbmAllocator::new(
gbm.clone(),
GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
),
GbmFramebufferExporter::new(gbm.clone(), render_node.into()),
Some(gbm.clone()),
[
Fourcc::Abgr2101010,
Fourcc::Argb2101010,
Fourcc::Abgr8888,
Fourcc::Argb8888,
],
render_formats,
);
Ok(Device {
drm,
inner: InnerDevice {
dev_node,
render_node,
is_software,
egl: None,
outputs: HashMap::new(),
surfaces: HashMap::new(),
gbm,
leased_connectors: Vec::new(),
leasing_global,
active_leases: Vec::new(),
active_clients,
},
supports_atomic,
texture_formats,
event_token: Some(token),
socket,
})
}
pub fn enumerate_surfaces(&mut self) -> Result<OutputChanges> {
// enumerate our outputs
let config =
@ -634,6 +891,27 @@ impl Device {
drm: self.drm.lock(),
}
}
fn reuse(self) -> (ReusableDevice, OldDeviceState) {
let device = ReusableDevice {
leasing_global: self.inner.leasing_global,
active_clients: self.inner.active_clients,
socket: self.socket,
};
let state = OldDeviceState {
fd: self.drm.device().device_fd().device_fd(),
outputs: self.inner.outputs,
leased_connectors: self
.inner
.leased_connectors
.iter()
.map(|(conn, _)| *conn)
.collect(),
};
(device, state)
}
}
impl LockedDevice<'_> {

View file

@ -394,15 +394,37 @@ impl State {
continue;
}
};
let dh = state.common.display_handle.clone();
if state.backend.kms().drm_devices.contains_key(&drm_node) {
match state.device_changed(dev) {
Ok(outputs) => added.extend(outputs),
Err(err) => {
error!(?err, "Failed to update drm device {}.", path.display(),)
error!(
?err,
"Failed to update drm device {}. Re-opening",
path.display(),
);
match state.reopen_device(dev, path, &dh) {
Ok(outputs) => added.extend(outputs),
Err(err) => {
error!(
?err,
"Failed to re-open drm device {}. Device lost",
path.display(),
);
if let Err(err) = state.device_removed(dev, &dh) {
error!(
?err,
"Failed to close drm device {}.",
path.display(),
);
};
}
}
}
}
} else {
let dh = state.common.display_handle.clone();
match state.device_added(dev, path, &dh) {
Ok(outputs) => added.extend(outputs),
Err(err) => error!(?err, "Failed to add drm device {}.", path.display(),),

View file

@ -18,7 +18,7 @@ use smithay::{
use std::sync::Arc;
use tracing::{info, warn};
use crate::state::{ClientState, State, advertised_node_for_client};
use crate::state::{ClientState, Common, State, advertised_node_for_client};
#[derive(Debug)]
pub struct Socket {
@ -27,7 +27,7 @@ pub struct Socket {
pub dmabuf_global: DmabufGlobal,
}
impl State {
impl Common {
pub(super) fn create_socket(
&mut self,
dh: &DisplayHandle,
@ -36,7 +36,7 @@ impl State {
) -> Result<Socket> {
let socket_name = format!(
"{}-{}",
&self.common.socket.to_string_lossy(),
&self.socket.to_string_lossy(),
render_node
.dev_path()
.unwrap()
@ -53,33 +53,28 @@ impl State {
.with_context(|| "Failed to create drm format shared memory table")?;
let dmabuf_global = self
.common
.dmabuf_state
.create_global_with_filter_and_default_feedback::<State, _>(dh, &feedback, filter);
let drm_global_id = self
.common
.wl_drm_state
.create_global_with_filter::<State, _>(
dh,
render_node
.dev_path_with_type(NodeType::Render)
.or_else(|| render_node.dev_path())
.ok_or(anyhow!(
"Could not determine path for gpu node: {}",
render_node
))?,
formats,
&dmabuf_global,
filter,
);
let drm_global_id = self.wl_drm_state.create_global_with_filter::<State, _>(
dh,
render_node
.dev_path_with_type(NodeType::Render)
.or_else(|| render_node.dev_path())
.ok_or(anyhow!(
"Could not determine path for gpu node: {}",
render_node
))?,
formats,
&dmabuf_global,
filter,
);
// add a special socket for the gpu
let listener = ListeningSocketSource::with_name(&socket_name)
.with_context(|| format!("Failed to bind socket to {}", socket_name))?;
let socket_name_clone = socket_name.clone();
let token = self
.common
.event_loop_handle
.insert_source(listener, move |client_stream, _, state: &mut State| {
if let Err(err) = state.common.display_handle.insert_client(

View file

@ -464,7 +464,7 @@ impl Surface {
pub fn drop_and_join(mut self) {
let thread = self.thread.take();
let _ = self;
std::mem::drop(self);
if let Some(thread) = thread {
let name = thread.thread().name().unwrap().to_string();
let _ = thread.join();

View file

@ -17,14 +17,25 @@ use smithay::{
Seat,
pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus},
},
reexports::wayland_server::protocol::wl_surface,
reexports::{
calloop::{
RegistrationToken,
timer::{TimeoutAction, Timer},
},
wayland_server::protocol::wl_surface,
},
render_elements,
utils::{
Buffer as BufferCoords, Logical, Monotonic, Physical, Point, Scale, Size, Time, Transform,
},
wayland::compositor::{get_role, with_states},
};
use std::{collections::HashMap, io::Read, sync::Mutex};
use std::{
collections::HashMap,
io::Read,
sync::Mutex,
time::{Duration, Instant},
};
use tracing::warn;
use xcursor::{
CursorTheme,
@ -196,6 +207,10 @@ pub struct CursorStateInner {
cursors: HashMap<CursorIcon, Cursor>,
current_image: Option<Image>,
image_cache: Vec<(Image, MemoryRenderBuffer)>,
hidden: bool,
idle_timer: Option<RegistrationToken>,
last_armed: Option<Instant>,
}
impl CursorStateInner {
@ -246,6 +261,10 @@ impl Default for CursorStateInner {
cursors: HashMap::new(),
current_image: None,
image_cache: Vec::new(),
hidden: false,
idle_timer: None,
last_armed: None,
}
}
}
@ -271,6 +290,10 @@ where
let mut state_ref = seat_userdata.get::<CursorState>().unwrap().lock().unwrap();
let state = &mut *state_ref;
if state.hidden {
return Vec::new();
}
let named_cursor = state.current_cursor.or(match cursor_status {
CursorImageStatus::Named(named_cursor) => Some(named_cursor),
_ => None,
@ -335,3 +358,71 @@ where
Vec::new()
}
}
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;
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 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()
&& inner
.last_armed
.is_some_and(|t| now.duration_since(t) < ACTIVITY_THROTTLE);
if throttled {
return was_hidden;
}
let old_token = inner.idle_timer.take();
inner.last_armed = None;
(was_hidden, old_token)
};
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 mut inner = cursor_state.lock().unwrap();
inner.idle_timer = Some(token);
inner.last_armed = Some(now);
}
}
was_hidden
}
fn hide_cursor(state: &mut State, seat: &Seat<State>) {
if let Some(ptr) = seat.get_pointer()
&& ptr.is_grabbed()
{
return;
}
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
{
let mut inner = cursor_state.lock().unwrap();
inner.hidden = true;
inner.idle_timer = None;
inner.last_armed = None;
}
let outputs: Vec<_> = state.common.shell.read().outputs().cloned().collect();
for output in outputs {
state.backend.schedule_render(&output);
}
}

View file

@ -947,6 +947,25 @@ fn config_changed(config: cosmic_config::Config, keys: Vec<String>, state: &mut
}
}
}
"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;
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);
}
if needs_render {
let outputs: Vec<_> =
state.common.shell.read().outputs().cloned().collect();
for output in outputs {
state.backend.schedule_render(&output);
}
}
}
}
_ => {}
}
}

View file

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::{
backend::render::ElementFilter,
backend::render::{ElementFilter, cursor::notify_cursor_activity},
config::{
Action, Config, PrivateAction,
key_bindings::{
@ -37,11 +37,12 @@ use cosmic_settings_config::shortcuts;
use cosmic_settings_config::shortcuts::action::{Direction, ResizeDirection};
use smithay::{
backend::input::{
AbsolutePositionEvent, Axis, AxisSource, Device, DeviceCapability, GestureBeginEvent,
GestureEndEvent, GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _, InputBackend,
InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent, ProximityState, Switch,
SwitchState, SwitchToggleEvent, TabletToolButtonEvent, TabletToolEvent,
TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent,
AbsolutePositionEvent, Axis, AxisRelativeDirection, AxisSource, Device, DeviceCapability,
GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _,
GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState, KeyboardKeyEvent,
PointerAxisEvent, ProximityState, Switch, SwitchState, SwitchToggleEvent,
TabletToolButtonEvent, TabletToolEvent, TabletToolProximityEvent, TabletToolTipEvent,
TabletToolTipState, TouchEvent,
},
desktop::{PopupKeyboardGrab, WindowSurfaceType, utils::under_from_surface_tree},
input::{
@ -308,6 +309,7 @@ impl State {
let shell = self.common.shell.write();
if let Some(seat) = shell.seats.for_device(&event.device()).cloned() {
self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
let current_output = seat.active_output();
let mut position = seat.get_pointer().unwrap().current_location().as_global();
@ -621,6 +623,7 @@ impl State {
.cloned();
if let Some(seat) = maybe_seat {
self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
let output = seat.active_output();
let geometry = output.geometry();
let position = geometry.loc.to_f64()
@ -687,6 +690,7 @@ impl State {
return;
};
self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
let current_focus = seat.get_keyboard().unwrap().current_focus();
let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| {
@ -898,6 +902,7 @@ impl State {
.cloned();
if let Some(seat) = maybe_seat {
self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
if seat.get_keyboard().unwrap().modifier_state().logo
&& self
@ -914,6 +919,12 @@ impl State {
.or_else(|| event.amount(Axis::Vertical))
.map(|val| val * scroll_factor)
{
if event.relative_direction(Axis::Vertical)
== AxisRelativeDirection::Inverted
{
percentage *= -1.;
}
if event.source() == AxisSource::Wheel {
percentage *= 5.;
}
@ -1360,6 +1371,7 @@ impl State {
let shell = self.common.shell.write();
if let Some(seat) = shell.seats.for_device(&event.device()).cloned() {
self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
let Some(output) =
mapped_output_for_device(&self.common.config, &shell, &event.device())
.cloned()
@ -1425,6 +1437,7 @@ impl State {
let shell = self.common.shell.write();
if let Some(seat) = shell.seats.for_device(&event.device()).cloned() {
self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
let Some(output) =
mapped_output_for_device(&self.common.config, &shell, &event.device())
.cloned()
@ -1486,6 +1499,7 @@ impl State {
.cloned();
if let Some(seat) = maybe_seat {
self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
if let Some(tool) = seat.tablet_seat().get_tool(&event.tool()) {
match event.tip_state() {
TabletToolTipState::Down => {
@ -1508,6 +1522,7 @@ impl State {
.cloned();
if let Some(seat) = maybe_seat {
self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
if let Some(tool) = seat.tablet_seat().get_tool(&event.tool()) {
tool.button(
event.button(),

View file

@ -9,7 +9,7 @@ use cosmic::{
};
use cosmic_comp_config::ZoomMovement;
use cosmic_config::ConfigSet;
use keyframe::{ease, functions::EaseInOutCubic};
use keyframe::{ease, functions::Linear};
use smithay::{
backend::renderer::{ImportMem, Renderer, element::AsRenderElements},
desktop::space::SpaceElement,
@ -135,7 +135,7 @@ impl OutputZoomState {
let percentage =
duration_since.as_millis() as f32 / ANIMATION_DURATION.as_millis() as f32;
ease(
EaseInOutCubic,
Linear,
EasePoint(*old_point),
EasePoint(self.focal_point),
percentage,
@ -159,7 +159,7 @@ impl OutputZoomState {
let percentage = Instant::now().duration_since(*start).as_millis() as f32
/ ANIMATION_DURATION.as_millis() as f32;
ease(EaseInOutCubic, *old_level, self.level, percentage)
ease(Linear, *old_level, self.level, percentage)
} else {
self.level
}

View file

@ -809,12 +809,14 @@ impl XwmHandler for State {
);
}
if !shell.pending_windows.iter().any(|w| w.surface == window) {
let fullscreen = window.is_fullscreen().then(|| seat.active_output());
let maximized = window.is_maximized();
let surface = CosmicSurface::from(window);
shell.pending_windows.push(PendingWindow {
surface,
seat,
fullscreen: None,
maximized: false,
fullscreen,
maximized,
sticky: false,
})
}
@ -914,7 +916,11 @@ impl XwmHandler for State {
// We only allow floating X11 windows to resize themselves. Nothing else
let shell = self.common.shell.read();
// TODO: Fullscreen
if window.is_fullscreen() {
let _ = window.configure(None);
return;
}
if let Some(mapped) = shell
.element_for_surface(&window)
.filter(|mapped| !mapped.is_minimized())