160 lines
8.0 KiB
Rust
160 lines
8.0 KiB
Rust
//! Real Edit/Mix panel workflows, persistence, and command ownership without OS windows.
|
|
use egui::{Event, Modifiers, PointerButton, vec2};
|
|
use egui_kittest::{Harness, kittest::Queryable};
|
|
use serde_json::json;
|
|
use soundcraft_ui_egui::{MainWindow, Services, SoundApp, UiState, docking::Panel};
|
|
|
|
fn harness(state: Option<UiState>, width: f32, scale: f32) -> Harness<'static, SoundApp> {
|
|
let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
|
|
app.run("track.new", json!({"name":"Voice"})).unwrap();
|
|
if let Some(state) = state {
|
|
app.ui = state;
|
|
}
|
|
let mut h = Harness::builder().with_size(vec2(width, 900.0)).with_pixels_per_point(scale).build_ui_state(
|
|
|ui, app: &mut SoundApp| {
|
|
app.logic(&ui.ctx().clone());
|
|
app.ui(ui);
|
|
},
|
|
app,
|
|
);
|
|
h.run_steps(5);
|
|
h
|
|
}
|
|
|
|
fn drag(h: &mut Harness<'_, SoundApp>, from: egui::Pos2, to: egui::Pos2) {
|
|
h.event(Event::PointerMoved(from));
|
|
h.run_steps(2);
|
|
h.event(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
|
h.run_steps(2);
|
|
for step in 1..=8 {
|
|
h.event(Event::PointerMoved(from.lerp(to, step as f32 / 8.0)));
|
|
h.run_steps(1);
|
|
}
|
|
h.event(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
|
h.run_steps(4);
|
|
}
|
|
|
|
#[test]
|
|
fn real_drag_float_close_reopen_reload_retains_transport_and_mixer_commands() {
|
|
let mut h = harness(None, 1400.0, 1.0);
|
|
let source = h.get_by_role_and_label(egui::accesskit::Role::Tab, "Clips").rect().center();
|
|
let target = h.get_by_role_and_label(egui::accesskit::Role::Tab, "Tracks and Groups").rect().center();
|
|
drag(&mut h, source, target);
|
|
assert_eq!(h.state().ui.docking.layout.location(&Panel::Clips).unwrap().anchor, Some(Panel::Tracks));
|
|
h.get_by_role_and_label(egui::accesskit::Role::Tab, "Clips").click_button(PointerButton::Secondary);
|
|
h.run_steps(3);
|
|
h.get_by_label("Float panel").click();
|
|
h.run_steps(5);
|
|
let rect = h.state().ui.docking.layout.floating.iter().find(|g| g.panels.contains(&Panel::Clips)).unwrap().rect;
|
|
h.get_by_role_and_label(egui::accesskit::Role::Tab, "Clips").click_button(PointerButton::Secondary);
|
|
h.run_steps(3);
|
|
h.get_by_label("Close panel").click();
|
|
h.run_steps(4);
|
|
assert!(!h.state().ui.show_clip_list);
|
|
h.state_mut().run("ui.dock", json!({"operation":"open","panel":"clips"})).unwrap();
|
|
h.run_steps(4);
|
|
assert_eq!(h.state().ui.docking.layout.floating.iter().find(|g| g.panels.contains(&Panel::Clips)).unwrap().rect, rect);
|
|
let saved = serde_json::to_string(&h.state().ui).unwrap();
|
|
let expected = serde_json::to_value(&h.state().ui.docking).unwrap();
|
|
let mut reloaded = harness(Some(serde_json::from_str(&saved).unwrap()), 1400.0, 1.0);
|
|
assert_eq!(serde_json::to_value(&reloaded.state().ui.docking).unwrap(), expected);
|
|
reloaded.state_mut().run("ui.dock", json!({"operation":"move","panel":"clips","target":"tracks","zone":"center"})).unwrap();
|
|
reloaded.run_steps(4);
|
|
reloaded.get_by_role_and_label(egui::accesskit::Role::Tab, "Tracks and Groups").click();
|
|
reloaded.run_steps(3);
|
|
reloaded.get_by_label("Trim (F6)").click();
|
|
reloaded.run_steps(3);
|
|
assert_eq!(reloaded.state().engine.session().edit.tool, soundcraft_model::Tool::Trim);
|
|
assert!(!reloaded.state().engine.transport.playing);
|
|
let edit = serde_json::to_value(&reloaded.state().ui.docking).unwrap();
|
|
reloaded.state_mut().ui.window = MainWindow::Mix;
|
|
reloaded.run_steps(5);
|
|
reloaded.state_mut().run("ui.dock", json!({"operation":"float","panel":"tracks","rect":[900,70,220,420]})).unwrap();
|
|
reloaded.run_steps(4);
|
|
assert_eq!(serde_json::to_value(&reloaded.state().ui.docking).unwrap(), edit);
|
|
assert!(!reloaded.state().ui.mix_docking.layout.contains(&Panel::Clips));
|
|
let record = reloaded.get_by_label("Record enable");
|
|
// A real pointer click must remain outside the floating panel's occluding rectangle.
|
|
assert!(!egui::Rect::from_min_size(egui::pos2(900.0, 70.0), vec2(220.0, 420.0)).contains(record.rect().center()));
|
|
record.click();
|
|
reloaded.run_steps(3);
|
|
assert!(reloaded.state().engine.session().tracks[0].mixer.record_arm);
|
|
assert!(!reloaded.state().engine.transport.recording);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_docking_requests_are_atomic_in_edit_and_mix() {
|
|
let mut h = harness(None, 1400.0, 1.0);
|
|
for window in [MainWindow::Edit, MainWindow::Mix] {
|
|
h.state_mut().ui.window = window;
|
|
h.run_steps(4);
|
|
let before = serde_json::to_value(&h.state().ui).unwrap();
|
|
for params in [
|
|
json!({"operation":"close","panel":"canvas"}),
|
|
json!({"operation":"move","panel":"tracks","target":"canvas","zone":"center"}),
|
|
json!({"operation":"float","panel":"tracks","rect":[0,0,-1,100]}),
|
|
] {
|
|
assert!(h.state_mut().run("ui.dock", params).is_err());
|
|
assert_eq!(serde_json::to_value(&h.state().ui).unwrap(), before);
|
|
}
|
|
if window == MainWindow::Mix {
|
|
assert!(h.state_mut().run("ui.dock", json!({"operation":"open","panel":"midi"})).is_err());
|
|
}
|
|
assert_eq!(h.state().engine.session().tracks.len(), 1);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn docking_renders_edit_and_mix_default_floating_and_redocked() {
|
|
for width in [1000.0, 1400.0] {
|
|
for scale in [1.0, 2.0] {
|
|
for window in [MainWindow::Edit, MainWindow::Mix] {
|
|
let mut h = harness(Some(UiState { window, ..Default::default() }), width, scale);
|
|
h.state_mut().engine = soundcraft_engine::demo::demo_engine();
|
|
for state in ["default", "floating", "redocked"] {
|
|
if state == "floating" {
|
|
h.state_mut().run("ui.dock", json!({"operation":"float","panel":"tracks","rect":[100,80,260,500]})).unwrap();
|
|
} else if state == "redocked" {
|
|
h.state_mut().run("ui.dock", json!({"operation":"move","panel":"tracks","target":"canvas","zone":"left"})).unwrap();
|
|
}
|
|
h.run_steps(5);
|
|
h.state().ui.docking.validate().unwrap();
|
|
h.state().ui.mix_docking.validate().unwrap();
|
|
if let Ok(directory) = std::env::var("CRAFT_UI_DOCKING_DIR") {
|
|
std::fs::create_dir_all(&directory).unwrap();
|
|
h.render().unwrap().save(format!("{directory}/sound-{window:?}-{width}-{scale}-{state}.png")).unwrap();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn reset_is_enabled_and_resolves_in_native_and_in_window_menu_trees() {
|
|
use soundcraft_ui_egui::menus;
|
|
let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
|
|
let path = "Window > Reset Panel Layout";
|
|
let extra: Vec<_> = menus::UI_COMMANDS.iter().map(|(id, _, path, _)| (*path, *id)).collect();
|
|
assert_eq!(menus::command_for_path(path, &extra).as_deref(), Some("ui.dock.reset"));
|
|
let tree = menus::tree();
|
|
let window = tree.iter().find(|node| node.label == "Window").unwrap();
|
|
assert!(window.children.iter().any(|node| node.path == path));
|
|
assert!(menus::item_state(&app, path, Some("ui.dock.reset")).0);
|
|
app.run("ui.dock", json!({"operation":"float","panel":"tracks","rect":[50,60,260,420]})).unwrap();
|
|
menus::invoke_menu(&mut app, "ui.dock.reset", path);
|
|
assert!(!app.ui.docking.layout.floating.iter().any(|group| group.panels.contains(&Panel::Tracks)));
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_preferences_restore_the_theme_and_validate_default_docking() {
|
|
let mut value = serde_json::to_value(UiState { theme: soundcraft_ui_egui::theme::ThemeMode::Light, ..Default::default() }).unwrap();
|
|
let object = value.as_object_mut().unwrap();
|
|
object.remove("docking");
|
|
object.remove("mix_docking");
|
|
let restored: UiState = serde_json::from_value(value).unwrap();
|
|
assert_eq!(restored.theme, soundcraft_ui_egui::theme::ThemeMode::Light);
|
|
restored.docking.validate().unwrap();
|
|
restored.mix_docking.validate_mix().unwrap();
|
|
}
|