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).