From 76bac6495e4ad19bafd5e1eb42b8f551caba1fd9 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Wed, 21 Oct 2026 10:23:03 +0200 Subject: [PATCH] Scroll Into View: a Mix request lasts one frame, so a hidden track doesn't scroll the Mix when it comes back --- crates/ui-egui/src/mix_window.rs | 10 ++++++---- crates/ui-egui/tests/scroll_into_view.rs | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/mix_window.rs b/crates/ui-egui/src/mix_window.rs index 0b63e3b4..4b9a2869 100644 --- a/crates/ui-egui/src/mix_window.rs +++ b/crates/ui-egui/src/mix_window.rs @@ -47,11 +47,14 @@ pub(crate) fn content(app: &mut SoundApp, ui: &mut Ui) { let ids: Vec = app.engine.session().tracks.iter().filter(|x| !x.hidden && x.kind != TrackKind::Video).map(|x| x.id).collect(); // Per-channel peaks for multichannel meters (read once per frame). let snap = app.player.as_ref().filter(|_| app.meters_live()).map(|p| p.meters()); + // Taken for this frame only: a strip that isn't drawn (hidden track) must not scroll the Mix + // whenever it comes back. A request made during this frame (the strip's menu) waits for the next. + let scroll_to = app.mix_scroll_into_view.take(); egui::ScrollArea::horizontal().auto_shrink([false, false]).show(ui, |ui| { ui.horizontal_top(|ui| { ui.spacing_mut().item_spacing.x = 1.0; for id in ids { - strip(app, ui, id, snap.as_ref()); + strip(app, ui, id, snap.as_ref(), scroll_to == Some(id)); } let rest = Rect::from_min_max(pos2(ui.cursor().min.x, ui.clip_rect().min.y), ui.clip_rect().max); if rest.width() > 4.0 && ui.interact(rest, ui.id().with("past_strips"), Sense::click()).double_clicked() { @@ -149,7 +152,7 @@ pub(crate) fn route_popup(app: &mut SoundApp, resp: &egui::Response, id: TrackId egui::Popup::menu(resp).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| route_menu(app, ui, id, input)); } -fn strip(app: &mut SoundApp, ui: &mut Ui, id: TrackId, snap: Option<&MeterSnapshot>) { +fn strip(app: &mut SoundApp, ui: &mut Ui, id: TrackId, snap: Option<&MeterSnapshot>, scroll_here: bool) { let t = Tokens::current(); let session = app.engine.session_arc(); let Some(track) = session.track(id) else { return }; @@ -157,8 +160,7 @@ fn strip(app: &mut SoundApp, ui: &mut Ui, id: TrackId, snap: Option<&MeterSnapsh let w = strip_width(narrow); let h = ui.available_height().max(560.0); let (r, _) = ui.allocate_exact_size(vec2(w, h), Sense::hover()); - if app.mix_scroll_into_view == Some(id) { - app.mix_scroll_into_view = None; + if scroll_here { // egui stops `item_spacing` short of the edge (the scroll area's, not the strips' 1 pt), // which would leave a sliver of the previous strip showing. let margin = ui.ctx().global_style().spacing.item_spacing.x; diff --git a/crates/ui-egui/tests/scroll_into_view.rs b/crates/ui-egui/tests/scroll_into_view.rs index fbb06d1a..2a817357 100644 --- a/crates/ui-egui/tests/scroll_into_view.rs +++ b/crates/ui-egui/tests/scroll_into_view.rs @@ -150,3 +150,19 @@ fn the_command_takes_a_track_by_name_from_the_control_channel() { assert!(h.state_mut().run("view.scroll_into_view", json!({"track": "T99"})).is_err()); } +#[test] +fn a_hidden_track_leaves_no_scroll_waiting_in_the_mix() { + let mut h = harness(20); + h.state_mut().ui.window = MainWindow::Mix; + h.state_mut().run("track.hide", json!({"tracks": ["T08"], "hidden": true})).unwrap(); + h.run_steps(4); + let first = plate(&h, "T01").expect("the first strip's name").x; + h.state_mut().run("view.scroll_into_view", json!({"track": "T08"})).unwrap(); + h.run_steps(4); + + // Shown again later, T08 must not pull the Mix to itself: the request was for frames long gone. + h.state_mut().run("track.hide", json!({"tracks": ["T08"], "hidden": false})).unwrap(); + h.run_steps(4); + let now = plate(&h, "T01").expect("T01 still on screen").x; + assert!((now - first).abs() < 1.0, "the Mix stayed put: T01 at {now}, was at {first}"); +}