diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index d212634..5848673 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -100,6 +100,11 @@ impl Dialogs { let s = soundcraft_engine::score::ScoreSetup::from_session(e.session()); Dialog::ScoreSetup { title: s.title, composer: s.composer, bars_per_system: s.bars_per_system, show_track_names: s.show_track_names } } + "clip.rename" => { + let target = soundcraft_engine::cmd::clip_ids_param(e, &json!({})).first().and_then(|id| e.session().find_clip(*id)); + let Some((_, clip)) = target else { return false }; + Dialog::RenameClip { id: clip.id, name: clip.name.clone() } + } _ => match path_dialog(id, &home, &name) { Some(d) => d, None => return false, @@ -273,15 +278,13 @@ pub fn show(app: &mut SoundApp, ctx: &egui::Context) { } } Dialog::RenameTrack { id, name } => { - let r = ui.text_edit_singleline(name); - r.request_focus(); + name_field(ui, name); if buttons(ui, "OK", enter) { action = Some(("track.rename".into(), json!({"track": id.0, "name": name}))); } } Dialog::RenameClip { id, name } => { - let r = ui.text_edit_singleline(name); - r.request_focus(); + name_field(ui, name); if buttons(ui, "OK", enter) { action = Some(("clip.rename".into(), json!({"clip": id.0, "name": name}))); } @@ -436,6 +439,17 @@ fn buttons(ui: &mut egui::Ui, ok: &str, enter: bool) -> bool { pressed } +/// A name field that takes the focus with its text selected, so typing replaces the old name. +fn name_field(ui: &mut egui::Ui, name: &mut String) { + let mut out = egui::TextEdit::singleline(name).show(ui); + if !out.response.has_focus() { + out.response.request_focus(); + let all = egui::text::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(name.chars().count())); + out.state.cursor.set_char_range(Some(all)); + out.state.store(ui.ctx(), out.response.id); + } +} + /// The command the tempo-change dialog runs on OK. fn tempo_change_action(at: Samples, bpm: f64) -> (String, Value) { ("event.tempo".into(), json!({"bpm": bpm, "at": at})) @@ -515,6 +529,18 @@ mod tests { assert!(matches!(d.open, Some(Dialog::ScoreSetup { bars_per_system: 4, .. }))); } + #[test] + fn clip_rename_opens_the_rename_dialog_for_the_selected_clip() { + let mut e = soundcraft_engine::demo::demo_engine(); + let mut d = Dialogs::default(); + e.execute("edit.select_none", &json!({})).unwrap(); + assert!(!d.open_for_command(&e, "clip.rename")); + let clip = e.session().track_by_name("Kick").unwrap().clips()[0].clone(); + e.execute("edit.select", &json!({"clips": [clip.id.0]})).unwrap(); + assert!(d.open_for_command(&e, "clip.rename")); + assert!(matches!(d.open, Some(Dialog::RenameClip { id, ref name }) if id == clip.id && *name == clip.name)); + } + #[test] fn tempo_change_dialog_runs_a_real_command_at_its_position() { let (cmd, params) = tempo_change_action(96_000, 133.5); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 89aca57..0e540e0 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -159,6 +159,7 @@ const DIALOG_COMMANDS: &[&str] = &[ "event.quantize", "event.transpose", "edit.strip_silence", + "clip.rename", ]; /// Shared menu hierarchy for egui and native desktop hosts. diff --git a/crates/ui-egui/tests/rename_dialog.rs b/crates/ui-egui/tests/rename_dialog.rs new file mode 100644 index 0000000..7b10b73 --- /dev/null +++ b/crates/ui-egui/tests/rename_dialog.rs @@ -0,0 +1,30 @@ +use egui::{Event, Key, Modifiers, vec2}; +use egui_kittest::Harness; +use serde_json::json; +use soundcraft_ui_egui::{Services, SoundApp, menus}; + +#[test] +fn typing_in_the_rename_clip_dialog_replaces_the_old_name() { + let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default()); + let clip = app.engine.session().track_by_name("Kick").unwrap().clips()[0].clone(); + app.run("edit.select", json!({"clips": [clip.id.0]})).unwrap(); + menus::invoke_menu(&mut app, "clip.rename", "Clip > Rename..."); + let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_ui_state( + |ui, app: &mut SoundApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + app, + ); + for _ in 0..3 { + h.step(); + } + h.input_mut().events.push(Event::Text("Intro".into())); + h.step(); + h.input_mut().events.push(Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }); + h.step(); + let session = h.state().engine.session(); + let renamed = session.tracks.iter().flat_map(|t| t.clips()).find(|c| c.id == clip.id).map(|c| c.name.clone()); + assert_eq!(renamed.as_deref(), Some("Intro")); +}