mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-15 06:46:45 -04:00
Merge pull request #322 from Limestudio90/mix-window-separate
Mix in a separate window: Edit and Mix on screen at once
This commit is contained in:
6 files changed
+103
-4
No files matched your search
@@ -122,11 +122,13 @@ mutated and truncated movies. Field reports from the first public week are track
|
||||
|
||||
## Progress log
|
||||
|
||||
- **2026-10-12 (Mix in a separate window):** Window › Mix in Separate Window shows the Mix window in its own native window, so Edit and Mix can be on screen at once (e.g. one per monitor), as in Pro Tools. No readiness percentage change.
|
||||
- **2026-10-12 (plugin editors on Windows):** VST3 editors and embedded CLAP editors open in a SoundCraft host window on Windows (owned by the app window, resized on request, closed from its title bar); verified with a JUCE VST3 compressor and a CLAP effect. No readiness percentage change.
|
||||
- **2026-10-12:** `soundcraft-cli convert` rejects an unparsable or zero `--rate` and an unknown `--bit-depth` (exit 1, nothing written) instead of converting with a default (#256)
|
||||
- **2026-10-12:** Timecode labels with huge fields no longer overflow: frame counts are computed in 128 bits and clamp, so `setup.current_timecode` and `session.format_time` saturate instead of failing (#248)
|
||||
- **2026-10-12:** `session.format_time` with the smallest bar number no longer fails with an internal error: the bar offset saturates (#249)
|
||||
- **2026-10-12 (plugin editors on Windows):** VST3 editors and embedded CLAP editors open in a SoundCraft host window on Windows (owned by the app window, resized on request, closed from its title bar); verified with a JUCE VST3 compressor and a CLAP effect. No readiness percentage change.
|
||||
- **2026-10-12 (Mix in a separate window):** Window › Mix in Separate Window shows the Mix window in its own native window, so Edit and Mix can be on screen at once (e.g. one per monitor), as in Pro Tools. No readiness percentage change.
|
||||
- **2026-10-10 (UI docking):** Tracks and Groups, Clips, MIDI Editor, Memory Locations, and Undo History use movable panels. Edit and Mix retain separate arrangements; Clips and MIDI Editor belong to Edit. Transport, plug-in, and specialist utility windows retain their existing hosts. Panel moves and reset are available as UI commands, and saved UI preferences retain arrangements. Common toolbar and track record-enable buttons share interaction, accessible labels and keyboard focus through `craft-ui`, retaining blue tool selection and red record-arm states. No readiness percentage change.
|
||||
- **2026-10-10** — Stage re-normalized to **pre-alpha** by the core-workflow gate (interface recording, MIDI recording and session interchange fail). Full re-measure against Pro Tools Ultimate 2026.4.1 and the standard
|
||||
progress-docs set (this page, target-app parity, gaps, roadmap, UI/format/hardware/plugin/
|
||||
|
||||
@@ -97,6 +97,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,
|
||||
@@ -159,6 +162,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,
|
||||
@@ -285,6 +289,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<egui::Event>,
|
||||
/// Where a dropped audio file loads into a track's sampler (last frame's rects).
|
||||
pub sample_drop_targets: Vec<(egui::Rect, TrackId)>,
|
||||
@@ -368,6 +374,7 @@ impl SoundApp {
|
||||
ops: ops_windows::OpsState::default(),
|
||||
extra: extra_windows::ExtraState::default(),
|
||||
palette: palette::PaletteState::default(),
|
||||
focus_mix: false,
|
||||
synthetic: Vec::new(),
|
||||
sample_drop_targets: Vec::new(),
|
||||
sample_reads: Vec::new(),
|
||||
@@ -1181,10 +1188,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);
|
||||
|
||||
@@ -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")),
|
||||
@@ -246,13 +247,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));
|
||||
@@ -476,14 +489,25 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option<Result<
|
||||
let r = match id {
|
||||
"window.mix" => {
|
||||
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),
|
||||
|
||||
@@ -19,6 +19,30 @@ 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()) {
|
||||
separate_closed(app);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The separate Mix window was closed: Mix goes back into the main window.
|
||||
pub fn separate_closed(app: &mut SoundApp) {
|
||||
app.ui.mix_separate = false;
|
||||
app.ui.window = crate::MainWindow::Mix;
|
||||
}
|
||||
|
||||
pub(crate) fn content(app: &mut SoundApp, ui: &mut Ui) {
|
||||
let ids: Vec<TrackId> = 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).
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
//! Window › Mix in Separate Window: Edit stays in the main window, and closing the Mix window
|
||||
//! brings Mix back into it.
|
||||
use egui::vec2;
|
||||
use egui_kittest::Harness;
|
||||
use serde_json::json;
|
||||
use soundcraft_ui_egui::{MainWindow, Services, SoundApp, mix_window};
|
||||
|
||||
#[test]
|
||||
fn mix_in_a_separate_window_toggles_and_closing_it_returns_to_the_main_window() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
assert!(!app.ui.mix_separate);
|
||||
app.run("window.mix_separate", json!({})).unwrap();
|
||||
assert!(app.ui.mix_separate);
|
||||
app.run("window.toggle_mix_edit", json!({})).unwrap();
|
||||
assert_eq!(app.ui.window, MainWindow::Edit, "Mix is already on screen: the main window keeps Edit");
|
||||
|
||||
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_ui_state(
|
||||
|ui, app: &mut SoundApp| {
|
||||
app.logic(&ui.ctx().clone());
|
||||
app.ui(ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
let app = h.state_mut();
|
||||
assert!(app.ui.mix_separate, "drawing both windows keeps them both");
|
||||
|
||||
mix_window::separate_closed(app);
|
||||
assert!(!app.ui.mix_separate);
|
||||
assert_eq!(app.ui.window, MainWindow::Mix, "closing the Mix window shows Mix in the main one");
|
||||
h.run_steps(3);
|
||||
|
||||
let app = h.state_mut();
|
||||
app.run("window.mix_separate", json!({"value": true})).unwrap();
|
||||
app.run("window.mix_separate", json!({"value": false})).unwrap();
|
||||
assert!(!app.ui.mix_separate);
|
||||
}
|
||||
+2
-1
@@ -42,7 +42,7 @@ Legend: ✅ matches · 🟡 partial · ❌ missing.
|
||||
| Window | Status | Notes |
|
||||
|---|---|---|
|
||||
| Edit window: rulers, track headers, view columns, Clip List, Groups, Tracks list | ✅ | Track Pin (new in 2026.4) ✅ (`track.pin`) |
|
||||
| Mix window: strips, views (inserts, sends, I/O, comments, EQ curve, preamps, instruments, object) | ✅ | Narrow Mix ✅; Delay Compensation view and HEAT missing |
|
||||
| Mix window: strips, views (inserts, sends, I/O, comments, EQ curve, preamps, instruments, object) | ✅ | Narrow Mix ✅; Mix and Edit on screen at once (Window › Mix in Separate Window, its own native window) ✅; Delay Compensation view and HEAT missing |
|
||||
| MIDI Editor, Score Editor, Event List | 🟡 | Dock height bug fixed (#113); notation editing shallow |
|
||||
| Transport, Big Counter, Memory Locations, Automation, Video, Universe, Renderer, UI Customization | ✅ present | |
|
||||
| Workspace / sound browser with previews | ❌ | G-16 |
|
||||
@@ -73,4 +73,5 @@ Legend: ✅ matches · 🟡 partial · ❌ missing.
|
||||
| 2026-10-13 | minor | Existing fades have draggable upper endpoints with matching hover/drag cursors, including at adjacent clip boundaries. Readiness estimate unchanged |
|
||||
| 2026-10-13 | minor | Fade drags use the same thin S-curve renderer as committed fades, replacing the dragged side's old curve. Readiness estimate unchanged |
|
||||
| 2026-10-13 | minor | Bounded Smart-tool fade previews and commits; full-clip fades target only the dragged clip and side. Readiness estimate unchanged |
|
||||
| 2026-10-12 | minor | Mix window: Mix in Separate Window (Edit and Mix on screen at once) |
|
||||
| 2026-10-10 | major | Created: tools, modes, shortcuts, windows and meters checklist against Pro Tools 2026.4.1 |
|
||||
Reference in new issue
Block a user