feat: shake cursor to find by magnifying the cursor
This commit is contained in:
parent
04026259e0
commit
f20de50e41
7 changed files with 205 additions and 7 deletions
|
|
@ -101,6 +101,8 @@ pub struct CosmicCompConfig {
|
||||||
pub appearance_settings: AppearanceConfig,
|
pub appearance_settings: AppearanceConfig,
|
||||||
/// Hide the cursor after this many seconds of pointer inactivity (None disables)
|
/// Hide the cursor after this many seconds of pointer inactivity (None disables)
|
||||||
pub cursor_hide_timeout: Option<u32>,
|
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,
|
pub activation_policy: ActivationPolicy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,6 +141,7 @@ impl Default for CosmicCompConfig {
|
||||||
accessibility_zoom: ZoomConfig::default(),
|
accessibility_zoom: ZoomConfig::default(),
|
||||||
appearance_settings: AppearanceConfig::default(),
|
appearance_settings: AppearanceConfig::default(),
|
||||||
cursor_hide_timeout: None,
|
cursor_hide_timeout: None,
|
||||||
|
cursor_shake_to_find: true,
|
||||||
activation_policy: ActivationPolicy::default(),
|
activation_policy: ActivationPolicy::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use crate::{
|
||||||
utils::prelude::*,
|
utils::prelude::*,
|
||||||
wayland::handlers::compositor::FRAME_TIME_FILTER,
|
wayland::handlers::compositor::FRAME_TIME_FILTER,
|
||||||
};
|
};
|
||||||
|
use keyframe::{ease, functions::EaseInOutCubic};
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::{
|
backend::{
|
||||||
allocator::Fourcc,
|
allocator::Fourcc,
|
||||||
|
|
@ -38,7 +39,7 @@ use smithay::{
|
||||||
wayland::compositor::{get_role, with_states},
|
wayland::compositor::{get_role, with_states},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{HashMap, VecDeque},
|
||||||
io::Read,
|
io::Read,
|
||||||
sync::Mutex,
|
sync::Mutex,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
|
|
@ -270,6 +271,43 @@ pub struct CursorStateInner {
|
||||||
hidden: bool,
|
hidden: bool,
|
||||||
idle_timer: Option<RegistrationToken>,
|
idle_timer: Option<RegistrationToken>,
|
||||||
last_armed: Option<Instant>,
|
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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One sampled pointer position on the recent motion path.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct PathSample {
|
||||||
|
position: Point<f64, Logical>,
|
||||||
|
time: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
/// 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 {
|
impl CursorStateInner {
|
||||||
|
|
@ -290,6 +328,116 @@ impl CursorStateInner {
|
||||||
pub fn size(&self) -> u32 {
|
pub fn size(&self) -> u32 {
|
||||||
self.cursor_size
|
self.cursor_size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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) {
|
pub fn load_cursor_env() -> (String, u32) {
|
||||||
|
|
@ -324,6 +472,14 @@ impl Default for CursorStateInner {
|
||||||
hidden: false,
|
hidden: false,
|
||||||
idle_timer: None,
|
idle_timer: None,
|
||||||
last_armed: 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,9 @@ where
|
||||||
Workspace(
|
Workspace(
|
||||||
RelocateRenderElement<CropRenderElement<RescaleRenderElement<WorkspaceRenderElement<R>>>>,
|
RelocateRenderElement<CropRenderElement<RescaleRenderElement<WorkspaceRenderElement<R>>>>,
|
||||||
),
|
),
|
||||||
Cursor(RescaleRenderElement<RelocateRenderElement<CursorRenderElement<R>>>),
|
Cursor(
|
||||||
|
RescaleRenderElement<RescaleRenderElement<RelocateRenderElement<CursorRenderElement<R>>>>,
|
||||||
|
),
|
||||||
Dnd(SurfaceRenderElement<R>),
|
Dnd(SurfaceRenderElement<R>),
|
||||||
MoveGrab(RescaleRenderElement<CosmicMappedRenderElement<R>>),
|
MoveGrab(RescaleRenderElement<CosmicMappedRenderElement<R>>),
|
||||||
Postprocess(
|
Postprocess(
|
||||||
|
|
|
||||||
|
|
@ -510,22 +510,35 @@ pub fn cursor_elements<'a, 'frame, R>(
|
||||||
};
|
};
|
||||||
let location = pointer.current_location() - output.current_location().to_f64();
|
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 {
|
if mode != CursorMode::None {
|
||||||
cursor::draw_cursor(
|
cursor::draw_cursor(
|
||||||
renderer,
|
renderer,
|
||||||
seat,
|
seat,
|
||||||
location,
|
location,
|
||||||
scale.into(),
|
scale.into(),
|
||||||
zoom_scale,
|
zoom_scale * cursor_magnification as f64,
|
||||||
now,
|
now,
|
||||||
blur_strength,
|
blur_strength,
|
||||||
mode != CursorMode::NotDefault,
|
mode != CursorMode::NotDefault,
|
||||||
&mut |elem, hotspot| {
|
&mut |elem, hotspot| {
|
||||||
push(CosmicElement::Cursor(RescaleRenderElement::from_element(
|
push(CosmicElement::Cursor(RescaleRenderElement::from_element(
|
||||||
RelocateRenderElement::from_element(
|
RescaleRenderElement::from_element(
|
||||||
elem,
|
RelocateRenderElement::from_element(
|
||||||
Point::from((-hotspot.x, -hotspot.y)),
|
elem,
|
||||||
Relocate::Relative,
|
Point::from((-hotspot.x, -hotspot.y)),
|
||||||
|
Relocate::Relative,
|
||||||
|
),
|
||||||
|
cursor_center,
|
||||||
|
cursor_magnification as f64,
|
||||||
),
|
),
|
||||||
focal_point
|
focal_point
|
||||||
.as_logical()
|
.as_logical()
|
||||||
|
|
|
||||||
|
|
@ -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" => {
|
"cursor_hide_timeout" => {
|
||||||
let new = get_config::<Option<u32>>(&config, "cursor_hide_timeout");
|
let new = get_config::<Option<u32>>(&config, "cursor_hide_timeout");
|
||||||
if new != state.common.config.cosmic_conf.cursor_hide_timeout {
|
if new != state.common.config.cosmic_conf.cursor_hide_timeout {
|
||||||
|
|
|
||||||
|
|
@ -337,6 +337,21 @@ impl State {
|
||||||
notify_cursor_activity(self, &seat);
|
notify_cursor_activity(self, &seat);
|
||||||
let current_output = seat.active_output();
|
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 mut position = seat.get_pointer().unwrap().current_location().as_global();
|
||||||
|
|
||||||
let under = State::surface_under(position, ¤t_output, &shell)
|
let under = State::surface_under(position, ¤t_output, &shell)
|
||||||
|
|
|
||||||
|
|
@ -2286,6 +2286,11 @@ impl Shell {
|
||||||
.is_some_and(|state| state.lock().unwrap().is_animating())
|
.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> {
|
pub fn update_animations(&mut self) -> HashMap<ClientId, Client> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue