diff --git a/apps/soundcraft/src/main.rs b/apps/soundcraft/src/main.rs index 9324d07..011f09e 100644 --- a/apps/soundcraft/src/main.rs +++ b/apps/soundcraft/src/main.rs @@ -168,9 +168,15 @@ fn services() -> Services { }; d.pick_file().map(|p| p.to_string_lossy().into_owned()) })), - pick_save: Some(Box::new(|_purpose: &str, default: &str| { + pick_save: Some(Box::new(|purpose: &str, default: &str| { let path = std::path::Path::new(default); - let mut d = rfd::FileDialog::new().add_filter("SoundCraft Session", &["scraft"]); + let mut d = match purpose { + "file.bounce_mix" => rfd::FileDialog::new().add_filter("Audio", &["wav", "aif", "aiff", "flac"]), + _ => rfd::FileDialog::new().add_filter("SoundCraft Session", &["scraft"]), + }; + if let Some(dir) = path.parent().filter(|dir| dir.is_dir()) { + d = d.set_directory(dir); + } if let Some(n) = path.file_name().and_then(|n| n.to_str()) { d = d.set_file_name(n); } diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 2ca955d..efc7bc3 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -91,7 +91,13 @@ impl Dialogs { } "session.new" => Dialog::NewSession { name: "Untitled".into(), sample_rate: 48_000, demo: false }, "file.bounce_mix" => { - Dialog::Bounce { path: format!("{home}/{name} Bounce.wav"), format: "wav".into(), bit_depth: "24".into(), normalize: false } + let dir = e + .path + .as_deref() + .and_then(|p| std::path::Path::new(p).parent()) + .filter(|d| !d.as_os_str().is_empty()) + .map_or(home.clone(), |d| d.display().to_string()); + Dialog::Bounce { path: format!("{dir}/{name} Bounce.wav"), format: "wav".into(), bit_depth: "24".into(), normalize: false } } "file.import_audio" => Dialog::PathPrompt { cmd: id.into(), title: "Import Audio".into(), path: home, key: "path".into() }, "file.import_midi" => Dialog::PathPrompt { cmd: id.into(), title: "Import MIDI".into(), path: home, key: "path".into() }, @@ -346,7 +352,19 @@ pub fn show(app: &mut SoundApp, ctx: &egui::Context) { Dialog::Bounce { path, format, bit_depth, normalize } => { ui.horizontal(|ui| { ui.label("File"); - ui.add(egui::TextEdit::singleline(path).desired_width(360.0)); + ui.add(egui::TextEdit::singleline(path).desired_width(420.0)); + if let Some(pick) = &app.services.pick_save + && ui.button("Browse…").clicked() + && let Some(p) = pick("file.bounce_mix", path) + { + *path = p; + match std::path::Path::new(path.as_str()).extension().map(|x| x.to_string_lossy().to_lowercase()).as_deref() { + Some("wav") => *format = "wav".into(), + Some("aif" | "aiff") => *format = "aiff".into(), + Some("flac") => *format = "flac".into(), + _ => {} + } + } }); ui.horizontal(|ui| { egui::ComboBox::from_label("Format").selected_text(format.to_uppercase()).show_ui(ui, |ui| { diff --git a/crates/ui-egui/tests/bounce_dialog.rs b/crates/ui-egui/tests/bounce_dialog.rs new file mode 100644 index 0000000..e3a5499 --- /dev/null +++ b/crates/ui-egui/tests/bounce_dialog.rs @@ -0,0 +1,54 @@ +//! Bounce Mix starts in the session's folder and has a Browse button for the file (#186). +use egui::vec2; +use egui_kittest::{Harness, kittest::Queryable}; +use soundcraft_ui_egui::{Services, SoundApp, dialogs::Dialog}; +use std::cell::RefCell; +use std::rc::Rc; + +#[test] +fn bounce_starts_in_the_session_folder_and_browse_picks_the_file_and_format() { + let asked = Rc::new(RefCell::new(Vec::new())); + let log = Rc::clone(&asked); + let pick_save: Box Option> = Box::new(move |purpose, default| { + log.borrow_mut().push((purpose.to_string(), default.to_string())); + Some("/Mixes/Final Mix.FLAC".into()) + }); + let mut engine = soundcraft_engine::demo::demo_engine(); + engine.path = Some("/Songs/Midnight Groove/Midnight Groove.scraft".into()); + let mut app = SoundApp::new(engine, None, Services { pick_save: Some(pick_save), ..Default::default() }); + assert!(app.dialogs.open_for_command(&app.engine, "file.bounce_mix")); + 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); + h.get_by_label("Browse…").click(); + h.run_steps(3); + let default = "/Songs/Midnight Groove/Midnight Groove Bounce.wav".to_string(); + assert_eq!(*asked.borrow(), vec![("file.bounce_mix".to_string(), default)]); + match &h.state().dialogs.open { + Some(Dialog::Bounce { path, format, .. }) => assert_eq!((path.as_str(), format.as_str()), ("/Mixes/Final Mix.FLAC", "flac")), + other => panic!("{other:?}"), + } +} + +#[test] +fn bounce_of_a_session_with_a_relative_path_starts_in_the_default_folder() { + let mut engine = soundcraft_engine::demo::demo_engine(); + engine.path = Some("Midnight Groove.scraft".into()); + let name = engine.session().name.clone(); + let mut app = SoundApp::new(engine, None, Services::default()); + assert!(app.dialogs.open_for_command(&app.engine, "file.bounce_mix")); + let mut fresh = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default()); + assert!(fresh.dialogs.open_for_command(&fresh.engine, "file.bounce_mix")); + match (&app.dialogs.open, &fresh.dialogs.open) { + (Some(Dialog::Bounce { path, .. }), Some(Dialog::Bounce { path: unsaved, .. })) => { + assert_ne!(path.as_str(), format!("/{name} Bounce.wav")); + assert_eq!(path, unsaved, "the same folder as a session never saved"); + } + other => panic!("{other:?}"), + } +}