@@ -14,6 +14,8 @@ use crate::{
1414const CURSOR_CLICK_DURATION : f64 = 0.25 ;
1515const CURSOR_CLICK_DURATION_MS : f64 = CURSOR_CLICK_DURATION * 1000.0 ;
1616const CLICK_SHRINK_SIZE : f32 = 0.7 ;
17+ const CURSOR_IDLE_MIN_DELAY_MS : f64 = 500.0 ;
18+ const CURSOR_IDLE_FADE_OUT_MS : f64 = 400.0 ;
1719
1820/// The size to render the svg to.
1921static SVG_CURSOR_RASTERIZED_HEIGHT : u32 = 200 ;
@@ -212,6 +214,24 @@ impl CursorLayer {
212214 let speed = ( velocity[ 0 ] * velocity[ 0 ] + velocity[ 1 ] * velocity[ 1 ] ) . sqrt ( ) ;
213215 let motion_blur_amount = ( speed * 0.3 ) . min ( 1.0 ) * 0.0 ; // uniforms.project.cursor.motion_blur;
214216
217+ let mut cursor_opacity = 1.0f32 ;
218+ if uniforms. project . cursor . hide_when_idle && !cursor. moves . is_empty ( ) {
219+ let hide_delay_secs = uniforms
220+ . project
221+ . cursor
222+ . hide_when_idle_delay
223+ . max ( ( CURSOR_IDLE_MIN_DELAY_MS / 1000.0 ) as f32 ) ;
224+ let hide_delay_ms = ( hide_delay_secs as f64 * 1000.0 ) . max ( CURSOR_IDLE_MIN_DELAY_MS ) ;
225+ cursor_opacity = compute_cursor_idle_opacity (
226+ cursor,
227+ segment_frames. recording_time as f64 * 1000.0 ,
228+ hide_delay_ms,
229+ ) ;
230+ if cursor_opacity <= f32:: EPSILON {
231+ cursor_opacity = 0.0 ;
232+ }
233+ }
234+
215235 // Remove all cursor assets if the svg configuration changes.
216236 // it might change the texture.
217237 //
@@ -336,20 +356,27 @@ impl CursorLayer {
336356 zoom,
337357 ) - zoomed_position;
338358
339- let uniforms = CursorUniforms {
340- position : [ zoomed_position. x as f32 , zoomed_position. y as f32 ] ,
341- size : [ zoomed_size. x as f32 , zoomed_size. y as f32 ] ,
342- output_size : [ uniforms. output_size . 0 as f32 , uniforms. output_size . 1 as f32 ] ,
359+ let cursor_uniforms = CursorUniforms {
360+ position_size : [
361+ zoomed_position. x as f32 ,
362+ zoomed_position. y as f32 ,
363+ zoomed_size. x as f32 ,
364+ zoomed_size. y as f32 ,
365+ ] ,
366+ output_size : [
367+ uniforms. output_size . 0 as f32 ,
368+ uniforms. output_size . 1 as f32 ,
369+ 0.0 ,
370+ 0.0 ,
371+ ] ,
343372 screen_bounds : uniforms. display . target_bounds ,
344- velocity,
345- motion_blur_amount,
346- _alignment : [ 0.0 ; 3 ] ,
373+ velocity_blur_opacity : [ velocity[ 0 ] , velocity[ 1 ] , motion_blur_amount, cursor_opacity] ,
347374 } ;
348375
349376 constants. queue . write_buffer (
350377 & self . statics . uniform_buffer ,
351378 0 ,
352- bytemuck:: cast_slice ( & [ uniforms ] ) ,
379+ bytemuck:: cast_slice ( & [ cursor_uniforms ] ) ,
353380 ) ;
354381
355382 self . bind_group = Some (
@@ -367,16 +394,149 @@ impl CursorLayer {
367394 }
368395}
369396
370- #[ repr( C , align ( 16 ) ) ]
397+ #[ repr( C ) ]
371398#[ derive( Debug , Clone , Copy , Pod , Zeroable , Default ) ]
372399pub struct CursorUniforms {
373- position : [ f32 ; 2 ] ,
374- size : [ f32 ; 2 ] ,
375- output_size : [ f32 ; 2 ] ,
400+ position_size : [ f32 ; 4 ] ,
401+ output_size : [ f32 ; 4 ] ,
376402 screen_bounds : [ f32 ; 4 ] ,
377- velocity : [ f32 ; 2 ] ,
378- motion_blur_amount : f32 ,
379- _alignment : [ f32 ; 3 ] ,
403+ velocity_blur_opacity : [ f32 ; 4 ] ,
404+ }
405+
406+ fn compute_cursor_idle_opacity (
407+ cursor : & CursorEvents ,
408+ current_time_ms : f64 ,
409+ hide_delay_ms : f64 ,
410+ ) -> f32 {
411+ if cursor. moves . is_empty ( ) {
412+ return 0.0 ;
413+ }
414+
415+ if current_time_ms <= cursor. moves [ 0 ] . time_ms {
416+ return 1.0 ;
417+ }
418+
419+ let Some ( last_index) = cursor
420+ . moves
421+ . iter ( )
422+ . rposition ( |event| event. time_ms <= current_time_ms)
423+ else {
424+ return 1.0 ;
425+ } ;
426+
427+ let last_move = & cursor. moves [ last_index] ;
428+
429+ let time_since_move = ( current_time_ms - last_move. time_ms ) . max ( 0.0 ) ;
430+
431+ let mut opacity = compute_cursor_fade_in ( cursor, current_time_ms, hide_delay_ms) ;
432+
433+ let fade_out = if time_since_move <= hide_delay_ms {
434+ 1.0
435+ } else {
436+ let delta = time_since_move - hide_delay_ms;
437+ let fade = 1.0 - smoothstep64 ( 0.0 , CURSOR_IDLE_FADE_OUT_MS , delta) ;
438+ fade. clamp ( 0.0 , 1.0 ) as f32
439+ } ;
440+
441+ opacity *= fade_out;
442+ opacity. clamp ( 0.0 , 1.0 )
443+ }
444+
445+ fn smoothstep64 ( edge0 : f64 , edge1 : f64 , x : f64 ) -> f64 {
446+ if edge1 <= edge0 {
447+ return if x < edge0 { 0.0 } else { 1.0 } ;
448+ }
449+
450+ let t = ( ( x - edge0) / ( edge1 - edge0) ) . clamp ( 0.0 , 1.0 ) ;
451+ t * t * ( 3.0 - 2.0 * t)
452+ }
453+
454+ fn compute_cursor_fade_in ( cursor : & CursorEvents , current_time_ms : f64 , hide_delay_ms : f64 ) -> f32 {
455+ let resume_time = cursor
456+ . moves
457+ . windows ( 2 )
458+ . rev ( )
459+ . find ( |pair| {
460+ let prev = & pair[ 0 ] ;
461+ let next = & pair[ 1 ] ;
462+ next. time_ms <= current_time_ms && next. time_ms - prev. time_ms > hide_delay_ms
463+ } )
464+ . map ( |pair| pair[ 1 ] . time_ms ) ;
465+
466+ let Some ( resume_time_ms) = resume_time else {
467+ return 1.0 ;
468+ } ;
469+
470+ let time_since_resume = ( current_time_ms - resume_time_ms) . max ( 0.0 ) ;
471+
472+ smoothstep64 ( 0.0 , CURSOR_IDLE_FADE_OUT_MS , time_since_resume) as f32
473+ }
474+
475+ #[ cfg( test) ]
476+ mod tests {
477+ use super :: * ;
478+
479+ fn move_event ( time_ms : f64 , x : f64 , y : f64 ) -> CursorMoveEvent {
480+ CursorMoveEvent {
481+ active_modifiers : vec ! [ ] ,
482+ cursor_id : "pointer" . into ( ) ,
483+ time_ms,
484+ x,
485+ y,
486+ }
487+ }
488+
489+ fn cursor_events ( times : & [ ( f64 , f64 , f64 ) ] ) -> CursorEvents {
490+ CursorEvents {
491+ moves : times
492+ . iter ( )
493+ . map ( |( time, x, y) | move_event ( * time, * x, * y) )
494+ . collect ( ) ,
495+ clicks : vec ! [ ] ,
496+ }
497+ }
498+
499+ #[ test]
500+ fn opacity_stays_visible_with_recent_move ( ) {
501+ let cursor = cursor_events ( & [ ( 0.0 , 0.0 , 0.0 ) , ( 1500.0 , 0.1 , 0.1 ) ] ) ;
502+
503+ let opacity = compute_cursor_idle_opacity ( & cursor, 2000.0 , 2000.0 ) ;
504+
505+ assert_eq ! ( opacity, 1.0 ) ;
506+ }
507+
508+ #[ test]
509+ fn opacity_fades_once_past_delay ( ) {
510+ let cursor = cursor_events ( & [ ( 0.0 , 0.0 , 0.0 ) ] ) ;
511+
512+ let opacity = compute_cursor_idle_opacity ( & cursor, 3000.0 , 1000.0 ) ;
513+
514+ assert_eq ! ( opacity, 0.0 ) ;
515+ }
516+
517+ #[ test]
518+ fn opacity_fades_in_after_long_inactivity ( ) {
519+ let cursor = cursor_events ( & [ ( 0.0 , 0.0 , 0.0 ) , ( 5000.0 , 0.5 , 0.5 ) ] ) ;
520+
521+ let hide_delay_ms = 2000.0 ;
522+
523+ let at_resume = compute_cursor_idle_opacity ( & cursor, 5000.0 , hide_delay_ms) ;
524+ assert_eq ! ( at_resume, 0.0 ) ;
525+
526+ let halfway = compute_cursor_idle_opacity (
527+ & cursor,
528+ 5000.0 + CURSOR_IDLE_FADE_OUT_MS / 2.0 ,
529+ hide_delay_ms,
530+ ) ;
531+ assert ! ( ( halfway - 0.5 ) . abs( ) < 0.05 ) ;
532+
533+ let after_fade = compute_cursor_idle_opacity (
534+ & cursor,
535+ 5000.0 + CURSOR_IDLE_FADE_OUT_MS * 2.0 ,
536+ hide_delay_ms,
537+ ) ;
538+ assert_eq ! ( after_fade, 1.0 ) ;
539+ }
380540}
381541
382542fn get_click_t ( clicks : & [ CursorClickEvent ] , time_ms : f64 ) -> f32 {
0 commit comments