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
|
|
|
},
|
2026-09-12 09:13:02 +02:00
|
|
|
shell::focus::target::KeyboardFocusTarget,
|
2026-03-04 18:16:48 +01:00
|
|
|
utils::prelude::*,
|
|
|
|
|
wayland::handlers::compositor::FRAME_TIME_FILTER,
|
|
|
|
|
};
|
2026-09-12 09:13:02 +02:00
|
|
|
use cosmic_comp_config::HideDecision;
|
2026-07-20 16:11:13 -06:00
|
|
|
use keyframe::{ease, functions::EaseInOutCubic};
|
2026-07-31 14:46:52 -06:00
|
|
|
use resvg::{tiny_skia, usvg};
|
|
|
|
|
use serde::Deserialize;
|
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::{
|
2026-07-20 16:11:13 -06:00
|
|
|
collections::{HashMap, VecDeque},
|
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
|
|
|
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
|
|
|
|
2026-07-31 14:46:52 -06:00
|
|
|
/// A single frame of a scalable SVG cursor: the parsed SVG plus its metadata.
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
struct SvgFrame {
|
|
|
|
|
tree: usvg::Tree,
|
|
|
|
|
/// The nominal (logical) size the SVG is authored for.
|
|
|
|
|
nominal_size: f32,
|
|
|
|
|
/// Hotspot coordinates, in the SVG's own (nominal) coordinate space.
|
|
|
|
|
hotspot_x: f32,
|
|
|
|
|
hotspot_y: f32,
|
|
|
|
|
/// Delay to the next frame in milliseconds (0 for static cursors).
|
|
|
|
|
delay: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
enum CursorKind {
|
|
|
|
|
/// Legacy raster XCursor frames.
|
|
|
|
|
Xcursor(Vec<Image>),
|
|
|
|
|
/// Scalable SVG frames, rasterized on demand.
|
|
|
|
|
Svg(Vec<SvgFrame>),
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-01 13:59:39 +01:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct Cursor {
|
2026-07-31 14:46:52 -06:00
|
|
|
kind: CursorKind,
|
2022-02-01 13:59:39 +01:00
|
|
|
size: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Cursor {
|
2024-09-09 16:21:27 +02:00
|
|
|
pub fn load(theme: &CursorTheme, shape: CursorIcon, size: u32) -> Cursor {
|
2026-07-31 14:46:52 -06:00
|
|
|
// Prefer a scalable SVG cursor when the theme provides one.
|
|
|
|
|
if let Some(frames) = load_svg_icon(theme, shape) {
|
|
|
|
|
return Cursor {
|
|
|
|
|
kind: CursorKind::Svg(frames),
|
|
|
|
|
size,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-09 16:21:27 +02:00
|
|
|
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
|
|
|
|
|
}]
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-31 14:46:52 -06:00
|
|
|
Cursor {
|
|
|
|
|
kind: CursorKind::Xcursor(icons),
|
|
|
|
|
size,
|
|
|
|
|
}
|
2022-02-01 13:59:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_image(&self, scale: u32, millis: u32) -> Image {
|
|
|
|
|
let size = self.size * scale;
|
2026-07-31 14:46:52 -06:00
|
|
|
let idx = self.frame_index(size, millis);
|
|
|
|
|
self.render_frame(size, idx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Selects the index of the frame to display at nominal size `size` (in px)
|
|
|
|
|
/// and elapsed `millis`.
|
|
|
|
|
fn frame_index(&self, size: u32, millis: u32) -> usize {
|
|
|
|
|
match &self.kind {
|
|
|
|
|
CursorKind::Xcursor(images) => xcursor_frame_index(millis, size, images),
|
|
|
|
|
CursorKind::Svg(frames) => svg_frame_index(millis, frames),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Produces the RGBA image for frame `idx`, rasterizing SVG cursors at the
|
|
|
|
|
/// requested nominal size `size` (in px).
|
|
|
|
|
fn render_frame(&self, size: u32, idx: usize) -> Image {
|
|
|
|
|
match &self.kind {
|
|
|
|
|
CursorKind::Xcursor(images) => images[idx].clone(),
|
|
|
|
|
CursorKind::Svg(frames) => rasterize_svg_frame(&frames[idx], size),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Rasterize a scalable cursor frame at nominal pixel size `size`. Per the KDE
|
|
|
|
|
/// SVG cursor format, the SVG canvas and hotspot are scaled by
|
|
|
|
|
/// `size / nominal_size`. The returned [`Image`] uses the same premultiplied
|
|
|
|
|
/// byte order (BGRA / little-endian ARGB) as xcursor images.
|
|
|
|
|
fn rasterize_svg_frame(frame: &SvgFrame, size: u32) -> Image {
|
|
|
|
|
let factor = size as f32 / frame.nominal_size;
|
|
|
|
|
let svg_size = frame.tree.size();
|
|
|
|
|
let width = ((svg_size.width() * factor).floor() as u32).max(1);
|
|
|
|
|
let height = ((svg_size.height() * factor).floor() as u32).max(1);
|
|
|
|
|
|
|
|
|
|
let mut image = Image {
|
|
|
|
|
size,
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
xhot: (frame.hotspot_x * factor).floor() as u32,
|
|
|
|
|
yhot: (frame.hotspot_y * factor).floor() as u32,
|
|
|
|
|
delay: frame.delay,
|
|
|
|
|
pixels_rgba: Vec::new(),
|
|
|
|
|
pixels_argb: Vec::new(), // unused
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match tiny_skia::Pixmap::new(width, height) {
|
|
|
|
|
Some(mut pixmap) => {
|
|
|
|
|
resvg::render(
|
|
|
|
|
&frame.tree,
|
|
|
|
|
tiny_skia::Transform::from_scale(factor, factor),
|
|
|
|
|
&mut pixmap.as_mut(),
|
|
|
|
|
);
|
|
|
|
|
// tiny-skia produces premultiplied RGBA; xcursor images consume the
|
|
|
|
|
// raw little-endian ARGB byte order (premultiplied BGRA). Swap R<->B.
|
|
|
|
|
let mut pixels = pixmap.take();
|
|
|
|
|
for px in pixels.as_chunks_mut::<4>().0 {
|
|
|
|
|
px.swap(0, 2);
|
|
|
|
|
}
|
|
|
|
|
image.pixels_rgba = pixels;
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
warn!(width, height, "Failed to allocate cursor pixmap");
|
|
|
|
|
image.pixels_rgba = vec![0; (width as usize) * (height as usize) * 4];
|
|
|
|
|
}
|
2022-02-01 13:59:39 +01:00
|
|
|
}
|
2026-07-31 14:46:52 -06:00
|
|
|
|
|
|
|
|
image
|
2022-02-01 13:59:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-31 14:46:52 -06:00
|
|
|
/// Indices (into `images`) of all frames sharing the resolution nearest to `size`.
|
|
|
|
|
fn nearest_image_indices(size: u32, images: &[Image]) -> Vec<usize> {
|
|
|
|
|
// Follow the nominal size of the cursor to choose the nearest.
|
2022-02-01 13:59:39 +01:00
|
|
|
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();
|
2026-07-31 14:46:52 -06:00
|
|
|
let (width, height) = (nearest_image.width, nearest_image.height);
|
|
|
|
|
|
|
|
|
|
images
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.filter(|(_, image)| image.width == width && image.height == height)
|
|
|
|
|
.map(|(i, _)| i)
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
2022-02-01 13:59:39 +01:00
|
|
|
|
2026-07-31 14:46:52 -06:00
|
|
|
fn xcursor_frame_index(mut millis: u32, size: u32, images: &[Image]) -> usize {
|
|
|
|
|
let indices = nearest_image_indices(size, images);
|
|
|
|
|
let total: u32 = indices.iter().map(|&i| images[i].delay).sum();
|
|
|
|
|
|
|
|
|
|
if total == 0 {
|
|
|
|
|
return indices[0];
|
|
|
|
|
}
|
|
|
|
|
millis %= total;
|
|
|
|
|
|
|
|
|
|
for &i in &indices {
|
|
|
|
|
if millis <= images[i].delay {
|
|
|
|
|
return i;
|
|
|
|
|
}
|
|
|
|
|
millis -= images[i].delay;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
*indices.last().unwrap()
|
2022-02-01 13:59:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-31 14:46:52 -06:00
|
|
|
fn svg_frame_index(mut millis: u32, frames: &[SvgFrame]) -> usize {
|
|
|
|
|
let total: u32 = frames.iter().map(|frame| frame.delay).sum();
|
2024-06-27 16:41:38 +02:00
|
|
|
|
|
|
|
|
if total == 0 {
|
2026-07-31 14:46:52 -06:00
|
|
|
return 0;
|
2024-06-27 16:41:38 +02:00
|
|
|
}
|
2026-07-31 14:46:52 -06:00
|
|
|
millis %= total;
|
2022-02-01 13:59:39 +01:00
|
|
|
|
2026-07-31 14:46:52 -06:00
|
|
|
for (i, frame) in frames.iter().enumerate() {
|
|
|
|
|
if millis <= frame.delay {
|
|
|
|
|
return i;
|
2022-02-01 13:59:39 +01:00
|
|
|
}
|
2026-07-31 14:46:52 -06:00
|
|
|
millis -= frame.delay;
|
2022-02-01 13:59:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-31 14:46:52 -06:00
|
|
|
frames.len() - 1
|
2022-02-01 13:59:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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
|
|
|
}
|
|
|
|
|
|
2026-07-31 14:46:52 -06:00
|
|
|
/// A frame entry in a `cursors_scalable/<shape>/metadata.json` file, per the
|
|
|
|
|
/// KDE SVG cursor format specification.
|
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
|
|
|
struct SvgCursorMeta {
|
|
|
|
|
filename: String,
|
|
|
|
|
nominal_size: f32,
|
|
|
|
|
hotspot_x: f32,
|
|
|
|
|
hotspot_y: f32,
|
|
|
|
|
/// Only present for animated cursors; defaults to 0 for static ones.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
delay: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Resolves a scalable (SVG) cursor for `shape`
|
|
|
|
|
fn load_svg_icon(theme: &CursorTheme, shape: CursorIcon) -> Option<Vec<SvgFrame>> {
|
|
|
|
|
let shape_name = shape.to_string();
|
|
|
|
|
let options = usvg::Options::default();
|
|
|
|
|
for name in cursor_aliases(&shape_name)
|
|
|
|
|
.iter()
|
|
|
|
|
.copied()
|
|
|
|
|
.chain(std::iter::once(shape_name.as_str()))
|
|
|
|
|
{
|
|
|
|
|
if let Some(dir) = theme.load_scalable(name)
|
|
|
|
|
&& let Some(frames) = parse_svg_dir(&dir, &options)
|
|
|
|
|
{
|
|
|
|
|
return Some(frames);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Reads a `cursors_scalable/<shape>` directory (its `metadata.json` and the
|
|
|
|
|
/// referenced SVG files) into a list of parsed frames.
|
|
|
|
|
fn parse_svg_dir(dir: &std::path::Path, options: &usvg::Options) -> Option<Vec<SvgFrame>> {
|
|
|
|
|
let metadata = std::fs::read(dir.join("metadata.json")).ok()?;
|
|
|
|
|
let metas: Vec<SvgCursorMeta> = match serde_json::from_slice(&metadata) {
|
|
|
|
|
Ok(metas) => metas,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!(?dir, ?err, "Malformed SVG cursor metadata");
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut frames = Vec::with_capacity(metas.len());
|
|
|
|
|
for meta in metas {
|
|
|
|
|
let svg_path = dir.join(&meta.filename);
|
|
|
|
|
let svg_data = match std::fs::read(&svg_path) {
|
|
|
|
|
Ok(data) => data,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!(?svg_path, ?err, "Unable to read SVG cursor");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let tree = match usvg::Tree::from_data(&svg_data, options) {
|
|
|
|
|
Ok(tree) => tree,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!(?svg_path, ?err, "Unable to parse SVG cursor");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
frames.push(SvgFrame {
|
|
|
|
|
tree,
|
|
|
|
|
nominal_size: meta.nominal_size,
|
|
|
|
|
hotspot_x: meta.hotspot_x,
|
|
|
|
|
hotspot_y: meta.hotspot_y,
|
|
|
|
|
delay: meta.delay,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(!frames.is_empty()).then_some(frames)
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-28 12:01:29 +02:00
|
|
|
render_elements! {
|
2026-06-11 13:18:41 +02:00
|
|
|
pub CursorRenderElement<R> where R: ImportAll + ImportMem + AsGlowRenderer, R::TextureId: Send;
|
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-10 17:16:48 +01:00
|
|
|
blur_strength: usize,
|
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],
|
2026-04-17 17:57:48 +02:00
|
|
|
None,
|
2026-03-10 17:16:48 +01:00
|
|
|
blur_strength,
|
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-10 17:16:48 +01:00
|
|
|
blur_strength: usize,
|
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],
|
2026-04-17 17:57:48 +02:00
|
|
|
None,
|
2026-03-10 17:16:48 +01:00
|
|
|
blur_strength,
|
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
|
|
|
}
|
|
|
|
|
|
2026-09-12 09:13:02 +02:00
|
|
|
/// Why the cursor is currently hidden. Determines what brings it back.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
pub enum HideReason {
|
|
|
|
|
/// No pointer activity for the configured timeout.
|
|
|
|
|
Idle,
|
|
|
|
|
/// A key was pressed while `while_typing` is enabled.
|
|
|
|
|
Typing,
|
|
|
|
|
/// Touch input arrived while `after_touch` is enabled.
|
|
|
|
|
Touch,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether a pointer event carries movement. A touch-hidden cursor comes back
|
|
|
|
|
/// only on real movement, so the distinction has to reach the call site.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
pub enum PointerEventKind {
|
|
|
|
|
Motion,
|
|
|
|
|
Other,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl HideReason {
|
|
|
|
|
/// Whether an event of this kind should reveal the cursor.
|
|
|
|
|
fn revealed_by(self, kind: PointerEventKind) -> bool {
|
|
|
|
|
matches!(kind, PointerEventKind::Motion) || !matches!(self, HideReason::Touch)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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>,
|
2026-08-24 10:57:29 -06:00
|
|
|
image_cache: Vec<CachedFrame>,
|
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
|
|
|
|
2026-09-12 09:13:02 +02:00
|
|
|
hidden: Option<HideReason>,
|
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
|
|
|
idle_timer: Option<RegistrationToken>,
|
|
|
|
|
last_armed: Option<Instant>,
|
2026-09-12 09:13:02 +02:00
|
|
|
last_pointer_activity: Instant,
|
2026-07-20 16:11:13 -06:00
|
|
|
|
|
|
|
|
// shake-to-find
|
|
|
|
|
shake_path: VecDeque<PathSample>,
|
|
|
|
|
shake_path_position: Point<f64, Logical>,
|
|
|
|
|
magnify_until: Option<Instant>,
|
|
|
|
|
magnify_target: f32,
|
|
|
|
|
magnification: f32,
|
|
|
|
|
anim_from: f32,
|
|
|
|
|
anim_start: Option<Instant>,
|
2026-08-24 10:57:29 -06:00
|
|
|
rest_started: Option<Instant>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A rasterized cursor frame, keyed by `(shape, pixel size, frame index)`.
|
|
|
|
|
struct CachedFrame {
|
|
|
|
|
key: (CursorIcon, u32, usize),
|
|
|
|
|
image: Image,
|
|
|
|
|
buffer: MemoryRenderBuffer,
|
|
|
|
|
unmagnified: bool,
|
2026-07-20 16:11:13 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// One sampled pointer position on the recent motion path.
|
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
|
struct PathSample {
|
|
|
|
|
position: Point<f64, Logical>,
|
|
|
|
|
time: Instant,
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 10:57:29 -06:00
|
|
|
/// How long everything must stay unmagnified before the enlarged frames go.
|
|
|
|
|
const MAGNIFIED_FRAME_GRACE: Duration = Duration::from_secs(10);
|
|
|
|
|
|
2026-07-20 16:11:13 -06:00
|
|
|
/// How far back the motion path is considered when looking for a shake.
|
|
|
|
|
const SHAKE_INTERVAL: Duration = Duration::from_millis(1000);
|
|
|
|
|
/// Path-length / bounding-box-diagonal ratio required to count as a shake.
|
|
|
|
|
const SHAKE_SENSITIVITY: f64 = 4.0;
|
|
|
|
|
/// Minimum bounding-box diagonal (logical px) before a shake is considered.
|
|
|
|
|
const SHAKE_DIAGONAL_MIN: f64 = 100.0;
|
|
|
|
|
/// Two deltas count as "the same direction" if both lie within this tolerance.
|
|
|
|
|
const SHAKE_SAME_SIGN_TOLERANCE: f64 = 1.0;
|
|
|
|
|
/// Keep the cursor enlarged for this long after the last detected shake.
|
|
|
|
|
const SHAKE_HOLD: Duration = Duration::from_millis(2000);
|
|
|
|
|
/// Extra magnification added by each shake, growing from the normal cursor size.
|
|
|
|
|
const OVER_MAGNIFICATION: f32 = 1.0;
|
2026-08-21 14:46:49 -06:00
|
|
|
/// Upper bound on the nominal size (in px) a cursor frame is rasterized at.
|
|
|
|
|
const MAX_RASTER_SIZE: u32 = 512;
|
2026-07-20 16:11:13 -06:00
|
|
|
/// Duration of the grow/shrink animation.
|
|
|
|
|
const MAGNIFICATION_ANIM: Duration = Duration::from_millis(200);
|
|
|
|
|
|
|
|
|
|
/// small movement is ignored and direction stays the same
|
|
|
|
|
fn same_direction(a: f64, b: f64) -> bool {
|
|
|
|
|
(a >= -SHAKE_SAME_SIGN_TOLERANCE && b >= -SHAKE_SAME_SIGN_TOLERANCE)
|
|
|
|
|
|| (a <= SHAKE_SAME_SIGN_TOLERANCE && b <= SHAKE_SAME_SIGN_TOLERANCE)
|
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 {
|
2026-07-31 14:46:52 -06:00
|
|
|
let cursor_theme = &self.cursor_theme;
|
|
|
|
|
let cursor_size = self.cursor_size;
|
2024-09-09 16:21:27 +02:00
|
|
|
self.cursors
|
|
|
|
|
.entry(shape)
|
2026-07-31 14:46:52 -06:00
|
|
|
.or_insert_with(|| Cursor::load(cursor_theme, shape, 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
|
|
|
|
|
}
|
2026-07-20 16:11:13 -06:00
|
|
|
|
2026-08-24 10:57:29 -06:00
|
|
|
/// Drop the rasterizations only a magnified cursor needed, once nothing has
|
|
|
|
|
/// magnified it for [`MAGNIFIED_FRAME_GRACE`].
|
|
|
|
|
pub fn drop_magnified_frames(&mut self, now: Instant, zoomed: bool) {
|
|
|
|
|
if zoomed || self.is_magnifying() {
|
|
|
|
|
self.rest_started = None;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let rest_started = *self.rest_started.get_or_insert(now);
|
|
|
|
|
if now.duration_since(rest_started) < MAGNIFIED_FRAME_GRACE {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.image_cache.retain(|frame| frame.unmagnified);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 16:11:13 -06:00
|
|
|
/// Feed one relative-motion event into the shake detector.
|
|
|
|
|
pub fn detect_shake(&mut self, delta: Point<f64, Logical>, now: Instant) {
|
|
|
|
|
// Drop samples that have aged out of the time window.
|
|
|
|
|
while let Some(oldest) = self.shake_path.front() {
|
|
|
|
|
if now.duration_since(oldest.time) >= SHAKE_INTERVAL {
|
|
|
|
|
self.shake_path.pop_front();
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if delta.x != 0.0 || delta.y != 0.0 {
|
|
|
|
|
self.shake_path_position += delta;
|
|
|
|
|
let sample = PathSample {
|
|
|
|
|
position: self.shake_path_position,
|
|
|
|
|
time: now,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if self.shake_path.len() >= 2 {
|
|
|
|
|
let last = self.shake_path[self.shake_path.len() - 1].position;
|
|
|
|
|
let prev = self.shake_path[self.shake_path.len() - 2].position;
|
|
|
|
|
let last_delta = last - prev;
|
|
|
|
|
if same_direction(last_delta.x, delta.x) && same_direction(last_delta.y, delta.y) {
|
|
|
|
|
*self.shake_path.back_mut().unwrap() = sample;
|
|
|
|
|
} else {
|
|
|
|
|
self.shake_path.push_back(sample);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
self.shake_path.push_back(sample);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.shake_path.len() < 2 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let first = self.shake_path[0].position;
|
|
|
|
|
let (mut left, mut top, mut right, mut bottom) = (first.x, first.y, first.x, first.y);
|
|
|
|
|
let mut path_length = 0.0;
|
|
|
|
|
for i in 1..self.shake_path.len() {
|
|
|
|
|
let p = self.shake_path[i].position;
|
|
|
|
|
left = left.min(p.x);
|
|
|
|
|
top = top.min(p.y);
|
|
|
|
|
right = right.max(p.x);
|
|
|
|
|
bottom = bottom.max(p.y);
|
|
|
|
|
|
|
|
|
|
let step = p - self.shake_path[i - 1].position;
|
|
|
|
|
path_length += step.x.hypot(step.y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let diagonal = (right - left).hypot(bottom - top);
|
|
|
|
|
if diagonal < SHAKE_DIAGONAL_MIN {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Path noticeably longer than the diagonal => a shake gesture.
|
|
|
|
|
if path_length / diagonal > SHAKE_SENSITIVITY {
|
|
|
|
|
self.grow(now);
|
|
|
|
|
self.shake_path.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// grow the cursor by one more increment (unbounded)
|
|
|
|
|
fn grow(&mut self, now: Instant) {
|
|
|
|
|
self.animate_to(self.magnify_target + OVER_MAGNIFICATION, now);
|
|
|
|
|
self.magnify_until = Some(now + SHAKE_HOLD);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Start a 200ms `InOutCubic` tween from the current size to `target`.
|
|
|
|
|
fn animate_to(&mut self, target: f32, now: Instant) {
|
|
|
|
|
if (target - self.magnify_target).abs() < f32::EPSILON {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
self.anim_from = self.magnification;
|
|
|
|
|
self.anim_start = Some(now);
|
|
|
|
|
self.magnify_target = target;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Advance the magnification animation and return the current factor.
|
|
|
|
|
pub fn animated_magnification(&mut self, now: Instant) -> f32 {
|
|
|
|
|
// Begin shrinking back once the hold window elapses.
|
|
|
|
|
if let Some(until) = self.magnify_until
|
|
|
|
|
&& now >= until
|
|
|
|
|
{
|
|
|
|
|
self.magnify_until = None;
|
|
|
|
|
self.animate_to(1.0, now);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.magnification = match self.anim_start {
|
|
|
|
|
Some(start) => {
|
|
|
|
|
// `ease` clamps the time to `0.0..=1.0` for us.
|
|
|
|
|
let t = now.duration_since(start).as_secs_f32() / MAGNIFICATION_ANIM.as_secs_f32();
|
|
|
|
|
if t >= 1.0 {
|
|
|
|
|
self.anim_start = None;
|
|
|
|
|
}
|
|
|
|
|
ease(EaseInOutCubic, self.anim_from, self.magnify_target, t)
|
|
|
|
|
}
|
|
|
|
|
None => self.magnify_target,
|
|
|
|
|
};
|
|
|
|
|
self.magnification
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether the cursor is currently magnified or pending; drives continued redraws.
|
|
|
|
|
pub fn is_magnifying(&self) -> bool {
|
|
|
|
|
self.magnify_until.is_some()
|
|
|
|
|
|| self.anim_start.is_some()
|
|
|
|
|
|| self.magnification > 1.001
|
|
|
|
|
|| self.magnify_target > 1.001
|
|
|
|
|
}
|
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
|
|
|
|
2026-09-12 09:13:02 +02:00
|
|
|
hidden: None,
|
|
|
|
|
last_pointer_activity: Instant::now(),
|
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
|
|
|
idle_timer: None,
|
|
|
|
|
last_armed: None,
|
2026-07-20 16:11:13 -06:00
|
|
|
|
|
|
|
|
shake_path: VecDeque::new(),
|
|
|
|
|
shake_path_position: Point::from((0.0, 0.0)),
|
|
|
|
|
magnify_until: None,
|
|
|
|
|
magnify_target: 1.0,
|
|
|
|
|
magnification: 1.0,
|
|
|
|
|
anim_from: 1.0,
|
|
|
|
|
anim_start: None,
|
2026-08-24 10:57:29 -06:00
|
|
|
rest_started: None,
|
2022-03-16 20:05:24 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-02-01 13:59:39 +01:00
|
|
|
|
2026-08-21 14:46:49 -06:00
|
|
|
/// Pick the size a cursor frame is rasterized at, given the size the output wants
|
|
|
|
|
/// (`needed`) and the size it would want unmagnified (`base`).
|
|
|
|
|
///
|
|
|
|
|
/// Rasterizations are restricted to `base * 2^n`, rounded up, and clamped to
|
|
|
|
|
/// [`MAX_RASTER_SIZE`].
|
|
|
|
|
fn raster_size(needed: u32, base: u32) -> u32 {
|
|
|
|
|
let base = base.max(1);
|
|
|
|
|
let cap = MAX_RASTER_SIZE.max(base);
|
|
|
|
|
let mut rung = base;
|
|
|
|
|
while rung < needed && rung.saturating_mul(2) <= cap {
|
|
|
|
|
rung *= 2;
|
|
|
|
|
}
|
|
|
|
|
rung
|
|
|
|
|
}
|
|
|
|
|
|
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>,
|
2026-03-10 17:16:48 +01:00
|
|
|
blur_strength: usize,
|
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
|
|
|
|
2026-09-12 09:13:02 +02:00
|
|
|
if state.hidden.is_some() {
|
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
|
|
|
}
|
|
|
|
|
|
2026-08-21 14:46:49 -06:00
|
|
|
let output_scale = scale.x.max(scale.y);
|
|
|
|
|
let integer_scale = (output_scale * buffer_scale).ceil() as u32;
|
|
|
|
|
let unmagnified_px = state.size() * (output_scale.ceil() as u32);
|
|
|
|
|
let size_px = raster_size(state.size() * integer_scale, unmagnified_px);
|
2026-07-31 14:46:52 -06:00
|
|
|
|
|
|
|
|
// Pick the frame to display without rasterizing, so a cache hit avoids
|
|
|
|
|
// any SVG rendering. The `&Cursor` borrow is scoped to this block.
|
|
|
|
|
let frame_idx = {
|
|
|
|
|
let cursor = state.get_named_cursor(current_cursor);
|
|
|
|
|
cursor.frame_index(size_px, time.as_millis())
|
|
|
|
|
};
|
|
|
|
|
let key = (current_cursor, size_px, frame_idx);
|
2022-02-01 13:59:39 +01:00
|
|
|
|
2026-07-31 14:46:52 -06:00
|
|
|
// Rasterize and upload this (shape, size, frame) only if not cached.
|
2026-08-24 10:57:29 -06:00
|
|
|
let index = match state.image_cache.iter().position(|frame| frame.key == key) {
|
|
|
|
|
Some(index) => index,
|
|
|
|
|
None => {
|
|
|
|
|
let image = {
|
|
|
|
|
let cursor = state.get_named_cursor(current_cursor);
|
|
|
|
|
cursor.render_frame(size_px, frame_idx)
|
|
|
|
|
};
|
|
|
|
|
let actual_scale = (image.size / state.size()).max(1);
|
|
|
|
|
let buffer = MemoryRenderBuffer::from_slice(
|
|
|
|
|
&image.pixels_rgba,
|
|
|
|
|
Fourcc::Argb8888,
|
|
|
|
|
(image.width as i32, image.height as i32),
|
|
|
|
|
actual_scale as i32,
|
|
|
|
|
Transform::Normal,
|
|
|
|
|
None,
|
|
|
|
|
);
|
|
|
|
|
state.image_cache.push(CachedFrame {
|
|
|
|
|
key,
|
|
|
|
|
image,
|
|
|
|
|
buffer,
|
|
|
|
|
unmagnified: size_px == unmagnified_px,
|
|
|
|
|
});
|
|
|
|
|
state.image_cache.len() - 1
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-07-31 14:46:52 -06:00
|
|
|
|
|
|
|
|
let (frame, pointer_image) = {
|
2026-08-24 10:57:29 -06:00
|
|
|
let entry = &mut state.image_cache[index];
|
|
|
|
|
entry.unmagnified |= size_px == unmagnified_px;
|
|
|
|
|
(entry.image.clone(), entry.buffer.clone())
|
2024-06-17 15:13:01 +02:00
|
|
|
};
|
2026-07-31 14:46:52 -06:00
|
|
|
let actual_scale = (frame.size / state.size()).max(1);
|
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),
|
2026-07-31 14:46:52 -06: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-10 17:16:48 +01:00
|
|
|
draw_surface_cursor(renderer, wl_surface, location, scale, blur_strength, 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);
|
|
|
|
|
|
2026-09-12 09:13:02 +02:00
|
|
|
/// Reveal the cursor if this event should, and (re)arm the idle-hide timer.
|
|
|
|
|
/// Returns true if the cursor was previously hidden and is now visible.
|
|
|
|
|
pub fn notify_cursor_activity(state: &State, seat: &Seat<State>, kind: PointerEventKind) -> bool {
|
|
|
|
|
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
|
|
|
|
|
|
|
|
|
let revealed = {
|
|
|
|
|
let mut inner = cursor_state.lock().unwrap();
|
|
|
|
|
inner.last_pointer_activity = Instant::now();
|
|
|
|
|
match inner.hidden {
|
|
|
|
|
Some(reason) if reason.revealed_by(kind) => {
|
|
|
|
|
inner.hidden = None;
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
// Still hidden on purpose: leave the timer alone.
|
|
|
|
|
Some(_) => return false,
|
|
|
|
|
None => false,
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
refresh_idle_timer(state, seat);
|
|
|
|
|
revealed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// (Re)arm the idle-hide timer without counting as pointer activity.
|
|
|
|
|
///
|
|
|
|
|
/// Armed with the shortest timeout any context could want: the input that arms
|
|
|
|
|
/// the timer is usually what goes on to make a window fullscreen, so the
|
|
|
|
|
/// context is not yet knowable here. `on_idle_timer` evaluates it on fire and
|
|
|
|
|
/// re-arms for the remainder if nothing applied yet.
|
|
|
|
|
pub fn refresh_idle_timer(state: &State, seat: &Seat<State>) {
|
|
|
|
|
let config = state.common.config.cosmic_conf.cursor_hide;
|
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
|
|
|
let loop_handle = &state.common.event_loop_handle;
|
|
|
|
|
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
|
|
|
|
let now = Instant::now();
|
|
|
|
|
|
2026-09-12 09:13:02 +02:00
|
|
|
let old_token = {
|
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
|
|
|
let mut inner = cursor_state.lock().unwrap();
|
2026-09-12 09:13:02 +02:00
|
|
|
if inner.hidden.is_some() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Coalesce bursts from high-frequency pointers.
|
|
|
|
|
let throttled = inner.idle_timer.is_some()
|
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
|
|
|
&& inner
|
|
|
|
|
.last_armed
|
|
|
|
|
.is_some_and(|t| now.duration_since(t) < ACTIVITY_THROTTLE);
|
|
|
|
|
if throttled {
|
2026-09-12 09:13:02 +02: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
|
|
|
}
|
|
|
|
|
inner.last_armed = None;
|
2026-09-12 09:13:02 +02:00
|
|
|
inner.idle_timer.take()
|
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 let Some(token) = old_token {
|
|
|
|
|
loop_handle.remove(token);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-12 09:13:02 +02:00
|
|
|
let Some(delay) = config.shortest_timeout() else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let timer = Timer::from_duration(delay);
|
|
|
|
|
let timer_seat = seat.clone();
|
|
|
|
|
if let Ok(token) =
|
|
|
|
|
loop_handle.insert_source(timer, move |_, _, state| on_idle_timer(state, &timer_seat))
|
|
|
|
|
{
|
|
|
|
|
let mut inner = cursor_state.lock().unwrap();
|
|
|
|
|
inner.idle_timer = Some(token);
|
|
|
|
|
inner.last_armed = Some(now);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether the seat is focusing a fullscreen surface. A fullscreen window that
|
|
|
|
|
/// is not focused can be stacked behind others, so its presence on the output
|
|
|
|
|
/// says nothing about what the user is looking at.
|
|
|
|
|
fn fullscreen_focused(seat: &Seat<State>) -> bool {
|
|
|
|
|
matches!(
|
|
|
|
|
seat.get_keyboard().and_then(|kbd| kbd.current_focus()),
|
|
|
|
|
Some(KeyboardFocusTarget::Fullscreen(_))
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn on_idle_timer(state: &mut State, seat: &Seat<State>) -> TimeoutAction {
|
|
|
|
|
let config = state.common.config.cosmic_conf.cursor_hide;
|
|
|
|
|
let fullscreen = fullscreen_focused(seat);
|
|
|
|
|
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
|
|
|
|
|
|
|
|
|
let elapsed = {
|
|
|
|
|
let inner = cursor_state.lock().unwrap();
|
|
|
|
|
Instant::now().duration_since(inner.last_pointer_activity)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match config.resolve(elapsed, fullscreen) {
|
|
|
|
|
HideDecision::Hide => {
|
|
|
|
|
hide_cursor(state, seat, HideReason::Idle);
|
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
|
|
|
TimeoutAction::Drop
|
2026-09-12 09:13:02 +02:00
|
|
|
}
|
|
|
|
|
HideDecision::Drop => {
|
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
|
|
|
let mut inner = cursor_state.lock().unwrap();
|
2026-09-12 09:13:02 +02:00
|
|
|
inner.idle_timer = None;
|
|
|
|
|
inner.last_armed = None;
|
|
|
|
|
TimeoutAction::Drop
|
|
|
|
|
}
|
|
|
|
|
HideDecision::RearmAfter(delay) => {
|
|
|
|
|
cursor_state.lock().unwrap().last_armed = Some(Instant::now());
|
|
|
|
|
TimeoutAction::ToDuration(delay)
|
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
|
|
|
}
|
|
|
|
|
}
|
2026-09-12 09:13:02 +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
|
|
|
|
2026-09-12 09:13:02 +02:00
|
|
|
/// Hide the cursor immediately, for a trigger that does not wait on the timer.
|
|
|
|
|
pub fn hide_cursor_now(state: &mut State, seat: &Seat<State>, reason: HideReason) {
|
|
|
|
|
let loop_handle = state.common.event_loop_handle.clone();
|
|
|
|
|
let token = {
|
|
|
|
|
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
|
|
|
|
let mut inner = cursor_state.lock().unwrap();
|
|
|
|
|
inner.idle_timer.take()
|
|
|
|
|
};
|
|
|
|
|
if let Some(token) = token {
|
|
|
|
|
loop_handle.remove(token);
|
|
|
|
|
}
|
|
|
|
|
hide_cursor(state, seat, reason);
|
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
|
|
|
}
|
|
|
|
|
|
2026-09-12 09:13:02 +02:00
|
|
|
fn hide_cursor(state: &mut State, seat: &Seat<State>, reason: HideReason) {
|
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 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();
|
2026-09-12 09:13:02 +02:00
|
|
|
if inner.hidden.is_some() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
inner.hidden = Some(reason);
|
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
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-12 09:13:02 +02:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod test {
|
|
|
|
|
use super::{HideReason, PointerEventKind};
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn touch_hidden_cursor_waits_for_movement() {
|
|
|
|
|
assert!(HideReason::Touch.revealed_by(PointerEventKind::Motion));
|
|
|
|
|
assert!(!HideReason::Touch.revealed_by(PointerEventKind::Other));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn idle_and_typing_hidden_cursors_are_revealed_by_any_pointer_event() {
|
|
|
|
|
for reason in [HideReason::Idle, HideReason::Typing] {
|
|
|
|
|
assert!(reason.revealed_by(PointerEventKind::Motion));
|
|
|
|
|
// A click or a scroll must rescue the cursor rather than forcing a
|
|
|
|
|
// blind click.
|
|
|
|
|
assert!(reason.revealed_by(PointerEventKind::Other));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|