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,
|
||||
/// 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(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use crate::{
|
|||
utils::prelude::*,
|
||||
wayland::handlers::compositor::FRAME_TIME_FILTER,
|
||||
};
|
||||
use keyframe::{ease, functions::EaseInOutCubic};
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::Fourcc,
|
||||
|
|
@ -38,7 +39,7 @@ use smithay::{
|
|||
wayland::compositor::{get_role, with_states},
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::{HashMap, VecDeque},
|
||||
io::Read,
|
||||
sync::Mutex,
|
||||
time::{Duration, Instant},
|
||||
|
|
@ -270,6 +271,43 @@ pub struct CursorStateInner {
|
|||
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>,
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
|
@ -290,6 +328,116 @@ impl CursorStateInner {
|
|||
pub fn size(&self) -> u32 {
|
||||
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) {
|
||||
|
|
@ -324,6 +472,14 @@ 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,22 +510,35 @@ 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(
|
||||
RelocateRenderElement::from_element(
|
||||
elem,
|
||||
Point::from((-hotspot.x, -hotspot.y)),
|
||||
Relocate::Relative,
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -2286,6 +2286,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> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue