From 8f4b40774872859e01c7764a60f9443b1255353a Mon Sep 17 00:00:00 2001 From: Aditya Garud <153842990+yashranaway@users.noreply.github.com> Date: Sun, 1 Nov 2026 08:02:51 +0000 Subject: [PATCH 1/2] fix(desktop-gpui): keep other tracks aligned when changing clip speed after a fullscreen title --- apps/desktop-gpui/src/editor_edits.rs | 73 +++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/apps/desktop-gpui/src/editor_edits.rs b/apps/desktop-gpui/src/editor_edits.rs index 13631e4537f..c2aebecd2ff 100644 --- a/apps/desktop-gpui/src/editor_edits.rs +++ b/apps/desktop-gpui/src/editor_edits.rs @@ -2558,6 +2558,7 @@ pub fn set_clip_segment_timescale( let old_duration = clip_timeline_duration(timeline); let old_offsets = editor_timeline::clip_timeline_offsets(timeline); + let holds = timeline.hold_windows(); let incoming_duration = timeline .effective_transition(index) .map_or(0.0, |transition| transition.duration); @@ -2571,9 +2572,10 @@ pub fn set_clip_segment_timescale( let old_next_boundary = old_offsets.get(index + 1).copied().unwrap_or(old_duration); let new_next_boundary = new_offsets.get(index + 1).copied().unwrap_or(new_duration); + let effective = |time: f64| time - held_time_before(&holds, time); let shift = |time: f64| { timeline_shift_after_clip_duration_change( - time, + effective(time), old_offsets[index], new_offsets[index], absolute_start, @@ -2598,14 +2600,26 @@ pub fn set_clip_segment_timescale( segment.start += shift(segment.start); segment.end += shift(segment.end); } - let bends = [old_offsets[index], absolute_start, old_next_boundary]; + // Track times include fullscreen-title holds, during which the clip clock + // stands still, so the remap also bends at every hold edge it spans. + let retimed = old_offsets[index]..=old_next_boundary; + let bends: Vec = [old_offsets[index], absolute_start, old_next_boundary] + .map(|bend| effective_to_output(&holds, bend)) + .into_iter() + .chain( + holds + .iter() + .flat_map(|&(start, end)| [start, end]) + .filter(|&edge| retimed.contains(&effective(edge))), + ) + .collect(); for segment in &mut timeline.mask_segments { let old_start = segment.start; let start_shift = shift(old_start); segment.start += start_shift; segment.end += shift(segment.end); let keyframes = &mut segment.keyframes; - let relative_bends = bends.map(|bend| bend - old_start); + let relative_bends: Vec = bends.iter().map(|bend| bend - old_start).collect(); split_keyframes_at( &mut keyframes.position, &relative_bends, @@ -4692,6 +4706,59 @@ mod tests { assert!(!set_clip_segment_timescale(timeline, 0, 2.0)); } + #[test] + fn clip_speed_changes_ripple_tracks_past_fullscreen_holds() { + let mut project = config(serde_json::json!({ + "timeline": { + "segments": [ + { "recordingSegment": 0, "timescale": 1.0, "start": 0.0, "end": 10.0 }, + { "recordingSegment": 1, "timescale": 1.0, "start": 0.0, "end": 10.0 } + ], + "zoomSegments": [ + { "start": 11.0, "end": 12.0, "amount": 1.5, "mode": "auto" }, + { "start": 20.0, "end": 21.0, "amount": 1.5, "mode": "auto" } + ], + "textSegments": [ + { "start": 0.0, "end": 3.0, "track": 0, "content": "intro", "layout": "fullscreen" }, + { "start": 23.0, "end": 25.0, "track": 0, "content": "outro", "layout": "fullscreen" } + ], + "maskSegments": [{ + "start": 11.0, "end": 17.0, "maskType": "sensitive", + "center": { "x": 0.5, "y": 0.5 }, "size": { "x": 0.2, "y": 0.2 }, + "keyframes": { "intensity": [ + { "time": 0.0, "value": 0.0 }, + { "time": 6.0, "value": 1.0 } + ] } + }] + } + })); + let timeline = project.timeline.as_mut().unwrap(); + + assert!(set_clip_segment_timescale(timeline, 1, 2.0)); + + let zooms: Vec<_> = timeline + .zoom_segments + .iter() + .map(|segment| (segment.start, segment.end)) + .collect(); + assert_eq!(zooms, [(11.0, 12.0), (16.5, 17.0)]); + let texts: Vec<_> = timeline + .text_segments + .iter() + .map(|segment| (segment.start, segment.end)) + .collect(); + assert_eq!(texts, [(0.0, 3.0), (18.0, 20.0)]); + let mask = &timeline.mask_segments[0]; + assert_eq!((mask.start, mask.end), (11.0, 15.0)); + let intensity: Vec<_> = mask + .keyframes + .intensity + .iter() + .map(|key| (key.time, key.value)) + .collect(); + assert_eq!(intensity, [(0.0, 0.0), (2.0, 1.0 / 3.0), (4.0, 1.0)]); + } + #[test] fn clip_speed_changes_remap_mask_keyframes_in_absolute_time() { for (timescale, expected) in [ From d8f35cabca5bd2b012d912d444742a852e9c2340 Mon Sep 17 00:00:00 2001 From: Aditya Garud <153842990+yashranaway@users.noreply.github.com> Date: Sun, 1 Nov 2026 08:34:14 +0000 Subject: [PATCH 2/2] test(desktop-gpui): cover mask keyframe splits at hold edges on clip speed change --- apps/desktop-gpui/src/editor_edits.rs | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/apps/desktop-gpui/src/editor_edits.rs b/apps/desktop-gpui/src/editor_edits.rs index c2aebecd2ff..56762e0e019 100644 --- a/apps/desktop-gpui/src/editor_edits.rs +++ b/apps/desktop-gpui/src/editor_edits.rs @@ -4759,6 +4759,45 @@ mod tests { assert_eq!(intensity, [(0.0, 0.0), (2.0, 1.0 / 3.0), (4.0, 1.0)]); } + #[test] + fn clip_speed_changes_split_mask_keyframes_at_hold_edges() { + let mut project = config(serde_json::json!({ + "timeline": { + "segments": [ + { "recordingSegment": 0, "timescale": 1.0, "start": 0.0, "end": 10.0 }, + { "recordingSegment": 1, "timescale": 1.0, "start": 0.0, "end": 10.0 } + ], + "zoomSegments": [], + "textSegments": [ + { "start": 14.0, "end": 16.0, "track": 0, "content": "title", "layout": "fullscreen" } + ], + "maskSegments": [{ + "start": 12.0, "end": 20.0, "maskType": "sensitive", + "center": { "x": 0.5, "y": 0.5 }, "size": { "x": 0.2, "y": 0.2 }, + "keyframes": { "intensity": [ + { "time": 0.0, "value": 0.0 }, + { "time": 8.0, "value": 1.0 } + ] } + }] + } + })); + let timeline = project.timeline.as_mut().unwrap(); + + assert!(set_clip_segment_timescale(timeline, 1, 2.0)); + + let text = &timeline.text_segments[0]; + assert_eq!((text.start, text.end), (12.0, 14.0)); + let mask = &timeline.mask_segments[0]; + assert_eq!((mask.start, mask.end), (11.0, 16.0)); + let intensity: Vec<_> = mask + .keyframes + .intensity + .iter() + .map(|key| (key.time, key.value)) + .collect(); + assert_eq!(intensity, [(0.0, 0.0), (1.0, 0.25), (3.0, 0.5), (5.0, 1.0)]); + } + #[test] fn clip_speed_changes_remap_mask_keyframes_in_absolute_time() { for (timescale, expected) in [