From ea531b723f06a9cfda60072774eec2f1b85f39eb Mon Sep 17 00:00:00 2001 From: Limestudio90 Date: Mon, 12 Oct 2026 18:47:18 +0200 Subject: [PATCH] Mix in a separate window: Edit and Mix on screen at once Window > Mix in Separate Window (window.mix_separate, kept with the UI preferences) shows the Mix window in its own native window through an egui immediate viewport, so Edit and Mix can be seen at once, e.g. one per monitor. The main window then keeps Edit; Window > Mix and Cmd/Ctrl+= bring the Mix window to the front; closing it puts Mix back into the main window. Backends without multiple windows show it as a floating egui window. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/lib.rs | 13 ++++++++++++- crates/ui-egui/src/menus.rs | 28 ++++++++++++++++++++++++++-- crates/ui-egui/src/mix_window.rs | 19 +++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index e0e2f2b..716adf1 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -88,6 +88,9 @@ pub struct UiState { pub show_workspace: bool, pub show_configurations: bool, pub show_playback_engine: bool, + /// The Mix window lives in its own native window (Window › Mix in Separate Window); the main + /// window then always shows Edit. + pub mix_separate: bool, pub show_io_setup: bool, pub show_shortcuts: bool, pub show_clip_effects: bool, @@ -149,6 +152,7 @@ impl Default for UiState { show_workspace: false, show_configurations: false, show_playback_engine: false, + mix_separate: false, show_io_setup: false, show_shortcuts: false, show_clip_effects: false, @@ -253,6 +257,8 @@ pub struct SoundApp { pub ops: ops_windows::OpsState, pub extra: extra_windows::ExtraState, pub palette: palette::PaletteState, + /// Bring the separate Mix window to the front on the next frame. + pub(crate) focus_mix: bool, pub synthetic: Vec, synthetic_grace: u32, control_rx: Option>, @@ -322,6 +328,7 @@ impl SoundApp { ops: ops_windows::OpsState::default(), extra: extra_windows::ExtraState::default(), palette: palette::PaletteState::default(), + focus_mix: false, synthetic: Vec::new(), synthetic_grace: 0, control_rx: None, @@ -929,10 +936,14 @@ impl SoundApp { if !self.native_menu_bar { menus::menu_bar(self, ui); } - match self.ui.window { + // With the Mix window in its own native window, the main window keeps Edit. + match if self.ui.mix_separate { MainWindow::Edit } else { self.ui.window } { MainWindow::Edit => edit_window::show(self, ui), MainWindow::Mix => mix_window::show(self, ui), } + if self.ui.mix_separate { + mix_window::show_separate(self, &ctx); + } panels::floating(self, &ctx); windows::show(self, &ctx); video_window::show(self, &ctx); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 3c20019..61d5052 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -12,6 +12,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, Option<&str>)] = &[ ("window.mix", "Mix", "Window > Mix", Some("Cmd+=")), ("window.edit", "Edit", "Window > Edit", Some("Cmd+=")), ("window.toggle_mix_edit", "Toggle Mix/Edit", "", Some("Cmd+=")), + ("window.mix_separate", "Mix in Separate Window", "", None), ("window.transport", "Transport", "Window > Transport", Some("Cmd+1")), ("window.big_counter", "Big Counter", "Window > Big Counter", Some("Cmd+3")), ("window.memory_locations", "Memory Locations", "Window > Memory Locations", Some("Cmd+5")), @@ -243,13 +244,25 @@ pub fn menu_bar(app: &mut SoundApp, ui: &mut egui::Ui) { ui.separator(); ui.label(egui::RichText::new("Processes selected clips offline").small().color(t.text_dim)); } + if root.label == "Window" { + ui.separator(); + let mut separate = app.ui.mix_separate; + if ui + .checkbox(&mut separate, "Mix in Separate Window") + .on_hover_text("Show Edit and Mix at once, e.g. one per monitor") + .changed() + { + let _ = run_ui_command(app, "window.mix_separate", &json!({"value": separate})); + ui.close(); + } + } }); } let title = format!( "{}{} — {}", app.engine.session().name, if app.engine.is_dirty() { " *" } else { "" }, - if app.ui.window == MainWindow::Edit { "Edit" } else { "Mix" } + if app.ui.window == MainWindow::Edit || app.ui.mix_separate { "Edit" } else { "Mix" } ); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.label(egui::RichText::new(title).color(t.text_dim)); @@ -473,14 +486,25 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option { app.ui.window = MainWindow::Mix; + app.focus_mix = app.ui.mix_separate; json!({"window": "mix"}) } + "window.mix_separate" => { + let v = toggle(&mut app.ui.mix_separate); + app.focus_mix = app.ui.mix_separate; + v + } "window.edit" => { app.ui.window = MainWindow::Edit; json!({"window": "edit"}) } "window.toggle_mix_edit" => { - app.ui.window = if app.ui.window == MainWindow::Edit { MainWindow::Mix } else { MainWindow::Edit }; + if app.ui.mix_separate { + // Both are on screen: bring the Mix window forward instead. + app.focus_mix = true; + } else { + app.ui.window = if app.ui.window == MainWindow::Edit { MainWindow::Mix } else { MainWindow::Edit }; + } json!({"window": app.ui.window}) } "window.transport" => toggle(&mut app.ui.show_transport), diff --git a/crates/ui-egui/src/mix_window.rs b/crates/ui-egui/src/mix_window.rs index 01bb21a..135dcd2 100644 --- a/crates/ui-egui/src/mix_window.rs +++ b/crates/ui-egui/src/mix_window.rs @@ -19,6 +19,25 @@ pub fn show(app: &mut SoundApp, ui: &mut Ui) { crate::docking::show(app, ui, crate::MainWindow::Mix); } +/// The Mix window in its own native window (Window › Mix in Separate Window), so Edit and Mix can +/// be seen at once, e.g. one per monitor. Closing that window brings Mix back into the main one. +/// Backends without multiple windows show it as a floating egui window instead. +pub fn show_separate(app: &mut SoundApp, ctx: &egui::Context) { + let title = format!("{} — Mix", app.engine.session().name); + let builder = egui::ViewportBuilder::default().with_title(title).with_inner_size([1100.0, 720.0]).with_min_inner_size([480.0, 320.0]); + ctx.show_viewport_immediate(egui::ViewportId::from_hash_of("soundcraft.mix"), builder, |ui, _class| { + ui.set_style(ui.ctx().global_style()); + if std::mem::take(&mut app.focus_mix) { + ui.ctx().send_viewport_cmd(egui::ViewportCommand::Focus); + } + show(app, ui); + if ui.ctx().input(|i| i.viewport().close_requested()) { + app.ui.mix_separate = false; + app.ui.window = crate::MainWindow::Mix; + } + }); +} + 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).