2022-02-01 13:59:39 +01:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
|
|
2026-03-04 18:16:48 +01:00
|
|
|
use crate::{
|
|
|
|
|
backend::render::{
|
|
|
|
|
element::AsGlowRenderer,
|
2026-03-04 18:57:37 +01:00
|
|
|
wayland::{SurfaceRenderElement, push_render_elements_from_surface_tree},
|
2026-03-04 18:16:48 +01:00
|
|
|
},
|
|
|
|
|
utils::prelude::*,
|
|
|
|
|
wayland::handlers::compositor::FRAME_TIME_FILTER,
|
|
|
|
|
};
|
2022-02-01 13:59:39 +01:00
|
|
|
use smithay::{
|
2023-04-18 17:10:21 +02:00
|
|
|
backend::{
|
|
|
|
|
allocator::Fourcc,
|
|
|
|
|
renderer::{
|
2025-10-16 18:53:57 +02:00
|
|
|
ImportAll, ImportMem, Renderer,
|
2023-04-18 17:10:21 +02:00
|
|
|
element::{
|
2023-09-13 20:24:11 -07:00
|
|
|
Kind,
|
2025-10-16 18:53:57 +02:00
|
|
|
memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement},
|
2023-04-18 17:10:21 +02:00
|
|
|
},
|
2022-09-28 12:01:29 +02:00
|
|
|
},
|
|
|
|
|
},
|
2026-03-04 18:16:48 +01:00
|
|
|
desktop::utils::bbox_from_surface_tree,
|
2022-08-31 13:01:23 +02:00
|
|
|
input::{
|
|
|
|
|
Seat,
|
2025-10-16 18:53:57 +02:00
|
|
|
pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus},
|
2022-08-31 13:01:23 +02:00
|
|
|
},
|
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-20 23:52:45 +02:00
|
|
|
reexports::{
|
|
|
|
|
calloop::{
|
|
|
|
|
RegistrationToken,
|
|
|
|
|
timer::{TimeoutAction, Timer},
|
|
|
|
|
},
|
|
|
|
|
wayland_server::protocol::wl_surface,
|
|
|
|
|
},
|
2022-09-28 12:01:29 +02:00
|
|
|
render_elements,
|
2024-03-12 19:42:48 +01:00
|
|
|
utils::{
|
2025-06-06 15:33:58 -07:00
|
|
|
Buffer as BufferCoords, Logical, Monotonic, Physical, Point, Scale, Size, Time, Transform,
|
2024-03-12 19:42:48 +01:00
|
|
|
},
|
2022-08-31 13:01:23 +02:00
|
|
|
wayland::compositor::{get_role, with_states},
|
2022-02-01 13:59:39 +01:00
|
|
|
};
|
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-20 23:52:45 +02:00
|
|
|
use std::{
|
|
|
|
|
collections::HashMap,
|
|
|
|
|
io::Read,
|
|
|
|
|
sync::Mutex,
|
|
|
|
|
time::{Duration, Instant},
|
|
|
|
|
};
|
2023-02-24 17:41:52 +01:00
|
|
|
use tracing::warn;
|
2022-02-01 13:59:39 +01:00
|
|
|
use xcursor::{
|
|
|
|
|
CursorTheme,
|
2025-10-16 18:53:57 +02:00
|
|
|
parser::{Image, parse_xcursor},
|
2022-02-01 13:59:39 +01:00
|
|
|
};
|
|
|
|
|
|
2022-02-04 21:04:17 +01:00
|
|
|
static FALLBACK_CURSOR_DATA: &[u8] = include_bytes!("../../../resources/cursor.rgba");
|
2022-02-01 13:59:39 +01:00
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct Cursor {
|
|
|
|
|
icons: Vec<Image>,
|
|
|
|
|
size: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Cursor {
|
2024-09-09 16:21:27 +02:00
|
|
|
pub fn load(theme: &CursorTheme, shape: CursorIcon, size: u32) -> Cursor {
|
|
|
|
|
let icons = load_icon(theme, shape)
|
2023-02-24 17:41:52 +01:00
|
|
|
.map_err(|err| warn!(?err, "Unable to load xcursor, using fallback cursor"))
|
2024-09-09 16:21:27 +02:00
|
|
|
.or_else(|_| load_icon(theme, CursorIcon::Default))
|
2022-02-01 13:59:39 +01:00
|
|
|
.unwrap_or_else(|_| {
|
|
|
|
|
vec![Image {
|
|
|
|
|
size: 32,
|
|
|
|
|
width: 64,
|
|
|
|
|
height: 64,
|
|
|
|
|
xhot: 1,
|
|
|
|
|
yhot: 1,
|
|
|
|
|
delay: 1,
|
|
|
|
|
pixels_rgba: Vec::from(FALLBACK_CURSOR_DATA),
|
|
|
|
|
pixels_argb: vec![], //unused
|
|
|
|
|
}]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Cursor { icons, size }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_image(&self, scale: u32, millis: u32) -> Image {
|
|
|
|
|
let size = self.size * scale;
|
|
|
|
|
frame(millis, size, &self.icons)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn nearest_images(size: u32, images: &[Image]) -> impl Iterator<Item = &Image> {
|
|
|
|
|
// Follow the nominal size of the cursor to choose the nearest
|
|
|
|
|
let nearest_image = images
|
|
|
|
|
.iter()
|
2024-09-09 16:21:27 +02:00
|
|
|
.min_by_key(|image| u32::abs_diff(size, image.size))
|
2022-02-01 13:59:39 +01:00
|
|
|
.unwrap();
|
|
|
|
|
|
2022-02-04 21:04:17 +01:00
|
|
|
images.iter().filter(move |image| {
|
|
|
|
|
image.width == nearest_image.width && image.height == nearest_image.height
|
|
|
|
|
})
|
2022-02-01 13:59:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn frame(mut millis: u32, size: u32, images: &[Image]) -> Image {
|
|
|
|
|
let total = nearest_images(size, images).fold(0, |acc, image| acc + image.delay);
|
2024-06-27 16:41:38 +02:00
|
|
|
|
|
|
|
|
if total == 0 {
|
|
|
|
|
millis = 0;
|
|
|
|
|
} else {
|
|
|
|
|
millis %= total;
|
|
|
|
|
}
|
2022-02-01 13:59:39 +01:00
|
|
|
|
|
|
|
|
for img in nearest_images(size, images) {
|
2024-06-27 16:41:38 +02:00
|
|
|
if millis <= img.delay {
|
2022-02-01 13:59:39 +01:00
|
|
|
return img.clone();
|
|
|
|
|
}
|
|
|
|
|
millis -= img.delay;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unreachable!()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
|
enum Error {
|
|
|
|
|
#[error("Theme has no default cursor")]
|
|
|
|
|
NoDefaultCursor,
|
|
|
|
|
#[error("Error opening xcursor file: {0}")]
|
|
|
|
|
File(#[from] std::io::Error),
|
2026-07-06 08:43:27 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn cursor_aliases(name: &str) -> &[&str] {
|
|
|
|
|
match name {
|
|
|
|
|
"default" => &["default", "left_ptr", "arrow"],
|
|
|
|
|
"pointer" => &["pointer", "hand2", "hand"],
|
|
|
|
|
"text" => &["text", "xterm"],
|
|
|
|
|
"wait" => &["wait", "watch"],
|
|
|
|
|
"progress" => &["progress", "left_ptr_watch"],
|
|
|
|
|
|
|
|
|
|
"ew-resize" => &["ew-resize", "h_double_arrow", "sb_h_double_arrow"],
|
|
|
|
|
"ns-resize" => &["ns-resize", "v_double_arrow", "sb_v_double_arrow"],
|
|
|
|
|
"nw-resize" => &["nw-resize", "top_left_corner"],
|
|
|
|
|
"ne-resize" => &["ne-resize", "top_right_corner"],
|
|
|
|
|
"sw-resize" => &["sw-resize", "bottom_left_corner"],
|
|
|
|
|
"se-resize" => &["se-resize", "bottom_right_corner"],
|
|
|
|
|
|
|
|
|
|
"w-resize" => &["w-resize", "left_side"],
|
|
|
|
|
"e-resize" => &["e-resize", "right_side"],
|
|
|
|
|
"n-resize" => &["n-resize", "top_side"],
|
|
|
|
|
"s-resize" => &["s-resize", "bottom_side"],
|
|
|
|
|
|
|
|
|
|
"move" => &["move", "fleur"],
|
|
|
|
|
"not-allowed" => &["not-allowed", "crossed_circle"],
|
|
|
|
|
"crosshair" => &["crosshair", "cross"],
|
|
|
|
|
"help" => &["help", "question_arrow", "left_ptr_help"],
|
|
|
|
|
|
|
|
|
|
_ => &[],
|
|
|
|
|
}
|
2022-02-01 13:59:39 +01:00
|
|
|
}
|
|
|
|
|
|
2024-09-09 16:21:27 +02:00
|
|
|
fn load_icon(theme: &CursorTheme, shape: CursorIcon) -> Result<Vec<Image>, Error> {
|
2026-07-06 08:43:27 +05:30
|
|
|
let shape_name = shape.to_string();
|
|
|
|
|
|
|
|
|
|
for name in cursor_aliases(&shape_name)
|
|
|
|
|
.iter()
|
|
|
|
|
.copied()
|
|
|
|
|
.chain(std::iter::once(shape_name.as_str()))
|
|
|
|
|
{
|
|
|
|
|
if let Some(icon_path) = theme.load_icon(name) {
|
|
|
|
|
let mut cursor_file = std::fs::File::open(&icon_path)?;
|
|
|
|
|
let mut cursor_data = Vec::new();
|
|
|
|
|
cursor_file.read_to_end(&mut cursor_data)?;
|
|
|
|
|
|
|
|
|
|
if let Some(images) = parse_xcursor(&cursor_data) {
|
|
|
|
|
return Ok(images);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Err(Error::NoDefaultCursor)
|
2022-02-01 13:59:39 +01:00
|
|
|
}
|
|
|
|
|
|
2022-09-28 12:01:29 +02:00
|
|
|
render_elements! {
|
2026-03-04 18:16:48 +01:00
|
|
|
pub CursorRenderElement<R> where R: ImportAll + ImportMem + AsGlowRenderer;
|
2024-02-12 20:10:44 +01:00
|
|
|
Static=MemoryRenderBufferRenderElement<R>,
|
2026-03-04 18:16:48 +01:00
|
|
|
Surface=SurfaceRenderElement<R>,
|
2022-09-28 12:01:29 +02:00
|
|
|
}
|
|
|
|
|
|
2022-11-28 17:48:50 +01:00
|
|
|
pub fn draw_surface_cursor<R>(
|
|
|
|
|
renderer: &mut R,
|
2022-09-28 12:01:29 +02:00
|
|
|
surface: &wl_surface::WlSurface,
|
2025-04-16 19:36:15 +02:00
|
|
|
location: Point<f64, Logical>,
|
2022-09-28 12:01:29 +02:00
|
|
|
scale: impl Into<Scale<f64>>,
|
2026-03-04 18:57:37 +01:00
|
|
|
push: &mut dyn FnMut(CursorRenderElement<R>, Point<i32, Physical>),
|
|
|
|
|
) where
|
2026-03-04 18:16:48 +01:00
|
|
|
R: Renderer + ImportAll + AsGlowRenderer,
|
2025-03-11 19:14:49 +01:00
|
|
|
R::TextureId: Clone + 'static,
|
2022-02-01 13:59:39 +01:00
|
|
|
{
|
2022-09-28 12:01:29 +02:00
|
|
|
let scale = scale.into();
|
2025-10-16 13:50:32 +02:00
|
|
|
let h = with_states(surface, |states| {
|
2022-07-04 15:29:31 +02:00
|
|
|
states
|
|
|
|
|
.data_map
|
|
|
|
|
.get::<Mutex<CursorImageAttributes>>()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.hotspot
|
2025-04-16 19:35:30 +02:00
|
|
|
.to_physical_precise_round(scale)
|
2022-07-04 15:29:31 +02:00
|
|
|
});
|
2022-09-28 12:01:29 +02:00
|
|
|
|
2026-03-04 18:57:37 +01:00
|
|
|
push_render_elements_from_surface_tree(
|
2022-11-28 17:48:50 +01:00
|
|
|
renderer,
|
|
|
|
|
surface,
|
2025-04-16 19:36:15 +02:00
|
|
|
location.to_physical(scale).to_i32_round(),
|
2026-03-04 18:16:48 +01:00
|
|
|
bbox_from_surface_tree(surface, location.to_i32_round()).to_f64(),
|
2022-11-28 17:48:50 +01:00
|
|
|
scale,
|
2023-05-12 20:01:37 +02:00
|
|
|
1.0,
|
2026-03-04 18:16:48 +01:00
|
|
|
false,
|
|
|
|
|
[0; 4],
|
2023-09-13 20:24:11 -07:00
|
|
|
Kind::Cursor,
|
2026-03-04 18:57:37 +01:00
|
|
|
&mut |elem| push(elem.into(), h),
|
|
|
|
|
None,
|
|
|
|
|
);
|
2022-02-01 13:59:39 +01:00
|
|
|
}
|
|
|
|
|
|
2023-10-07 19:15:44 -07:00
|
|
|
#[profiling::function]
|
2022-11-28 17:48:50 +01:00
|
|
|
pub fn draw_dnd_icon<R>(
|
|
|
|
|
renderer: &mut R,
|
2022-09-28 12:01:29 +02:00
|
|
|
surface: &wl_surface::WlSurface,
|
2025-04-16 19:36:15 +02:00
|
|
|
location: Point<f64, Logical>,
|
2022-09-28 12:01:29 +02:00
|
|
|
scale: impl Into<Scale<f64>>,
|
2026-03-04 18:57:37 +01:00
|
|
|
push: &mut dyn FnMut(SurfaceRenderElement<R>),
|
|
|
|
|
) where
|
2026-03-04 18:16:48 +01:00
|
|
|
R: Renderer + ImportAll + AsGlowRenderer,
|
2025-03-11 19:14:49 +01:00
|
|
|
R::TextureId: Clone + 'static,
|
2022-11-28 17:48:50 +01:00
|
|
|
{
|
2025-10-16 13:50:32 +02:00
|
|
|
if get_role(surface) != Some("dnd_icon") {
|
2023-02-24 17:41:52 +01:00
|
|
|
warn!(
|
|
|
|
|
?surface,
|
2022-02-04 21:04:17 +01:00
|
|
|
"Trying to display as a dnd icon a surface that does not have the DndIcon role."
|
|
|
|
|
);
|
2022-02-01 13:59:39 +01:00
|
|
|
}
|
2022-09-28 12:01:29 +02:00
|
|
|
let scale = scale.into();
|
2026-03-04 18:57:37 +01:00
|
|
|
push_render_elements_from_surface_tree(
|
2022-11-28 17:48:50 +01:00
|
|
|
renderer,
|
2022-02-01 13:59:39 +01:00
|
|
|
surface,
|
2025-04-16 19:36:15 +02:00
|
|
|
location.to_physical(scale).to_i32_round(),
|
2026-03-04 18:16:48 +01:00
|
|
|
bbox_from_surface_tree(surface, location.to_i32_round()).to_f64(),
|
2022-09-28 12:01:29 +02:00
|
|
|
scale,
|
2023-05-12 20:01:37 +02:00
|
|
|
1.0,
|
2026-03-04 18:16:48 +01:00
|
|
|
false,
|
|
|
|
|
[0; 4],
|
2025-07-22 16:31:56 +02:00
|
|
|
FRAME_TIME_FILTER,
|
2026-03-04 18:57:37 +01:00
|
|
|
push,
|
|
|
|
|
None,
|
|
|
|
|
);
|
2022-02-01 13:59:39 +01:00
|
|
|
}
|
|
|
|
|
|
2024-06-07 19:26:23 +02:00
|
|
|
pub type CursorState = Mutex<CursorStateInner>;
|
|
|
|
|
pub struct CursorStateInner {
|
2024-09-09 16:21:27 +02:00
|
|
|
current_cursor: Option<CursorIcon>,
|
|
|
|
|
|
|
|
|
|
cursor_theme: CursorTheme,
|
|
|
|
|
cursor_size: u32,
|
|
|
|
|
|
|
|
|
|
cursors: HashMap<CursorIcon, Cursor>,
|
2024-06-07 19:26:23 +02:00
|
|
|
current_image: Option<Image>,
|
2024-06-17 15:13:01 +02:00
|
|
|
image_cache: Vec<(Image, MemoryRenderBuffer)>,
|
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-20 23:52:45 +02:00
|
|
|
|
|
|
|
|
hidden: bool,
|
|
|
|
|
idle_timer: Option<RegistrationToken>,
|
|
|
|
|
last_armed: Option<Instant>,
|
2022-02-01 13:59:39 +01:00
|
|
|
}
|
|
|
|
|
|
2024-06-07 19:26:23 +02:00
|
|
|
impl CursorStateInner {
|
2024-09-09 16:21:27 +02:00
|
|
|
pub fn set_shape(&mut self, shape: CursorIcon) {
|
|
|
|
|
self.current_cursor = Some(shape);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn unset_shape(&mut self) {
|
|
|
|
|
self.current_cursor = None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_named_cursor(&mut self, shape: CursorIcon) -> &Cursor {
|
|
|
|
|
self.cursors
|
|
|
|
|
.entry(shape)
|
|
|
|
|
.or_insert_with(|| Cursor::load(&self.cursor_theme, shape, self.cursor_size))
|
2023-07-11 16:33:23 +02:00
|
|
|
}
|
2025-01-20 18:25:01 +01:00
|
|
|
|
|
|
|
|
pub fn size(&self) -> u32 {
|
|
|
|
|
self.cursor_size
|
|
|
|
|
}
|
2023-07-11 16:33:23 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-16 19:36:39 +02:00
|
|
|
pub fn load_cursor_env() -> (String, u32) {
|
2023-07-11 16:33:23 +02:00
|
|
|
let name = std::env::var("XCURSOR_THEME")
|
|
|
|
|
.ok()
|
|
|
|
|
.unwrap_or_else(|| "default".into());
|
|
|
|
|
let size = std::env::var("XCURSOR_SIZE")
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|s| s.parse().ok())
|
|
|
|
|
.unwrap_or(24);
|
2025-04-16 19:36:39 +02:00
|
|
|
(name, size)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn load_cursor_theme() -> (CursorTheme, u32) {
|
|
|
|
|
let (name, size) = load_cursor_env();
|
2023-07-11 16:33:23 +02:00
|
|
|
(CursorTheme::load(&name), size)
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-07 19:26:23 +02:00
|
|
|
impl Default for CursorStateInner {
|
|
|
|
|
fn default() -> CursorStateInner {
|
2023-07-11 16:33:23 +02:00
|
|
|
let (theme, size) = load_cursor_theme();
|
2024-06-07 19:26:23 +02:00
|
|
|
CursorStateInner {
|
2024-09-09 16:21:27 +02:00
|
|
|
current_cursor: None,
|
|
|
|
|
|
|
|
|
|
cursor_size: size,
|
|
|
|
|
cursor_theme: theme,
|
|
|
|
|
|
|
|
|
|
cursors: HashMap::new(),
|
2024-06-07 19:26:23 +02:00
|
|
|
current_image: None,
|
2024-06-17 15:13:01 +02:00
|
|
|
image_cache: Vec::new(),
|
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-20 23:52:45 +02:00
|
|
|
|
|
|
|
|
hidden: false,
|
|
|
|
|
idle_timer: None,
|
|
|
|
|
last_armed: None,
|
2022-03-16 20:05:24 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-02-01 13:59:39 +01:00
|
|
|
|
2023-10-07 19:15:44 -07:00
|
|
|
#[profiling::function]
|
2022-09-28 12:01:29 +02:00
|
|
|
pub fn draw_cursor<R>(
|
2022-03-16 20:05:24 +01:00
|
|
|
renderer: &mut R,
|
2022-07-04 15:29:31 +02:00
|
|
|
seat: &Seat<State>,
|
|
|
|
|
location: Point<f64, Logical>,
|
2022-09-28 12:01:29 +02:00
|
|
|
scale: Scale<f64>,
|
2025-01-23 15:45:00 +01:00
|
|
|
buffer_scale: f64,
|
2022-11-17 20:32:54 +01:00
|
|
|
time: Time<Monotonic>,
|
2022-02-01 13:59:39 +01:00
|
|
|
draw_default: bool,
|
2026-03-04 18:57:37 +01:00
|
|
|
push: &mut dyn FnMut(CursorRenderElement<R>, Point<i32, Physical>),
|
|
|
|
|
) where
|
2026-03-04 18:16:48 +01:00
|
|
|
R: Renderer + ImportMem + ImportAll + AsGlowRenderer,
|
2025-03-11 19:14:49 +01:00
|
|
|
R::TextureId: Send + Clone + 'static,
|
2022-03-16 20:05:24 +01:00
|
|
|
{
|
2022-02-01 13:59:39 +01:00
|
|
|
// draw the cursor as relevant
|
2025-06-06 15:33:58 -07:00
|
|
|
let cursor_status = seat.cursor_image_status();
|
2022-02-01 13:59:39 +01:00
|
|
|
|
2024-09-09 16:21:27 +02:00
|
|
|
let seat_userdata = seat.user_data();
|
|
|
|
|
let mut state_ref = seat_userdata.get::<CursorState>().unwrap().lock().unwrap();
|
|
|
|
|
let state = &mut *state_ref;
|
2022-09-28 12:01:29 +02:00
|
|
|
|
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-20 23:52:45 +02:00
|
|
|
if state.hidden {
|
2026-03-04 18:57:37 +01:00
|
|
|
return;
|
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-20 23:52:45 +02:00
|
|
|
}
|
|
|
|
|
|
2024-09-09 16:21:27 +02:00
|
|
|
let named_cursor = state.current_cursor.or(match cursor_status {
|
|
|
|
|
CursorImageStatus::Named(named_cursor) => Some(named_cursor),
|
|
|
|
|
_ => None,
|
|
|
|
|
});
|
|
|
|
|
if let Some(current_cursor) = named_cursor {
|
|
|
|
|
if !draw_default && current_cursor == CursorIcon::Default {
|
2026-03-04 18:57:37 +01:00
|
|
|
return;
|
2024-09-09 16:21:27 +02:00
|
|
|
}
|
|
|
|
|
|
2025-01-23 15:45:00 +01:00
|
|
|
let integer_scale = (scale.x.max(scale.y) * buffer_scale).ceil() as u32;
|
2024-09-09 16:21:27 +02:00
|
|
|
let frame = state
|
|
|
|
|
.get_named_cursor(current_cursor)
|
|
|
|
|
.get_image(integer_scale, time.as_millis());
|
2025-01-20 18:25:01 +01:00
|
|
|
let actual_scale = (frame.size / state.size()).max(1);
|
2022-02-01 13:59:39 +01:00
|
|
|
|
2024-06-17 15:13:01 +02:00
|
|
|
let pointer_images = &mut state.image_cache;
|
2025-10-16 18:53:57 +02:00
|
|
|
let maybe_image = pointer_images
|
|
|
|
|
.iter()
|
|
|
|
|
.find_map(|(image, texture)| if image == &frame { Some(texture) } else { None });
|
2024-06-17 15:13:01 +02:00
|
|
|
let pointer_image = match maybe_image {
|
2022-11-17 20:32:54 +01:00
|
|
|
Some(image) => image,
|
|
|
|
|
None => {
|
2024-06-17 15:13:01 +02:00
|
|
|
let buffer = MemoryRenderBuffer::from_slice(
|
2022-11-17 20:32:54 +01:00
|
|
|
&frame.pixels_rgba,
|
2024-02-12 20:10:44 +01:00
|
|
|
Fourcc::Argb8888,
|
2022-11-17 20:32:54 +01:00
|
|
|
(frame.width as i32, frame.height as i32),
|
2025-01-20 18:25:01 +01:00
|
|
|
actual_scale as i32,
|
2022-11-17 20:32:54 +01:00
|
|
|
Transform::Normal,
|
|
|
|
|
None,
|
2024-02-12 20:10:44 +01:00
|
|
|
);
|
|
|
|
|
pointer_images.push((frame.clone(), buffer));
|
|
|
|
|
pointer_images.last().map(|(_, i)| i).unwrap()
|
2022-11-17 20:32:54 +01:00
|
|
|
}
|
2024-06-17 15:13:01 +02:00
|
|
|
};
|
2022-09-28 12:01:29 +02:00
|
|
|
|
2025-04-16 19:35:30 +02:00
|
|
|
let hotspot = Point::<i32, BufferCoords>::from((frame.xhot as i32, frame.yhot as i32))
|
|
|
|
|
.to_logical(
|
|
|
|
|
actual_scale as i32,
|
|
|
|
|
Transform::Normal,
|
|
|
|
|
&Size::from((frame.width as i32, frame.height as i32)),
|
|
|
|
|
);
|
2024-06-07 19:26:23 +02:00
|
|
|
state.current_image = Some(frame);
|
2022-02-01 13:59:39 +01:00
|
|
|
|
2026-03-04 18:57:37 +01:00
|
|
|
push(
|
2024-03-12 19:42:48 +01:00
|
|
|
CursorRenderElement::Static(
|
|
|
|
|
MemoryRenderBufferRenderElement::from_buffer(
|
|
|
|
|
renderer,
|
|
|
|
|
location.to_physical(scale),
|
2025-10-16 13:50:32 +02:00
|
|
|
pointer_image,
|
2024-03-12 19:42:48 +01:00
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
Kind::Cursor,
|
|
|
|
|
)
|
|
|
|
|
.expect("Failed to import cursor bitmap"),
|
|
|
|
|
),
|
2025-04-16 19:35:30 +02:00
|
|
|
hotspot.to_physical_precise_round(scale),
|
2026-03-04 18:57:37 +01:00
|
|
|
);
|
2024-09-09 16:21:27 +02:00
|
|
|
} else if let CursorImageStatus::Surface(ref wl_surface) = cursor_status {
|
2026-03-04 18:57:37 +01:00
|
|
|
draw_surface_cursor(renderer, wl_surface, location, scale, push);
|
2022-02-01 13:59:39 +01:00
|
|
|
}
|
|
|
|
|
}
|
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-20 23:52:45 +02:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|