Browse / github.com/storytold/soundcraft / patch
fix(ui): open the plugin's own editor, not the generic panel, on insert
fix(ui): open the plugin's own editor, not the generic panel, on insert ## Problem Inserting a plugin that has a native editor also opened SoundCraft's generic parameter panel, leaving a ghost panel behind the plugin's own window. Two paths caused it: - **On insert**, the generic panel was pushed before the player had created the instance, so `has_editor` was still false. - **On click**, an occupied insert always opened the generic panel, even when the plugin had a native editor. ## Fix - `Player::plugin_ready(track, slot)` exposes whether the side thread has prepared (or refused) the slot's instance, so the UI can tell “this plugin has no native editor” from
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- NDIO-AI (imported from pull request #796, open)
- Base
f0d5acb800c65bcd2a9d637cd1726cf6597cb629- Size
- +80 −16 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
67301337e2e03dbabf7cce6c
How it went
No other operator has reported using it yet.
Diff
raw · crates/playback/src/lib.rs crates/ui-egui/src/lib.rs crates/ui-egui/src/mix_window.rs
@@ -698,6 +698,13 @@ impl Player { out } + /// True once the plugin instance for this slot has been prepared (or its creation+ /// refused). The editor handle is known by then, so the UI can tell a plugin without a+ /// native editor from one whose instance is still being created.+ pub fn plugin_ready(&self, track: TrackId, slot: usize) -> bool {+ self.side().shadow.contains_key(&(track, slot))+ }+ /// True when the plugin in this slot has its own editor. pub fn has_editor(&self, track: TrackId, slot: usize) -> bool { self.side().editors.contains_key(&(track, slot))@@ -624,22 +624,38 @@ impl SoundApp { /// Opens the insert just picked from its menu: a hosted plugin's own editor, else (a built-in /// plugin, no audio engine, or an editor that can't open here) its plugin window. fn open_inserted(&mut self) {- let Some((track, slot)) = self.pending_open.take() else { return };+ let Some((track, slot)) = self.pending_open else { return }; let Some(plugin) = self.engine.session().track(track).and_then(|t| t.mixer.inserts.get(slot).cloned().flatten()).map(|i| i.plugin) else {+ self.pending_open = None; return; };- if soundcraft_mix::is_third_party(&plugin)- && let Some(p) = &self.player- && p.has_editor(track, slot)- {- match p.open_editor(track, slot) {- Ok(()) => return,- Err(e) => self.ui.status = format!("{}: {e}", mix_window::plugin_info(&plugin).map_or(plugin.as_str(), |i| i.name)),+ if soundcraft_mix::is_third_party(&plugin) {+ let Some((ready, has_editor)) = self.player.as_ref().map(|p| (p.plugin_ready(track, slot), p.has_editor(track, slot))) else {+ // No player (headless or failed audio): fall back to the generic window.+ self.pending_open = None;+ mix_window::open_plugin(self, track, slot);+ return;+ };+ if !ready {+ // The player is still creating the instance; its editor handle appears with it.+ // Keep the request for the next frame instead of opening the generic window now,+ // which used to leave a ghost panel behind the plugin's own editor.+ return; }+ self.pending_open = None;+ if has_editor {+ if let Some(p) = &self.player+ && let Err(e) = p.open_editor(track, slot)+ {+ self.ui.status = format!("{}: {e}", mix_window::plugin_info(&plugin).map_or(plugin.as_str(), |i| i.name));+ }+ return;+ }+ mix_window::open_plugin(self, track, slot);+ return; }- if !self.ui.plugin_windows.contains(&(track, slot)) {- self.ui.plugin_windows.push((track, slot));- }+ self.pending_open = None;+ mix_window::open_plugin(self, track, slot); } /// Parameter edits made in plugin editors go into the session (one undo step per gesture).@@ -488,6 +488,27 @@ fn strip(app: &mut SoundApp, ui: &mut Ui, id: TrackId, snap: Option<&MeterSnapsh ui.painter().line_segment([pos2(r.max.x, r.min.y), pos2(r.max.x, r.max.y)], Stroke::new(1.0, t.border)); } +/// Opens a plugin's own editor when it has one, otherwise its generic parameter window.+/// Clicking an insert with a native editor must not also open the generic window: doing both+/// left a ghost panel behind the plugin's own UI.+pub(crate) fn open_plugin(app: &mut SoundApp, track: TrackId, slot: usize) {+ if app.player.as_ref().is_some_and(|p| p.has_editor(track, slot)) {+ let name =+ app.engine.session().track(track).and_then(|t| {+ if slot == soundcraft_mix::INSTRUMENT_SLOT { t.instrument.clone() } else { t.mixer.inserts.get(slot).cloned().flatten() }+ });+ let name = name.map(|i| plugin_info(&i.plugin).map_or_else(|| i.plugin.clone(), |p| p.name.to_string()));+ let result = app.player.as_ref().map(|p| p.open_editor(track, slot));+ if let Some(Err(e)) = result {+ app.ui.status = format!("{}: {e}", name.unwrap_or_else(|| "Plugin".to_string()));+ }+ return;+ }+ if !app.ui.plugin_windows.contains(&(track, slot)) {+ app.ui.plugin_windows.push((track, slot));+ }+}+ /// An instrument track's INSTRUMENT row: click opens a hosted instrument's plugin window, /// right-click (or click, when none is set) picks another instrument. A built-in's parameters /// open from that menu. Dropping an audio file on it loads the sampler.@@ -503,8 +524,8 @@ pub(crate) fn instrument_slot(app: &mut SoundApp, ui: &mut Ui, track: &Track, r: None => empty_slot(ui.painter(), r, resp.hovered(), Mark::Dot), } let hosted = ins.is_some_and(|i| soundcraft_mix::is_third_party(&i.plugin));- if resp.clicked() && hosted && !app.ui.plugin_windows.contains(&(id, slot)) {- app.ui.plugin_windows.push((id, slot));+ if resp.clicked() && hosted {+ open_plugin(app, id, slot); } let menu_resp = if hosted { resp.clone().on_hover_text("Click: open instrument · right-click: change") } else { resp.clone() }; let popup = if hosted { egui::Popup::context_menu(&menu_resp) } else { egui::Popup::menu(&menu_resp) };@@ -602,8 +623,8 @@ pub(crate) fn insert_slot(app: &mut SoundApp, ui: &mut Ui, track: &Track, slot: if resp.clicked() && ins.is_some() { if ui.input(|i| i.modifiers.command) { let _ = app.run("mix.insert_bypass", json!({"track": id.0, "slot": slot}));- } else if !app.ui.plugin_windows.contains(&(id, slot)) {- app.ui.plugin_windows.push((id, slot));+ } else {+ open_plugin(app, id, slot); } } let cmd = if ui.ctx().os().is_mac() { "⌘" } else { "Ctrl+" };@@ -625,6 +646,26 @@ pub fn plugin_menu(app: &mut SoundApp, ui: &mut Ui, id: TrackId, slot: usize, oc return; } if occupied {+ if app.player.as_ref().is_some_and(|p| p.has_editor(id, slot)) {+ let open = app.player.as_ref().is_some_and(|p| p.editor_open(id, slot));+ if ui.button(if open { "Close Plugin Editor" } else { "Open Plugin Editor" }).clicked() {+ if open {+ if let Some(p) = &app.player {+ p.close_editor(id, slot);+ }+ } else {+ open_plugin(app, id, slot);+ }+ ui.close();+ }+ }+ if ui.button("Edit Parameters…").clicked() {+ if !app.ui.plugin_windows.contains(&(id, slot)) {+ app.ui.plugin_windows.push((id, slot));+ }+ ui.close();+ }+ ui.separator(); if ui.button("Bypass").clicked() { run_pick(app, ui, "mix.insert_bypass", json!({"track": id.0, "slot": slot})); }@@ -668,7 +709,7 @@ pub fn instrument_menu(app: &mut SoundApp, ui: &mut Ui, id: TrackId) { let current = app.engine.session().track(id).and_then(|t| t.instrument.clone()); if let Some(info) = current.as_ref().and_then(|i| plugin_info(&i.plugin)) { ui.label(egui::RichText::new(info.name).strong());- if current.as_ref().is_some_and(|i| !soundcraft_mix::is_third_party(&i.plugin)) && ui.button("Edit Parameters…").clicked() {+ if ui.button("Edit Parameters…").clicked() { if !app.ui.plugin_windows.contains(&(id, slot)) { app.ui.plugin_windows.push((id, slot)); }