From fe7efdaffc64b597362edbbc6acaedf3c87a836d Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Tue, 13 Oct 2026 04:22:50 +0200 Subject: [PATCH] Timeline: scrolling the video tracks up leaves room to drop a new track (fixes #1522) --- crates/ui-egui/src/panels/timeline.rs | 60 +++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index ac4caf53..3c513a67 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -233,6 +233,19 @@ fn new_track_at(seq: &Sequence, layout: &Layout, y: f32, vh: f32, ah: f32) -> Op } } +/// The top of each of `n` video rows `vh` high, V1 first: V1 sits at the bottom of `area` and the +/// tracks stack upward; `v_scroll` (clamped here) shifts them up. Also gives the largest `v_scroll`. +/// The scroll range includes one empty track's height above the top track: without it, tracks that +/// fill the area leave no space where a drop adds a new one (#1522). +fn video_row_tops(area: Rect, n: usize, vh: f32, v_scroll: &mut f32) -> (Vec, f32) { + let total = (n + 1) as f32 * vh; + let v_off = (area.height() - total).max(0.0); + let max_vs = (total - area.height()).max(0.0); + *v_scroll = v_scroll.clamp(0.0, max_vs); + let tops = (0..n).map(|i| area.min.y + v_off + (n - i) as f32 * vh - (max_vs - *v_scroll)).collect(); + (tops, max_vs) +} + /// How far the Timeline reaches, in seconds: ten minutes past the end of the sequence. Premiere /// Pro's Timeline does the same (on a 6 s sequence its view stops with 00:10:06 at the right /// edge). The view cannot be scrolled past it and the scroll bar spans it. @@ -412,14 +425,9 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) { let vh = app.ui.timeline.video_track_h; let ah = app.ui.timeline.audio_track_h; let mut rows = Vec::new(); - // Video: V1 at the bottom of the video area, stacking upward; v_scroll shifts up. let nv = seq.video_tracks.len(); - let video_total = nv as f32 * vh; - let v_off = (video_area.height() - video_total).max(0.0); - let max_vs = (video_total - video_area.height()).max(0.0); - app.ui.timeline.v_scroll = app.ui.timeline.v_scroll.clamp(0.0, max_vs); - for (i, tr) in seq.video_tracks.iter().enumerate() { - let top = video_area.min.y + v_off + (nv - 1 - i) as f32 * vh - (max_vs - app.ui.timeline.v_scroll); + let (video_tops, max_vs) = video_row_tops(video_area, nv, vh, &mut app.ui.timeline.v_scroll); + for ((i, tr), top) in seq.video_tracks.iter().enumerate().zip(video_tops) { let rect = Rect::from_min_max(pos2(content.min.x, top), pos2(content.max.x, top + vh)); rows.push(Row { track: tr.id, kind: TrackKind::Video, index: i, rect, lane: false }); } @@ -3201,4 +3209,42 @@ mod new_track_tests { assert_eq!(at(y), None, "y = {y}"); } } + + /// #1522: when the video tracks fill the video area, scrolling all the way up still leaves a + /// track's height of empty space above the top one, so a drop there can add a fourth track. + #[test] + fn a_full_video_area_scrolls_to_space_for_a_new_track() { + let mut p = Project::new("t"); + let id = p.new_sequence("s", SequenceSettings::default(), 3, 1, None); + let seq = p.sequence(id).cloned().expect("sequence"); + // video area 0..150 holds V1..V3 exactly; scrolled up as far as it goes + let area = Rect::from_min_max(pos2(0.0, 0.0), pos2(800.0, 150.0)); + let mut v_scroll = f32::MAX; + let (tops, _) = video_row_tops(area, 3, 50.0, &mut v_scroll); + let rows = seq + .video_tracks + .iter() + .zip(&tops) + .enumerate() + .map(|(index, (t, &y0))| Row { + track: t.id, + kind: TrackKind::Video, + index, + rect: Rect::from_min_max(pos2(0.0, y0), pos2(800.0, y0 + 50.0)), + lane: false, + }) + .collect(); + let layout = Layout { + content: Rect::from_min_max(pos2(0.0, 0.0), pos2(800.0, 500.0)), + ruler: Rect::from_min_max(pos2(0.0, -20.0), pos2(800.0, 0.0)), + rows, + pps: 100.0, + scroll: 0.0, + split_y: 150.0, + }; + assert_eq!(new_track_at(&seq, &layout, 10.0, 50.0, 50.0), Some((TrackKind::Video, Rect::from_min_max(pos2(0.0, 0.0), pos2(800.0, 50.0))))); + // scrolled to the bottom, V1..V3 fill the area as before + let mut v_scroll = 0.0; + assert_eq!(video_row_tops(area, 3, 50.0, &mut v_scroll).0, vec![100.0, 50.0, 0.0]); + } }