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", "libdisplay-info",
"output", "output",
] } ] }
cosmic-config = { git = "https://github.com/pop-os/libcosmic", features = [ cosmic-config = { path = "../libcosmic/cosmic-config", features = [
"calloop", "calloop",
"macro", "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", "server",
] } ] }
cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon" } cosmic-settings-config = { path = "../cosmic-settings-daemon/config" }
cosmic-settings-daemon-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", features = [ cosmic-settings-daemon-config = { path = "../cosmic-settings-daemon/cosmic-settings-daemon-config", features = [
"greeter", "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", "shape-run-cache",
] } ] }
libdisplay-info = "0.3.0" libdisplay-info = "0.3.0"
@ -39,10 +39,14 @@ i18n-embed = { version = "0.16", features = [
"desktop-requester", "desktop-requester",
] } ] }
i18n-embed-fl = "0.10" 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" indexmap = "2.13"
keyframe = "1.1.1" 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 } libsystemd = { version = "0.7", optional = true }
log-panics = { version = "2", features = ["with-backtrace"] } log-panics = { version = "2", features = ["with-backtrace"] }
ordered-float = "5.1" ordered-float = "5.1"
@ -138,9 +142,36 @@ inherits = "release"
[profile.release] [profile.release]
lto = "fat" lto = "fat"
[patch."https://github.com/pop-os/cosmic-protocols"] [patch.'https://github.com/pop-os/libcosmic']
cosmic-protocols = { git = "https://github.com/pop-os//cosmic-protocols", branch = "main" } cosmic-config = { path = "../libcosmic/cosmic-config" }
cosmic-client-toolkit = { git = "https://github.com/pop-os//cosmic-protocols", branch = "main" } 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] [patch.crates-io]
smithay = { git = "https://github.com/smithay/smithay.git", rev = "774f2ab" } smithay = { git = "https://github.com/smithay/smithay.git", rev = "774f2ab" }

View file

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

View file

@ -99,6 +99,8 @@ pub struct CosmicCompConfig {
pub edge_snap_threshold: u32, pub edge_snap_threshold: u32,
pub accessibility_zoom: ZoomConfig, pub accessibility_zoom: ZoomConfig,
pub appearance_settings: AppearanceConfig, pub appearance_settings: AppearanceConfig,
/// Hide the cursor after this many seconds of pointer inactivity (None disables)
pub cursor_hide_timeout: Option<u32>,
} }
impl Default for CosmicCompConfig { impl Default for CosmicCompConfig {
@ -135,6 +137,7 @@ impl Default for CosmicCompConfig {
edge_snap_threshold: 0, edge_snap_threshold: 0,
accessibility_zoom: ZoomConfig::default(), accessibility_zoom: ZoomConfig::default(),
appearance_settings: AppearanceConfig::default(), appearance_settings: AppearanceConfig::default(),
cursor_hide_timeout: None,
} }
} }
} }

View file

@ -28,7 +28,7 @@ use smithay::{
}, },
egl::{EGLContext, EGLDevice, EGLDisplay, context::ContextPriority}, egl::{EGLContext, EGLDevice, EGLDisplay, context::ContextPriority},
renderer::glow::GlowRenderer, renderer::glow::GlowRenderer,
session::Session, session::{Session, libseat::LibSeatSession},
}, },
desktop::utils::OutputPresentationFeedback, desktop::utils::OutputPresentationFeedback,
output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel}, output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel},
@ -40,7 +40,10 @@ use smithay::{
wayland_server::DisplayHandle, wayland_server::DisplayHandle,
}, },
utils::{Clock, DevPath, DeviceFd, Monotonic, Point, Transform}, 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 tracing::{error, info, warn};
use wayland_backend::server::ClientId; use wayland_backend::server::ClientId;
@ -99,6 +102,20 @@ pub struct Device {
pub socket: Option<Socket>, 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)] #[derive(Debug)]
pub struct LockedDevice<'a> { pub struct LockedDevice<'a> {
pub inner: &'a mut InnerDevice, pub inner: &'a mut InnerDevice,
@ -222,138 +239,14 @@ impl State {
} }
} }
let fd = DrmDeviceFd::new(DeviceFd::from( let mut device = Device::new(
self.backend dev,
.kms() path,
.session &mut self.backend.kms().session,
.open( &mut self.common,
path, dh,
OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK, None,
) )?;
.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 connectors = device.enumerate_surfaces()?.added; // There are no removed outputs on newly added devices let connectors = device.enumerate_surfaces()?.added; // There are no removed outputs on newly added devices
let mut wl_outputs = Vec::new(); let mut wl_outputs = Vec::new();
let mut w = self.common.shell.read().global_space().size.w as u32; 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 // TODO atomic commit all surfaces together and drop surfaces, if it fails due to bandwidth
let kms = self.backend.kms(); let kms = self.backend.kms();
kms.drm_devices.insert(drm_node, device); kms.drm_devices.insert(device.inner.dev_node, device);
} }
self.common self.common
@ -489,6 +382,218 @@ impl State {
Ok(outputs_added) 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<()> { pub fn device_removed(&mut self, dev: dev_t, dh: &DisplayHandle) -> Result<()> {
let backend = self.backend.kms(); let backend = self.backend.kms();
// we can't use DrmNode::from_node_id, because that assumes the node is still on sysfs // 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 { 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> { pub fn enumerate_surfaces(&mut self) -> Result<OutputChanges> {
// enumerate our outputs // enumerate our outputs
let config = let config =
@ -634,6 +891,27 @@ impl Device {
drm: self.drm.lock(), 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<'_> { impl LockedDevice<'_> {

View file

@ -394,15 +394,37 @@ impl State {
continue; continue;
} }
}; };
let dh = state.common.display_handle.clone();
if state.backend.kms().drm_devices.contains_key(&drm_node) { if state.backend.kms().drm_devices.contains_key(&drm_node) {
match state.device_changed(dev) { match state.device_changed(dev) {
Ok(outputs) => added.extend(outputs), Ok(outputs) => added.extend(outputs),
Err(err) => { 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 { } else {
let dh = state.common.display_handle.clone();
match state.device_added(dev, path, &dh) { match state.device_added(dev, path, &dh) {
Ok(outputs) => added.extend(outputs), Ok(outputs) => added.extend(outputs),
Err(err) => error!(?err, "Failed to add drm device {}.", path.display(),), Err(err) => error!(?err, "Failed to add drm device {}.", path.display(),),

View file

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

View file

@ -17,14 +17,25 @@ use smithay::{
Seat, Seat,
pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus}, pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus},
}, },
reexports::wayland_server::protocol::wl_surface, reexports::{
calloop::{
RegistrationToken,
timer::{TimeoutAction, Timer},
},
wayland_server::protocol::wl_surface,
},
render_elements, render_elements,
utils::{ utils::{
Buffer as BufferCoords, Logical, Monotonic, Physical, Point, Scale, Size, Time, Transform, Buffer as BufferCoords, Logical, Monotonic, Physical, Point, Scale, Size, Time, Transform,
}, },
wayland::compositor::{get_role, with_states}, 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 tracing::warn;
use xcursor::{ use xcursor::{
CursorTheme, CursorTheme,
@ -196,6 +207,10 @@ pub struct CursorStateInner {
cursors: HashMap<CursorIcon, Cursor>, cursors: HashMap<CursorIcon, Cursor>,
current_image: Option<Image>, current_image: Option<Image>,
image_cache: Vec<(Image, MemoryRenderBuffer)>, image_cache: Vec<(Image, MemoryRenderBuffer)>,
hidden: bool,
idle_timer: Option<RegistrationToken>,
last_armed: Option<Instant>,
} }
impl CursorStateInner { impl CursorStateInner {
@ -246,6 +261,10 @@ impl Default for CursorStateInner {
cursors: HashMap::new(), cursors: HashMap::new(),
current_image: None, current_image: None,
image_cache: Vec::new(), 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 mut state_ref = seat_userdata.get::<CursorState>().unwrap().lock().unwrap();
let state = &mut *state_ref; let state = &mut *state_ref;
if state.hidden {
return Vec::new();
}
let named_cursor = state.current_cursor.or(match cursor_status { let named_cursor = state.current_cursor.or(match cursor_status {
CursorImageStatus::Named(named_cursor) => Some(named_cursor), CursorImageStatus::Named(named_cursor) => Some(named_cursor),
_ => None, _ => None,
@ -335,3 +358,71 @@ where
Vec::new() 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 // SPDX-License-Identifier: GPL-3.0-only
use crate::{ use crate::{
backend::render::ElementFilter, backend::render::{ElementFilter, cursor::notify_cursor_activity},
config::{ config::{
Action, Config, PrivateAction, Action, Config, PrivateAction,
key_bindings::{ key_bindings::{
@ -37,11 +37,12 @@ use cosmic_settings_config::shortcuts;
use cosmic_settings_config::shortcuts::action::{Direction, ResizeDirection}; use cosmic_settings_config::shortcuts::action::{Direction, ResizeDirection};
use smithay::{ use smithay::{
backend::input::{ backend::input::{
AbsolutePositionEvent, Axis, AxisSource, Device, DeviceCapability, GestureBeginEvent, AbsolutePositionEvent, Axis, AxisRelativeDirection, AxisSource, Device, DeviceCapability,
GestureEndEvent, GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _, InputBackend, GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _,
InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent, ProximityState, Switch, GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState, KeyboardKeyEvent,
SwitchState, SwitchToggleEvent, TabletToolButtonEvent, TabletToolEvent, PointerAxisEvent, ProximityState, Switch, SwitchState, SwitchToggleEvent,
TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent, TabletToolButtonEvent, TabletToolEvent, TabletToolProximityEvent, TabletToolTipEvent,
TabletToolTipState, TouchEvent,
}, },
desktop::{PopupKeyboardGrab, WindowSurfaceType, utils::under_from_surface_tree}, desktop::{PopupKeyboardGrab, WindowSurfaceType, utils::under_from_surface_tree},
input::{ input::{
@ -308,6 +309,7 @@ impl State {
let shell = self.common.shell.write(); let shell = self.common.shell.write();
if let Some(seat) = shell.seats.for_device(&event.device()).cloned() { if let Some(seat) = shell.seats.for_device(&event.device()).cloned() {
self.common.idle_notifier_state.notify_activity(&seat); self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
let current_output = seat.active_output(); let current_output = seat.active_output();
let mut position = seat.get_pointer().unwrap().current_location().as_global(); let mut position = seat.get_pointer().unwrap().current_location().as_global();
@ -621,6 +623,7 @@ impl State {
.cloned(); .cloned();
if let Some(seat) = maybe_seat { if let Some(seat) = maybe_seat {
self.common.idle_notifier_state.notify_activity(&seat); self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
let output = seat.active_output(); let output = seat.active_output();
let geometry = output.geometry(); let geometry = output.geometry();
let position = geometry.loc.to_f64() let position = geometry.loc.to_f64()
@ -687,6 +690,7 @@ impl State {
return; return;
}; };
self.common.idle_notifier_state.notify_activity(&seat); self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
let current_focus = seat.get_keyboard().unwrap().current_focus(); let current_focus = seat.get_keyboard().unwrap().current_focus();
let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| { let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| {
@ -898,6 +902,7 @@ impl State {
.cloned(); .cloned();
if let Some(seat) = maybe_seat { if let Some(seat) = maybe_seat {
self.common.idle_notifier_state.notify_activity(&seat); self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
if seat.get_keyboard().unwrap().modifier_state().logo if seat.get_keyboard().unwrap().modifier_state().logo
&& self && self
@ -914,6 +919,12 @@ impl State {
.or_else(|| event.amount(Axis::Vertical)) .or_else(|| event.amount(Axis::Vertical))
.map(|val| val * scroll_factor) .map(|val| val * scroll_factor)
{ {
if event.relative_direction(Axis::Vertical)
== AxisRelativeDirection::Inverted
{
percentage *= -1.;
}
if event.source() == AxisSource::Wheel { if event.source() == AxisSource::Wheel {
percentage *= 5.; percentage *= 5.;
} }
@ -1360,6 +1371,7 @@ impl State {
let shell = self.common.shell.write(); let shell = self.common.shell.write();
if let Some(seat) = shell.seats.for_device(&event.device()).cloned() { if let Some(seat) = shell.seats.for_device(&event.device()).cloned() {
self.common.idle_notifier_state.notify_activity(&seat); self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
let Some(output) = let Some(output) =
mapped_output_for_device(&self.common.config, &shell, &event.device()) mapped_output_for_device(&self.common.config, &shell, &event.device())
.cloned() .cloned()
@ -1425,6 +1437,7 @@ impl State {
let shell = self.common.shell.write(); let shell = self.common.shell.write();
if let Some(seat) = shell.seats.for_device(&event.device()).cloned() { if let Some(seat) = shell.seats.for_device(&event.device()).cloned() {
self.common.idle_notifier_state.notify_activity(&seat); self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
let Some(output) = let Some(output) =
mapped_output_for_device(&self.common.config, &shell, &event.device()) mapped_output_for_device(&self.common.config, &shell, &event.device())
.cloned() .cloned()
@ -1486,6 +1499,7 @@ impl State {
.cloned(); .cloned();
if let Some(seat) = maybe_seat { if let Some(seat) = maybe_seat {
self.common.idle_notifier_state.notify_activity(&seat); self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
if let Some(tool) = seat.tablet_seat().get_tool(&event.tool()) { if let Some(tool) = seat.tablet_seat().get_tool(&event.tool()) {
match event.tip_state() { match event.tip_state() {
TabletToolTipState::Down => { TabletToolTipState::Down => {
@ -1508,6 +1522,7 @@ impl State {
.cloned(); .cloned();
if let Some(seat) = maybe_seat { if let Some(seat) = maybe_seat {
self.common.idle_notifier_state.notify_activity(&seat); self.common.idle_notifier_state.notify_activity(&seat);
notify_cursor_activity(self, &seat);
if let Some(tool) = seat.tablet_seat().get_tool(&event.tool()) { if let Some(tool) = seat.tablet_seat().get_tool(&event.tool()) {
tool.button( tool.button(
event.button(), event.button(),

View file

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

View file

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