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,
|
|
|
|
|
};
|
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
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
hidden: bool,
|
|
|
|
|
idle_timer: Option<RegistrationToken>,
|
|
|
|
|
last_armed: Option<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
|
|
|
|
|
|
|
|
hidden: false,
|
|
|
|
|
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
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
/// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|