Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions crates/rendering/src/cursor_interpolation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,10 +340,17 @@ struct CursorTimelineVariant {
timeline: Arc<PrecomputedCursorTimeline>,
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct CursorEventOrder {
pub moves: bool,
pub clicks: bool,
}

pub struct PrecomputedCursorTimeline {
timeline: Vec<SmoothedCursorEvent>,
raw_cursor: CursorEvents,
raw_moves_are_ordered: bool,
clicks_are_ordered: bool,
has_smoothing: bool,
variants: Mutex<Vec<CursorTimelineVariant>>,
}
Expand All @@ -355,11 +362,19 @@ impl PrecomputedCursorTimeline {
click_spring: Option<ClickSpringConfig>,
) -> Self {
let raw_moves_are_ordered = cursor.moves.is_sorted_by(|a, b| a.time_ms <= b.time_ms);
let clicks_are_ordered = cursor
.clicks
.iter()
.try_fold(f64::NEG_INFINITY, |previous, click| {
(previous <= click.time_ms).then_some(click.time_ms)
})
.is_some();
if cursor.moves.is_empty() || smoothing.is_none() {
return Self {
timeline: vec![],
raw_cursor: cursor.clone(),
raw_moves_are_ordered,
clicks_are_ordered,
has_smoothing: false,
variants: Mutex::new(Vec::new()),
};
Expand All @@ -379,6 +394,7 @@ impl PrecomputedCursorTimeline {
timeline,
raw_cursor: CursorEvents::default(),
raw_moves_are_ordered,
clicks_are_ordered,
has_smoothing: true,
variants: Mutex::new(Vec::new()),
}
Expand Down Expand Up @@ -417,6 +433,13 @@ impl PrecomputedCursorTimeline {
timeline
}

pub(crate) fn event_order(&self) -> CursorEventOrder {
CursorEventOrder {
moves: self.raw_moves_are_ordered,
clicks: self.clicks_are_ordered,
}
}

pub fn interpolate(&self, time_secs: f32) -> Option<InterpolatedCursorPosition> {
let time_ms = (time_secs * 1000.0) as f64;
if self.has_smoothing {
Expand Down
188 changes: 165 additions & 23 deletions crates/rendering/src/layers/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ impl CursorLayer {
cursor,
segment_frames.recording_time as f64 * 1000.0,
hide_delay_ms,
uniforms.cursor_event_order.moves,
);
if cursor_opacity <= f32::EPSILON {
cursor_opacity = 0.0;
Expand Down Expand Up @@ -568,7 +569,11 @@ impl CursorLayer {
};

let size = {
let click_t = get_click_t(&cursor.clicks, (time_s as f64) * 1000.0);
let click_t = get_click_t(
&cursor.clicks,
(time_s as f64) * 1000.0,
uniforms.cursor_event_order.clicks,
);
let click_scale_factor = click_t * 1.0 + (1.0 - click_t) * CLICK_SHRINK_SIZE;
let display = &uniforms.display;
let size = cursor_height_px(
Expand Down Expand Up @@ -770,6 +775,7 @@ fn compute_cursor_idle_opacity(
cursor: &CursorEvents,
current_time_ms: f64,
hide_delay_ms: f64,
moves_ordered: bool,
) -> f32 {
if cursor.moves.is_empty() {
return 0.0;
Expand All @@ -791,11 +797,15 @@ fn compute_cursor_idle_opacity(
return 1.0;
}

let Some(last_index) = cursor
.moves
.iter()
.rposition(|event| event.time_ms <= current_time_ms)
else {
let last_index = if moves_ordered {
idx.checked_sub(1)
} else {
cursor
.moves
.iter()
.rposition(|event| event.time_ms <= current_time_ms)
};
let Some(last_index) = last_index else {
return 1.0;
};

Expand All @@ -815,7 +825,12 @@ fn compute_cursor_idle_opacity(
return 0.0;
}

let opacity = compute_cursor_fade_in(cursor, current_time_ms, hide_delay_ms);
let elapsed_moves = if moves_ordered {
&cursor.moves[..idx]
} else {
&cursor.moves[..]
};
let opacity = compute_cursor_fade_in(elapsed_moves, current_time_ms, hide_delay_ms);
(opacity * fade_out).clamp(0.0, 1.0)
}

Expand All @@ -828,9 +843,12 @@ fn smoothstep64(edge0: f64, edge1: f64, x: f64) -> f64 {
t * t * (3.0 - 2.0 * t)
}

fn compute_cursor_fade_in(cursor: &CursorEvents, current_time_ms: f64, hide_delay_ms: f64) -> f32 {
let resume_time = cursor
.moves
fn compute_cursor_fade_in(
moves: &[CursorMoveEvent],
current_time_ms: f64,
hide_delay_ms: f64,
) -> f32 {
let resume_time = moves
.windows(2)
.rev()
.find(|pair| {
Expand All @@ -849,13 +867,17 @@ fn compute_cursor_fade_in(cursor: &CursorEvents, current_time_ms: f64, hide_dela
smoothstep64(0.0, CURSOR_IDLE_FADE_OUT_MS, time_since_resume) as f32
}

fn get_click_t(clicks: &[CursorClickEvent], time_ms: f64) -> f32 {
fn get_click_t(clicks: &[CursorClickEvent], time_ms: f64, clicks_ordered: bool) -> f32 {
fn smoothstep(low: f32, high: f32, v: f32) -> f32 {
let t = f32::clamp((v - low) / (high - low), 0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}

let next_click = clicks.iter().find(|c| c.time_ms > time_ms);
let split = clicks_ordered.then(|| clicks.partition_point(|c| c.time_ms <= time_ms));
let next_click = match split {
Some(split) => clicks.get(split).filter(|c| c.time_ms > time_ms),
None => clicks.iter().find(|c| c.time_ms > time_ms),
};

if let Some(next) = next_click {
if next.down && next.time_ms - time_ms <= CURSOR_CLICK_DURATION_MS {
Expand All @@ -871,7 +893,10 @@ fn get_click_t(clicks: &[CursorClickEvent], time_ms: f64) -> f32 {
}
}

let prev_click = clicks.iter().rev().find(|c| c.time_ms <= time_ms);
let prev_click = match split {
Some(split) => clicks[..split].last(),
None => clicks.iter().rev().find(|c| c.time_ms <= time_ms),
};

if let Some(prev) = prev_click {
if prev.down {
Expand Down Expand Up @@ -1356,7 +1381,7 @@ mod tests {
fn opacity_stays_visible_with_recent_move() {
let cursor = cursor_events(&[(0.0, 0.0, 0.0), (1500.0, 0.1, 0.1)]);

let opacity = compute_cursor_idle_opacity(&cursor, 2000.0, 2000.0);
let opacity = compute_cursor_idle_opacity(&cursor, 2000.0, 2000.0, true);

assert_eq!(opacity, 1.0);
}
Expand All @@ -1365,7 +1390,7 @@ mod tests {
fn opacity_fades_once_past_delay() {
let cursor = cursor_events(&[(0.0, 0.0, 0.0)]);

let opacity = compute_cursor_idle_opacity(&cursor, 3000.0, 1000.0);
let opacity = compute_cursor_idle_opacity(&cursor, 3000.0, 1000.0, true);

assert_eq!(opacity, 0.0);
}
Expand All @@ -1376,28 +1401,39 @@ mod tests {

let hide_delay_ms = 2000.0;

let at_resume = compute_cursor_idle_opacity(&cursor, 5000.0, hide_delay_ms);
let at_resume = compute_cursor_idle_opacity(&cursor, 5000.0, hide_delay_ms, true);
assert_eq!(at_resume, 0.0);

let halfway = compute_cursor_idle_opacity(
&cursor,
5000.0 + CURSOR_IDLE_FADE_OUT_MS / 2.0,
hide_delay_ms,
true,
);
assert!((halfway - 0.5).abs() < 0.05);

let after_fade = compute_cursor_idle_opacity(
&cursor,
5000.0 + CURSOR_IDLE_FADE_OUT_MS * 2.0,
hide_delay_ms,
true,
);
assert_eq!(after_fade, 1.0);
}

fn idle_opacity_both_paths(cursor: &CursorEvents, time_ms: f64, hide_delay_ms: f64) -> f32 {
let unordered = compute_cursor_idle_opacity(cursor, time_ms, hide_delay_ms, false);
if cursor.moves.is_sorted_by(|a, b| a.time_ms <= b.time_ms) {
let ordered = compute_cursor_idle_opacity(cursor, time_ms, hide_delay_ms, true);
assert_eq!(ordered.to_bits(), unordered.to_bits());
}
unordered
}

#[test]
fn opacity_handles_empty_history_and_duplicate_timestamps() {
assert_eq!(
compute_cursor_idle_opacity(&CursorEvents::default(), 0.0, 1000.0),
idle_opacity_both_paths(&CursorEvents::default(), 0.0, 1000.0),
0.0
);
let cursor = cursor_events(&[
Expand All @@ -1407,7 +1443,7 @@ mod tests {
(5010.0, 0.7, 0.7),
]);
for (time, expected) in [(0.0, 1.0), (5000.0, 1.0), (5200.0, 0.5), (5400.0, 1.0)] {
assert_eq!(compute_cursor_idle_opacity(&cursor, time, 1000.0), expected);
assert_eq!(idle_opacity_both_paths(&cursor, time, 1000.0), expected);
}
}

Expand All @@ -1421,7 +1457,7 @@ mod tests {
(5000.0, 0.0),
(5400.0, 1.0),
] {
assert_eq!(compute_cursor_idle_opacity(&cursor, time, 1000.0), expected);
assert_eq!(idle_opacity_both_paths(&cursor, time, 1000.0), expected);
}
}

Expand All @@ -1443,22 +1479,128 @@ mod tests {
(10400.0, 1.0),
(5200.0, 0.5),
] {
assert_eq!(compute_cursor_idle_opacity(&cursor, time, 1000.0), expected);
assert_eq!(idle_opacity_both_paths(&cursor, time, 1000.0), expected);
}
}

#[test]
fn opacity_preserves_fade_out_boundary() {
let cursor = cursor_events(&[(0.0, 0.0, 0.0)]);
for (time, expected) in [(1000.0, 1.0), (1200.0, 0.5), (1400.0, 0.0), (1400.001, 0.0)] {
assert_eq!(compute_cursor_idle_opacity(&cursor, time, 1000.0), expected);
assert_eq!(idle_opacity_both_paths(&cursor, time, 1000.0), expected);
}
assert!(compute_cursor_idle_opacity(&cursor, 1399.999, 1000.0) > 0.0);
assert!(idle_opacity_both_paths(&cursor, 1399.999, 1000.0) > 0.0);
}

#[test]
fn opacity_preserves_recent_resume_for_out_of_order_history() {
let cursor = cursor_events(&[(0.0, 0.0, 0.0), (5000.0, 0.5, 0.5), (4000.0, 0.6, 0.6)]);
assert_eq!(compute_cursor_idle_opacity(&cursor, 5200.0, 1000.0), 0.25);
assert_eq!(idle_opacity_both_paths(&cursor, 5200.0, 1000.0), 0.25);
}

fn click_event(time_ms: f64, down: bool) -> CursorClickEvent {
CursorClickEvent {
active_modifiers: vec![],
cursor_num: 0,
cursor_id: "pointer".into(),
time_ms,
down,
}
}

fn event_time_cases() -> Vec<Vec<f64>> {
vec![
vec![],
vec![0.0],
vec![f64::NAN],
vec![0.0, 0.0, 10.0, 10.0, 20.0, 100.0, 100.0],
vec![-0.0, 0.0, 0.0, 10.0, 100.0],
vec![0.0, 20.0, 10.0, 50.0, 100.0],
vec![0.0, 10.0, f64::NAN, 30.0, 100.0],
vec![f64::NAN, 10.0, 30.0, 100.0],
vec![0.0, 10.0, f64::NAN],
vec![f64::NEG_INFINITY, 0.0, 10.0, f64::INFINITY],
vec![0.0, f64::INFINITY, 10.0, 100.0],
vec![
0.0, 16.0, 400.0, 1200.0, 1300.0, 3000.0, 3100.0, 3150.0, 6000.0, 6000.0, 9000.0,
],
]
}

fn lookup_queries(times: &[f64]) -> Vec<f64> {
let mut queries = vec![
f64::NAN,
f64::NEG_INFINITY,
f64::INFINITY,
-1.0,
-0.0,
0.0,
1.0,
1.0e9,
];
for &time in times {
for offset in [
0.0,
CURSOR_CLICK_DURATION_MS,
CURSOR_IDLE_RESUME_LOOKAHEAD_MS,
CURSOR_IDLE_FADE_OUT_MS,
500.0,
1000.0,
2000.0,
] {
for base in [time - offset, time + offset] {
queries.extend([base.next_down(), base, base.next_up()]);
}
}
}
queries
}

#[test]
fn ordered_cursor_lookups_match_linear_scans() {
let mut ordered_moves = 0;
let mut ordered_clicks = 0;
for times in event_time_cases() {
let mut cursor = cursor_events(
&times
.iter()
.enumerate()
.map(|(index, &time)| (time, index as f64 * 0.1, 0.5))
.collect::<Vec<_>>(),
);
for all_down in [false, true] {
cursor.clicks = times
.iter()
.enumerate()
.map(|(index, &time)| click_event(time, all_down || index % 2 == 0))
.collect();
let order = crate::cursor_interpolation::PrecomputedCursorTimeline::new(
&cursor, None, None,
)
.event_order();
ordered_moves += usize::from(order.moves);
ordered_clicks += usize::from(order.clicks);
for time in lookup_queries(&times) {
if order.clicks {
assert_eq!(
get_click_t(&cursor.clicks, time, true).to_bits(),
get_click_t(&cursor.clicks, time, false).to_bits(),
"{times:?} clicks at {time}"
);
}
if order.moves {
for delay in [500.0, 1000.0, 2000.0, 2000.5] {
assert_eq!(
compute_cursor_idle_opacity(&cursor, time, delay, true).to_bits(),
compute_cursor_idle_opacity(&cursor, time, delay, false).to_bits(),
"{times:?} moves at {time} with delay {delay}"
);
}
}
}
}
}
assert_eq!(ordered_moves, 14);
assert_eq!(ordered_clicks, 12);
}
}
Loading
Loading