Merge pull request #29 from Garygunn94/claude/track-delete-menu-disappear-9456d6

Keep the clip context menu open while moving onto it
This commit is contained in:
Brandon Thomas
2026-10-09 18:11:39 -04:00
committed by GitHub
2 changed files with 80 additions and 1 deletions
+14 -1
View File
@@ -1378,6 +1378,20 @@ fn clip_at(track: &Track, at: Samples) -> Option<&Clip> {
fn lane_interaction(app: &mut SoundApp, ui: &mut Ui, track: &Track, lane: Rect, tl: Rect) {
let id = ui.id().with(("lane", track.id.0));
let resp = ui.interact(lane, id, Sense::click_and_drag());
// The clip menu is attached before the early return below: once the menu is open the pointer
// is over the popup, the lane has no hover position, and the rest of this function bails out.
// The clip is remembered at the right-click so the menu keeps its target while it is used.
let menu_clip_id = id.with("menu_clip");
if resp.secondary_clicked() {
let clip = resp.interact_pointer_pos().and_then(|p| {
let s = app.engine.session();
clip_at(track, sample_at(s, tl, p.x).max(0)).map(|c| c.id)
});
ui.ctx().data_mut(|d| d.insert_temp(menu_clip_id, clip));
}
// The clip may have been removed or moved since the right-click; only use it if it still exists.
let menu_clip = ui.ctx().data(|d| d.get_temp::<Option<ClipId>>(menu_clip_id)).flatten().filter(|c| track.clips().iter().any(|x| x.id == *c));
resp.context_menu(|ui| clip_context_menu(app, ui, menu_clip));
// Audio files dropped from the Clip List.
if let Some(src) = resp.dnd_release_payload::<crate::DragSource>()
&& let Some(p) = ui.ctx().pointer_latest_pos()
@@ -1619,7 +1633,6 @@ fn lane_interaction(app: &mut SoundApp, ui: &mut Ui, track: &Track, lane: Rect,
{
let _ = app.run("edit.select", json!({"clips": [c.id.0]}));
}
resp.context_menu(|ui| clip_context_menu(app, ui, hit.as_ref().map(|c| c.id)));
}
fn automation_id(p: &AutoParam) -> String {
+66
View File
@@ -0,0 +1,66 @@
//! Regression: the clip context menu must stay open while the pointer moves onto it, so its
//! Delete item can be used. It used to vanish once the pointer left the lane, because the lane
//! returned early (no hover position) before the menu was drawn.
use egui::{Event, Modifiers, PointerButton, pos2, vec2};
use egui_kittest::Harness;
use soundcraft_ui_egui::{Services, SoundApp};
/// One UI frame, with queued synthetic pointer input applied first (as the control channel does).
fn step(h: &mut Harness<'_, SoundApp>) {
let mut raw = std::mem::take(h.input_mut());
h.state_mut().raw_input_hook(&mut raw);
*h.input_mut() = raw;
h.step();
}
fn press(h: &mut Harness<'_, SoundApp>, x: f32, y: f32, button: PointerButton) {
let pos = pos2(x, y);
let s = &mut h.state_mut().synthetic;
s.push(Event::PointerMoved(pos));
s.push(Event::PointerButton { pos, button, pressed: true, modifiers: Modifiers::NONE });
s.push(Event::PointerButton { pos, button, pressed: false, modifiers: Modifiers::NONE });
}
fn kick_clip_count(h: &Harness<'_, SoundApp>) -> usize {
let s = h.state().engine.session();
s.tracks.iter().find(|t| t.name == "Kick").map_or(0, |t| t.clips().len())
}
#[test]
fn clip_menu_stays_open_while_moving_onto_delete() {
let engine = soundcraft_engine::demo::demo_engine();
let app = SoundApp::new(engine, None, Services::default());
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).with_pixels_per_point(2.0).build_ui_state(
|ui, app: &mut SoundApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
app,
);
for _ in 0..6 {
step(&mut h);
}
let before = kick_clip_count(&h);
assert!(before > 0, "the demo session should have a recorded Kick clip");
// Right-click the Kick clip, then move the pointer into the menu and down to Delete.
press(&mut h, 880.0, 240.0, PointerButton::Secondary);
for _ in 0..10 {
step(&mut h);
}
// Move (no click) into the menu, as a real pointer would, so the lane loses its hover.
for (x, y) in [(900.0, 300.0), (907.0, 442.0)] {
h.state_mut().synthetic.push(Event::PointerMoved(pos2(x, y)));
for _ in 0..10 {
step(&mut h);
}
}
press(&mut h, 907.0, 442.0, PointerButton::Primary);
for _ in 0..15 {
step(&mut h);
}
assert_eq!(kick_clip_count(&h), before - 1, "clicking Delete in the clip menu should remove the clip");
}