Merge tag 'epoch-1.7.0' into local/yoda-main
Epoch 1.7.0
This commit is contained in:
commit
af6d31f413
12 changed files with 552 additions and 49 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
|
@ -937,6 +937,7 @@ dependencies = [
|
|||
"profiling",
|
||||
"rand 0.10.0",
|
||||
"regex",
|
||||
"resvg 0.45.1",
|
||||
"ron 0.12.0",
|
||||
"rust-embed",
|
||||
"rustix 1.1.4",
|
||||
|
|
@ -7645,9 +7646,9 @@ checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
|||
|
||||
[[package]]
|
||||
name = "xcursor"
|
||||
version = "0.3.10"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b"
|
||||
checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23"
|
||||
|
||||
[[package]]
|
||||
name = "xdg"
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ log-panics = { version = "2", features = ["with-backtrace"] }
|
|||
ordered-float = "5.1"
|
||||
png = "0.18"
|
||||
regex = "1"
|
||||
resvg = "0.45"
|
||||
ron = "0.12"
|
||||
rust-embed = { version = "8.11", features = ["debug-embed"] }
|
||||
sanitize-filename = "0.6.0"
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ pub struct CosmicCompConfig {
|
|||
pub appearance_settings: AppearanceConfig,
|
||||
/// Hide the cursor after this many seconds of pointer inactivity (None disables)
|
||||
pub cursor_hide_timeout: Option<u32>,
|
||||
/// Briefly magnify the cursor when the pointer is shaken, to help locate it
|
||||
pub cursor_shake_to_find: bool,
|
||||
pub activation_policy: ActivationPolicy,
|
||||
}
|
||||
|
||||
|
|
@ -139,6 +141,7 @@ impl Default for CosmicCompConfig {
|
|||
accessibility_zoom: ZoomConfig::default(),
|
||||
appearance_settings: AppearanceConfig::default(),
|
||||
cursor_hide_timeout: None,
|
||||
cursor_shake_to_find: true,
|
||||
activation_policy: ActivationPolicy::default(),
|
||||
}
|
||||
}
|
||||
|
|
@ -195,6 +198,10 @@ pub struct ZoomConfig {
|
|||
pub enable_mouse_zoom_shortcuts: bool,
|
||||
}
|
||||
|
||||
impl ZoomConfig {
|
||||
pub const ZOOM_INCREMENT_PRESETS: &[u32] = &[10, 25, 50, 75, 100, 150, 200];
|
||||
}
|
||||
|
||||
impl Default for ZoomConfig {
|
||||
fn default() -> Self {
|
||||
ZoomConfig {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ use crate::{
|
|||
utils::prelude::*,
|
||||
wayland::handlers::compositor::FRAME_TIME_FILTER,
|
||||
};
|
||||
use keyframe::{ease, functions::EaseInOutCubic};
|
||||
use resvg::{tiny_skia, usvg};
|
||||
use serde::Deserialize;
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::Fourcc,
|
||||
|
|
@ -38,7 +41,7 @@ use smithay::{
|
|||
wayland::compositor::{get_role, with_states},
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::{HashMap, VecDeque},
|
||||
io::Read,
|
||||
sync::Mutex,
|
||||
time::{Duration, Instant},
|
||||
|
|
@ -51,14 +54,43 @@ use xcursor::{
|
|||
|
||||
static FALLBACK_CURSOR_DATA: &[u8] = include_bytes!("../../../resources/cursor.rgba");
|
||||
|
||||
/// 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>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cursor {
|
||||
icons: Vec<Image>,
|
||||
kind: CursorKind,
|
||||
size: u32,
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
pub fn load(theme: &CursorTheme, shape: CursorIcon, size: u32) -> Cursor {
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
|
||||
let icons = load_icon(theme, shape)
|
||||
.map_err(|err| warn!(?err, "Unable to load xcursor, using fallback cursor"))
|
||||
.or_else(|_| load_icon(theme, CursorIcon::Default))
|
||||
|
|
@ -75,44 +107,134 @@ impl Cursor {
|
|||
}]
|
||||
});
|
||||
|
||||
Cursor { icons, size }
|
||||
Cursor {
|
||||
kind: CursorKind::Xcursor(icons),
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_image(&self, scale: u32, millis: u32) -> Image {
|
||||
let size = self.size * scale;
|
||||
frame(millis, size, &self.icons)
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_images(size: u32, images: &[Image]) -> impl Iterator<Item = &Image> {
|
||||
// Follow the nominal size of the cursor to choose the nearest
|
||||
/// 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];
|
||||
}
|
||||
}
|
||||
|
||||
image
|
||||
}
|
||||
|
||||
/// 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.
|
||||
let nearest_image = images
|
||||
.iter()
|
||||
.min_by_key(|image| u32::abs_diff(size, image.size))
|
||||
.unwrap();
|
||||
let (width, height) = (nearest_image.width, nearest_image.height);
|
||||
|
||||
images.iter().filter(move |image| {
|
||||
image.width == nearest_image.width && image.height == nearest_image.height
|
||||
})
|
||||
images
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, image)| image.width == width && image.height == height)
|
||||
.map(|(i, _)| i)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn frame(mut millis: u32, size: u32, images: &[Image]) -> Image {
|
||||
let total = nearest_images(size, images).fold(0, |acc, image| acc + image.delay);
|
||||
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 {
|
||||
millis = 0;
|
||||
} else {
|
||||
return indices[0];
|
||||
}
|
||||
millis %= total;
|
||||
|
||||
for &i in &indices {
|
||||
if millis <= images[i].delay {
|
||||
return i;
|
||||
}
|
||||
millis -= images[i].delay;
|
||||
}
|
||||
|
||||
for img in nearest_images(size, images) {
|
||||
if millis <= img.delay {
|
||||
return img.clone();
|
||||
*indices.last().unwrap()
|
||||
}
|
||||
|
||||
fn svg_frame_index(mut millis: u32, frames: &[SvgFrame]) -> usize {
|
||||
let total: u32 = frames.iter().map(|frame| frame.delay).sum();
|
||||
|
||||
if total == 0 {
|
||||
return 0;
|
||||
}
|
||||
millis -= img.delay;
|
||||
millis %= total;
|
||||
|
||||
for (i, frame) in frames.iter().enumerate() {
|
||||
if millis <= frame.delay {
|
||||
return i;
|
||||
}
|
||||
millis -= frame.delay;
|
||||
}
|
||||
|
||||
unreachable!()
|
||||
frames.len() - 1
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
|
|
@ -174,6 +296,79 @@ fn load_icon(theme: &CursorTheme, shape: CursorIcon) -> Result<Vec<Image>, Error
|
|||
Err(Error::NoDefaultCursor)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
render_elements! {
|
||||
pub CursorRenderElement<R> where R: ImportAll + ImportMem + AsGlowRenderer, R::TextureId: Send;
|
||||
Static=MemoryRenderBufferRenderElement<R>,
|
||||
|
|
@ -265,11 +460,62 @@ pub struct CursorStateInner {
|
|||
|
||||
cursors: HashMap<CursorIcon, Cursor>,
|
||||
current_image: Option<Image>,
|
||||
image_cache: Vec<(Image, MemoryRenderBuffer)>,
|
||||
image_cache: Vec<CachedFrame>,
|
||||
|
||||
hidden: bool,
|
||||
idle_timer: Option<RegistrationToken>,
|
||||
last_armed: Option<Instant>,
|
||||
|
||||
// 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>,
|
||||
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,
|
||||
}
|
||||
|
||||
/// One sampled pointer position on the recent motion path.
|
||||
#[derive(Clone, Copy)]
|
||||
struct PathSample {
|
||||
position: Point<f64, Logical>,
|
||||
time: Instant,
|
||||
}
|
||||
|
||||
/// How long everything must stay unmagnified before the enlarged frames go.
|
||||
const MAGNIFIED_FRAME_GRACE: Duration = Duration::from_secs(10);
|
||||
|
||||
/// 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;
|
||||
/// Upper bound on the nominal size (in px) a cursor frame is rasterized at.
|
||||
const MAX_RASTER_SIZE: u32 = 512;
|
||||
/// 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)
|
||||
}
|
||||
|
||||
impl CursorStateInner {
|
||||
|
|
@ -282,14 +528,141 @@ impl CursorStateInner {
|
|||
}
|
||||
|
||||
pub fn get_named_cursor(&mut self, shape: CursorIcon) -> &Cursor {
|
||||
let cursor_theme = &self.cursor_theme;
|
||||
let cursor_size = self.cursor_size;
|
||||
self.cursors
|
||||
.entry(shape)
|
||||
.or_insert_with(|| Cursor::load(&self.cursor_theme, shape, self.cursor_size))
|
||||
.or_insert_with(|| Cursor::load(cursor_theme, shape, cursor_size))
|
||||
}
|
||||
|
||||
pub fn size(&self) -> u32 {
|
||||
self.cursor_size
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_cursor_env() -> (String, u32) {
|
||||
|
|
@ -324,10 +697,34 @@ impl Default for CursorStateInner {
|
|||
hidden: false,
|
||||
idle_timer: None,
|
||||
last_armed: None,
|
||||
|
||||
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,
|
||||
rest_started: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
pub fn draw_cursor<R>(
|
||||
renderer: &mut R,
|
||||
|
|
@ -363,32 +760,53 @@ pub fn draw_cursor<R>(
|
|||
return;
|
||||
}
|
||||
|
||||
let integer_scale = (scale.x.max(scale.y) * buffer_scale).ceil() as u32;
|
||||
let frame = state
|
||||
.get_named_cursor(current_cursor)
|
||||
.get_image(integer_scale, time.as_millis());
|
||||
let actual_scale = (frame.size / state.size()).max(1);
|
||||
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);
|
||||
|
||||
let pointer_images = &mut state.image_cache;
|
||||
let maybe_image = pointer_images
|
||||
.iter()
|
||||
.find_map(|(image, texture)| if image == &frame { Some(texture) } else { None });
|
||||
let pointer_image = match maybe_image {
|
||||
Some(image) => image,
|
||||
// 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);
|
||||
|
||||
// Rasterize and upload this (shape, size, frame) only if not cached.
|
||||
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(
|
||||
&frame.pixels_rgba,
|
||||
&image.pixels_rgba,
|
||||
Fourcc::Argb8888,
|
||||
(frame.width as i32, frame.height as i32),
|
||||
(image.width as i32, image.height as i32),
|
||||
actual_scale as i32,
|
||||
Transform::Normal,
|
||||
None,
|
||||
);
|
||||
pointer_images.push((frame.clone(), buffer));
|
||||
pointer_images.last().map(|(_, i)| i).unwrap()
|
||||
state.image_cache.push(CachedFrame {
|
||||
key,
|
||||
image,
|
||||
buffer,
|
||||
unmagnified: size_px == unmagnified_px,
|
||||
});
|
||||
state.image_cache.len() - 1
|
||||
}
|
||||
};
|
||||
|
||||
let (frame, pointer_image) = {
|
||||
let entry = &mut state.image_cache[index];
|
||||
entry.unmagnified |= size_px == unmagnified_px;
|
||||
(entry.image.clone(), entry.buffer.clone())
|
||||
};
|
||||
let actual_scale = (frame.size / state.size()).max(1);
|
||||
|
||||
let hotspot = Point::<i32, BufferCoords>::from((frame.xhot as i32, frame.yhot as i32))
|
||||
.to_logical(
|
||||
actual_scale as i32,
|
||||
|
|
@ -402,7 +820,7 @@ pub fn draw_cursor<R>(
|
|||
MemoryRenderBufferRenderElement::from_buffer(
|
||||
renderer,
|
||||
location.to_physical(scale),
|
||||
pointer_image,
|
||||
&pointer_image,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@ where
|
|||
Workspace(
|
||||
RelocateRenderElement<CropRenderElement<RescaleRenderElement<WorkspaceRenderElement<R>>>>,
|
||||
),
|
||||
Cursor(RescaleRenderElement<RelocateRenderElement<CursorRenderElement<R>>>),
|
||||
Cursor(
|
||||
RescaleRenderElement<RescaleRenderElement<RelocateRenderElement<CursorRenderElement<R>>>>,
|
||||
),
|
||||
Dnd(SurfaceRenderElement<R>),
|
||||
MoveGrab(RescaleRenderElement<CosmicMappedRenderElement<R>>),
|
||||
Postprocess(
|
||||
|
|
|
|||
|
|
@ -510,23 +510,36 @@ pub fn cursor_elements<'a, 'frame, R>(
|
|||
};
|
||||
let location = pointer.current_location() - output.current_location().to_f64();
|
||||
|
||||
// Shake-to-find magnification, applied around the pointer tip.
|
||||
let cursor_magnification = seat
|
||||
.user_data()
|
||||
.get::<cursor::CursorState>()
|
||||
.map_or(1.0, |s| {
|
||||
s.lock().unwrap().animated_magnification(Instant::now())
|
||||
});
|
||||
let cursor_center = location.to_physical(scale).to_i32_round();
|
||||
|
||||
if mode != CursorMode::None {
|
||||
cursor::draw_cursor(
|
||||
renderer,
|
||||
seat,
|
||||
location,
|
||||
scale.into(),
|
||||
zoom_scale,
|
||||
zoom_scale * cursor_magnification as f64,
|
||||
now,
|
||||
blur_strength,
|
||||
mode != CursorMode::NotDefault,
|
||||
&mut |elem, hotspot| {
|
||||
push(CosmicElement::Cursor(RescaleRenderElement::from_element(
|
||||
RescaleRenderElement::from_element(
|
||||
RelocateRenderElement::from_element(
|
||||
elem,
|
||||
Point::from((-hotspot.x, -hotspot.y)),
|
||||
Relocate::Relative,
|
||||
),
|
||||
cursor_center,
|
||||
cursor_magnification as f64,
|
||||
),
|
||||
focal_point
|
||||
.as_logical()
|
||||
.to_physical(output.current_scale().fractional_scale())
|
||||
|
|
|
|||
|
|
@ -969,6 +969,10 @@ fn config_changed(config: cosmic_config::Config, keys: Vec<String>, state: &mut
|
|||
}
|
||||
}
|
||||
}
|
||||
"cursor_shake_to_find" => {
|
||||
let new = get_config::<bool>(&config, "cursor_shake_to_find");
|
||||
state.common.config.cosmic_conf.cursor_shake_to_find = new;
|
||||
}
|
||||
"cursor_hide_timeout" => {
|
||||
let new = get_config::<Option<u32>>(&config, "cursor_hide_timeout");
|
||||
if new != state.common.config.cosmic_conf.cursor_hide_timeout {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ use std::{os::unix::process::CommandExt, thread};
|
|||
|
||||
use super::gestures;
|
||||
|
||||
const MAX_ZOOM: f64 = 256.0;
|
||||
|
||||
fn propagate_by_default(action: &shortcuts::Action) -> bool {
|
||||
matches!(
|
||||
action,
|
||||
|
|
@ -1116,7 +1118,14 @@ impl State {
|
|||
}
|
||||
|
||||
if zoom_seat == *seat {
|
||||
let new_level = (current_level + change).max(1.0);
|
||||
let factor = 1.0 + change.abs();
|
||||
let new_level = if change < 0. {
|
||||
current_level / factor
|
||||
} else {
|
||||
current_level * factor
|
||||
}
|
||||
.clamp(1.0, MAX_ZOOM);
|
||||
let new_level = if new_level < 1.01 { 1.0 } else { new_level };
|
||||
shell.trigger_zoom(
|
||||
seat,
|
||||
Some(&output),
|
||||
|
|
|
|||
|
|
@ -337,6 +337,21 @@ impl State {
|
|||
notify_cursor_activity(self, &seat);
|
||||
let current_output = seat.active_output();
|
||||
|
||||
if self.common.config.cosmic_conf.cursor_shake_to_find
|
||||
&& let Some(cursor_state) =
|
||||
seat.user_data()
|
||||
.get::<crate::backend::render::cursor::CursorState>()
|
||||
{
|
||||
let active = {
|
||||
let mut cursor = cursor_state.lock().unwrap();
|
||||
cursor.detect_shake(event.delta(), std::time::Instant::now());
|
||||
cursor.is_magnifying()
|
||||
};
|
||||
if active {
|
||||
self.backend.schedule_render(¤t_output);
|
||||
}
|
||||
}
|
||||
|
||||
let mut position = seat.get_pointer().unwrap().current_location().as_global();
|
||||
|
||||
let under = State::surface_under(position, ¤t_output, &shell)
|
||||
|
|
|
|||
|
|
@ -1614,6 +1614,29 @@ impl Common {
|
|||
a11y_keyboard_monitor.refresh();
|
||||
}
|
||||
self.image_copy_capture_state.cleanup();
|
||||
self.cleanup_cursor_images();
|
||||
}
|
||||
|
||||
/// Release the enlarged cursor frames a finished shake or zoom left behind.
|
||||
fn cleanup_cursor_images(&mut self) {
|
||||
let shell = self.shell.read();
|
||||
let zoomed = shell.zoom_state.as_ref().is_some_and(|zoom_state| {
|
||||
shell
|
||||
.outputs()
|
||||
.any(|output| zoom_state.animating_level(output) > 1.0)
|
||||
});
|
||||
let now = Instant::now();
|
||||
for seat in shell.seats.iter() {
|
||||
if let Some(cursor_state) = seat
|
||||
.user_data()
|
||||
.get::<crate::backend::render::cursor::CursorState>()
|
||||
{
|
||||
cursor_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.drop_magnified_frames(now, zoomed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_idle_inhibit(&mut self) {
|
||||
|
|
@ -2286,6 +2309,11 @@ impl Shell {
|
|||
.is_some_and(|state| state.lock().unwrap().is_animating())
|
||||
})
|
||||
})
|
||||
|| self.seats.iter().any(|seat| {
|
||||
seat.user_data()
|
||||
.get::<crate::backend::render::cursor::CursorState>()
|
||||
.is_some_and(|state| state.lock().unwrap().is_magnifying())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_animations(&mut self) -> HashMap<ClientId, Client> {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use cosmic::{
|
|||
theme,
|
||||
widget::{self, icon::Named},
|
||||
};
|
||||
use cosmic_comp_config::ZoomMovement;
|
||||
use cosmic_comp_config::{ZoomConfig, ZoomMovement};
|
||||
use cosmic_config::ConfigSet;
|
||||
use keyframe::{ease, functions::Linear};
|
||||
use smithay::{
|
||||
|
|
@ -447,7 +447,7 @@ pub enum MenuMessage {
|
|||
|
||||
impl ZoomProgram {
|
||||
pub fn new(level: f64, movement: ZoomMovement, increment: u32) -> Self {
|
||||
let mut increments = vec![25, 50, 100, 150, 200];
|
||||
let mut increments = ZoomConfig::ZOOM_INCREMENT_PRESETS.to_vec();
|
||||
if !increments.contains(&increment) {
|
||||
increments.push(increment);
|
||||
}
|
||||
|
|
@ -829,7 +829,7 @@ impl Program for ZoomProgram {
|
|||
if let Some(pos) = self.increments.iter().position(|val| *val == increment) {
|
||||
self.increment_idx = pos;
|
||||
} else {
|
||||
let mut increments = vec![25, 50, 100, 150, 200];
|
||||
let mut increments = ZoomConfig::ZOOM_INCREMENT_PRESETS.to_vec();
|
||||
if !increments.contains(&increment) {
|
||||
increments.push(increment);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,11 @@ impl SessionLockHandler for State {
|
|||
let mut shell = self.common.shell.write();
|
||||
shell.session_lock = None;
|
||||
|
||||
let seats = shell.seats.iter().cloned().collect::<Vec<_>>();
|
||||
for seat in &seats {
|
||||
self.common.idle_notifier_state.notify_activity(seat);
|
||||
}
|
||||
|
||||
for output in shell.outputs() {
|
||||
self.backend.schedule_render(output);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue