Track height: resize live while dragging, Pro Tools modifiers and Ctrl+Up/Down
Dragging a track's bottom edge showed nothing until release; then the track jumped to the nearest height preset. Pro Tools' track height modifiers and keys were missing too. Dragging: - The edge follows the pointer as in Pro Tools: the track steps through Micro…Extreme while the pointer moves, and the tracks below move with it. The whole drag is one undo step (execute_merged, ended on release like fader moves). - Cmd (Ctrl on Windows and Linux) drags to any height from Micro to Extreme instead of snapping. The model gains TrackHeight::Custom (points), and track.height takes a number of points as well as a preset name. - Alt resizes every track the Edit window shows; Alt+Shift resizes the selected tracks too. The track menu's Track Height items follow the same rule. - The drag is measured from where it started, not from the edge's current place: when the tracks above the dragged one resize they move its edge, and edge-relative math flipped between two heights on every frame. This also fixes rows showing playlist lanes, which a plain click on the edge used to make taller. Keys: - New commands track.height_up and track.height_down move each track one preset taller or shorter; a track already at Extreme or Micro is left alone, with no undo step. Control+Up/Down runs them on macOS, as in Pro Tools. Windows and Linux use Ctrl+Up/Down, because the Start key Pro Tools uses there isn't available. Alt steps every shown track, Alt+Shift the selected ones. - Ctrl+Up/Down used to run Extend Edit Up/Down, keys Pro Tools doesn't use for them. Those commands now have Pro Tools' Control+Shift+P and Control+Shift+; on macOS (macOS sends the latter as Control+:). Windows and Linux reach them from Edit > Selection. - Off macOS, menus no longer show Mac Control chords that can't be pressed there (Time Lock's Ctrl+Alt+L, Toggle Recent Playlist's Ctrl+Alt+Shift+Left, the new Extend Edit keys); the arrow chords that fixed() maps to Ctrl keep their labels. Track headers show each row of controls only once it fits: the keys from 46 points and the view and automation selectors from 86, where each row ends. The old 30 and 60 point cutoffs suited the presets only and cut controls off at heights in between. Every preset draws exactly as before. Tests: the model's custom heights, steps and JSON; the engine's point heights and steps, with no undo step when nothing changes; in the UI, the live drag and its single undo step, Cmd, Alt and Alt+Shift drags that hold steady while the pointer does, the height keys on macOS, Windows and Linux, and the Extend Edit keys on macOS.
This commit is contained in:
parent
6cf896e76c
commit
1953f2ecc6
|
|
@ -72,8 +72,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
cmd!(noundo "edit.move_selection_right", "Move Edit Right", ["Edit", "Selection"], Some("Cmd+Alt+'"), "{}", always, |e, _| shift_selection(e, 1)),
|
||||
cmd!(noundo "edit.halve_selection", "Halve Edit", ["Edit", "Selection"], None, "{}", always, |e, _| scale_selection(e, 0.5)),
|
||||
cmd!(noundo "edit.double_selection", "Double Edit", ["Edit", "Selection"], None, "{}", always, |e, _| scale_selection(e, 2.0)),
|
||||
cmd!(noundo "edit.extend_selection_up", "Extend Edit Up", ["Edit", "Selection"], None, "{}", has_tracks, |e, _| extend_tracks(e, -1)),
|
||||
cmd!(noundo "edit.extend_selection_down", "Extend Edit Down", ["Edit", "Selection"], None, "{}", has_tracks, |e, _| extend_tracks(e, 1)),
|
||||
cmd!(noundo "edit.extend_selection_up", "Extend Edit Up", ["Edit", "Selection"], Some("Ctrl+Shift+P"), "{}", has_tracks, |e, _| extend_tracks(e, -1)),
|
||||
cmd!(noundo "edit.extend_selection_down", "Extend Edit Down", ["Edit", "Selection"], Some("Ctrl+Shift+;"), "{}", has_tracks, |e, _| extend_tracks(e, 1)),
|
||||
cmd!(noundo "edit.remove_selection_top", "Remove Edit from Top", ["Edit", "Selection"], None, "{}", has_tracks, |e, _| {
|
||||
let s = e.session_mut();
|
||||
if !s.edit.selected_tracks.is_empty() {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,21 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
cmd!("track.rename", "Rename Track", [], None, "{track, name}", has_tracks, rename),
|
||||
cmd!("track.comments", "Track Comments", [], None, "{track, comments}", has_tracks, comments),
|
||||
cmd!("track.color", "Track Color", [], None, "{tracks?, color: [r,g,b] | palette index}", has_selection, color),
|
||||
cmd!("track.height", "Track Height", [], None, "{tracks?, height: micro|mini|small|medium|large|jumbo|extreme}", has_selection, height),
|
||||
cmd!(
|
||||
"track.height",
|
||||
"Track Height",
|
||||
[],
|
||||
None,
|
||||
"{tracks?, height: micro|mini|small|medium|large|jumbo|extreme | points}",
|
||||
has_selection,
|
||||
height
|
||||
),
|
||||
cmd!("track.height_up", "Increase Track Height", [], Some("Ctrl+Up"), "{tracks?} — one preset taller", has_selection, |e, p| height_step(
|
||||
e, p, true
|
||||
)),
|
||||
cmd!("track.height_down", "Decrease Track Height", [], Some("Ctrl+Down"), "{tracks?} — one preset shorter", has_selection, |e, p| {
|
||||
height_step(e, p, false)
|
||||
}),
|
||||
cmd!("track.view", "Track View", [], None, "{tracks?, view: waveform|blocks|notes|volume|pan|mute|<automation id>}", has_selection, view),
|
||||
cmd!("track.hide", "Hide Track", [], None, "{tracks?, hidden?: bool}", has_selection, |e, p| flag(
|
||||
e,
|
||||
|
|
@ -485,7 +499,10 @@ fn color(e: &mut Engine, p: &Value) -> Result<Value> {
|
|||
|
||||
fn height(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let tracks = tracks_required(e, "track.height", p)?;
|
||||
let h = str_param(p, "height").and_then(TrackHeight::from_id).ok_or_else(|| bad("track.height", "unknown height"))?;
|
||||
let h = match p.get("height").and_then(Value::as_f64) {
|
||||
Some(points) => TrackHeight::from_points(points as f32),
|
||||
None => str_param(p, "height").and_then(TrackHeight::from_id).ok_or_else(|| bad("track.height", "unknown height"))?,
|
||||
};
|
||||
let s = e.session_mut();
|
||||
for t in &tracks {
|
||||
if let Some(tr) = s.track_mut(*t) {
|
||||
|
|
@ -495,6 +512,26 @@ fn height(e: &mut Engine, p: &Value) -> Result<Value> {
|
|||
Ok(json!({"height": h}))
|
||||
}
|
||||
|
||||
fn height_step(e: &mut Engine, p: &Value, taller: bool) -> Result<Value> {
|
||||
let tracks = tracks_required(e, if taller { "track.height_up" } else { "track.height_down" }, p)?;
|
||||
let mut steps = Vec::new();
|
||||
for tr in tracks.iter().filter_map(|t| e.session().track(*t)) {
|
||||
let h = tr.height.step(taller);
|
||||
if h != tr.height {
|
||||
steps.push((tr.id, h));
|
||||
}
|
||||
}
|
||||
if !steps.is_empty() {
|
||||
let s = e.session_mut();
|
||||
for (t, h) in &steps {
|
||||
if let Some(tr) = s.track_mut(*t) {
|
||||
tr.height = *h;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(json!({"changed": steps.len()}))
|
||||
}
|
||||
|
||||
fn view(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let tracks = tracks_required(e, "track.view", p)?;
|
||||
let v = str_param(p, "view").ok_or_else(|| bad("track.view", "`view` required"))?.to_string();
|
||||
|
|
@ -680,4 +717,20 @@ mod tests {
|
|||
assert!(matches!(kick.mixer.output, Route::Bus(_)));
|
||||
assert!(s.tracks.iter().any(|t| t.kind == TrackKind::Folder && t.mixer.input == kick.mixer.output));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heights_take_points_and_step_through_the_presets() {
|
||||
let mut e = crate::demo::demo_engine();
|
||||
let height = |e: &Engine, name: &str| e.session().track_by_name(name).map(|t| t.height);
|
||||
e.execute("track.height", &json!({"tracks": ["Kick"], "height": 57})).unwrap();
|
||||
assert_eq!(height(&e, "Kick"), Some(TrackHeight::Custom(57)));
|
||||
e.execute("track.height_up", &json!({"tracks": ["Kick", "Snare"]})).unwrap();
|
||||
assert_eq!((height(&e, "Kick"), height(&e, "Snare")), (Some(TrackHeight::Medium), Some(TrackHeight::Large)));
|
||||
e.execute("track.height", &json!({"tracks": ["Kick"], "height": "extreme"})).unwrap();
|
||||
let steps = e.undo_history().len();
|
||||
assert_eq!(e.execute("track.height_up", &json!({"tracks": ["Kick"]})).unwrap()["changed"], 0);
|
||||
assert_eq!(e.undo_history().len(), steps);
|
||||
e.execute("track.height_down", &json!({"tracks": ["Kick"]})).unwrap();
|
||||
assert_eq!(height(&e, "Kick"), Some(TrackHeight::Jumbo));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@ impl TrackKind {
|
|||
/// Track heights (View › Track Height presets).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TrackHeight {
|
||||
/// Any height from Micro to Extreme, in points.
|
||||
Custom(u16),
|
||||
Micro,
|
||||
Mini,
|
||||
Small,
|
||||
|
|
@ -108,6 +110,7 @@ impl TrackHeight {
|
|||
];
|
||||
pub fn points(self) -> f32 {
|
||||
match self {
|
||||
TrackHeight::Custom(points) => f32::from(points).clamp(TrackHeight::Micro.points(), TrackHeight::Extreme.points()),
|
||||
TrackHeight::Micro => 16.0,
|
||||
TrackHeight::Mini => 24.0,
|
||||
TrackHeight::Small => 46.0,
|
||||
|
|
@ -120,6 +123,7 @@ impl TrackHeight {
|
|||
}
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
TrackHeight::Custom(_) => "custom",
|
||||
TrackHeight::Micro => "micro",
|
||||
TrackHeight::Mini => "mini",
|
||||
TrackHeight::Small => "small",
|
||||
|
|
@ -133,6 +137,18 @@ impl TrackHeight {
|
|||
pub fn from_id(s: &str) -> Option<TrackHeight> {
|
||||
TrackHeight::ALL.into_iter().find(|h| h.label().eq_ignore_ascii_case(s))
|
||||
}
|
||||
/// The preset this many points tall, else a custom height clamped to Micro..Extreme.
|
||||
pub fn from_points(points: f32) -> TrackHeight {
|
||||
let p = points.max(TrackHeight::Micro.points()).min(TrackHeight::Extreme.points()).round() as u16;
|
||||
TrackHeight::ALL.into_iter().find(|h| h.points() == f32::from(p)).unwrap_or(TrackHeight::Custom(p))
|
||||
}
|
||||
/// The next preset taller (or shorter) than this height; Extreme (or Micro) stays.
|
||||
pub fn step(self, taller: bool) -> TrackHeight {
|
||||
let p = self.points();
|
||||
let next =
|
||||
if taller { TrackHeight::ALL.into_iter().find(|h| h.points() > p) } else { TrackHeight::ALL.into_iter().rev().find(|h| h.points() < p) };
|
||||
next.unwrap_or(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default track colour palette (original, chosen for SoundCraft).
|
||||
|
|
@ -337,4 +353,20 @@ mod tests {
|
|||
assert_eq!(TrackKind::from_id(k.id()), Some(k));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_heights_round_to_presets_and_step_through_them() {
|
||||
assert_eq!(TrackHeight::from_points(88.0), TrackHeight::Medium);
|
||||
assert_eq!(TrackHeight::from_points(57.4), TrackHeight::Custom(57));
|
||||
assert_eq!(TrackHeight::from_points(9999.0), TrackHeight::Extreme);
|
||||
assert_eq!(TrackHeight::from_points(f32::NAN), TrackHeight::Micro);
|
||||
assert_eq!(TrackHeight::Custom(u16::MAX).points(), 440.0);
|
||||
assert_eq!(TrackHeight::Custom(57).step(true), TrackHeight::Medium);
|
||||
assert_eq!(TrackHeight::Custom(57).step(false), TrackHeight::Small);
|
||||
assert_eq!(TrackHeight::Extreme.step(true), TrackHeight::Extreme);
|
||||
assert_eq!(TrackHeight::Micro.step(false), TrackHeight::Micro);
|
||||
let json = serde_json::to_string(&TrackHeight::Custom(57)).unwrap();
|
||||
assert_eq!(serde_json::from_str::<TrackHeight>(&json).unwrap(), TrackHeight::Custom(57));
|
||||
assert_eq!(serde_json::from_str::<TrackHeight>("\"Large\"").unwrap(), TrackHeight::Large);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -259,6 +259,12 @@ fn in_edit_window(s: &Session, t: &Track) -> bool {
|
|||
!t.hidden && t.folder.and_then(|f| s.track(f)).is_none_or(|f| f.folder_open)
|
||||
}
|
||||
|
||||
/// Pro Tools' rule for track height: Alt reaches every shown track, Alt+Shift the selected ones too.
|
||||
pub(crate) fn height_targets(s: &Session, m: egui::Modifiers, own: &[TrackId]) -> Vec<u64> {
|
||||
let extra = |t: &Track| if m.shift { s.edit.selected_tracks.contains(&t.id) } else { in_edit_window(s, t) };
|
||||
s.tracks.iter().filter(|t| own.contains(&t.id) || (m.alt && extra(t))).map(|t| t.id.0).collect()
|
||||
}
|
||||
|
||||
/// Pro Tools' track-name click: Shift adds the tracks up to the nearest selected one, Cmd/Ctrl adds or removes one.
|
||||
pub(crate) fn click_track(app: &mut SoundApp, id: TrackId, mods: egui::Modifiers, shown: impl Fn(&Session, &Track) -> bool) {
|
||||
let s = app.engine.session();
|
||||
|
|
@ -777,13 +783,26 @@ fn track_header(app: &mut SoundApp, ui: &mut Ui, track: &Track, head: Rect, sele
|
|||
if eresp.hovered() || eresp.dragged() {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical);
|
||||
}
|
||||
if eresp.drag_stopped()
|
||||
if eresp.drag_started() {
|
||||
ui.data_mut(|d| d.insert_temp(eresp.id, track.height));
|
||||
}
|
||||
if eresp.dragged()
|
||||
&& let Some(p) = ui.ctx().pointer_latest_pos()
|
||||
&& let Some(origin) = ui.input(|i| i.pointer.press_origin()).filter(|o| o.y != p.y)
|
||||
{
|
||||
let want = (p.y - head.min.y).max(10.0);
|
||||
let best = TrackHeight::ALL.into_iter().min_by_key(|h| ((h.points() - want).abs() * 10.0) as i64).unwrap_or(TrackHeight::Medium);
|
||||
if best != track.height {
|
||||
let _ = app.run("track.height", json!({"tracks": [id.0], "height": best.label()}));
|
||||
let start = ui.data(|d| d.get_temp::<TrackHeight>(eresp.id)).unwrap_or(track.height);
|
||||
let want = start.points() + p.y - origin.y;
|
||||
let m = ui.input(|i| i.modifiers);
|
||||
let best = if m.command {
|
||||
TrackHeight::from_points(want)
|
||||
} else {
|
||||
TrackHeight::ALL.into_iter().min_by_key(|h| ((h.points() - want).abs() * 10.0) as i64).unwrap_or(start)
|
||||
};
|
||||
let s = app.engine.session();
|
||||
let tracks = height_targets(s, m, &[id]);
|
||||
if tracks.iter().any(|t| s.track(TrackId(*t)).is_some_and(|tr| tr.height != best)) {
|
||||
let _ = app.engine.execute_merged("track.height", &json!({"tracks": tracks, "height": best.points()}), &format!("track_height:{}", id.0));
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
let x0 = head.min.x + 12.0;
|
||||
|
|
@ -896,7 +915,7 @@ fn track_header(app: &mut SoundApp, ui: &mut Ui, track: &Track, head: Rect, sele
|
|||
egui::Popup::menu(&resp).show(|ui| crate::mix_window::instrument_menu(app, ui, id));
|
||||
}
|
||||
}
|
||||
if h < 30.0 {
|
||||
if h < 46.0 {
|
||||
return;
|
||||
}
|
||||
// Buttons row.
|
||||
|
|
@ -926,7 +945,7 @@ fn track_header(app: &mut SoundApp, ui: &mut Ui, track: &Track, head: Rect, sele
|
|||
let _ = app.run("mix.mute", json!({"track": id.0}));
|
||||
}
|
||||
}
|
||||
if h < 60.0 {
|
||||
if h < 86.0 {
|
||||
return;
|
||||
}
|
||||
// View selector.
|
||||
|
|
@ -1107,7 +1126,8 @@ fn track_context_menu(app: &mut SoundApp, ui: &mut Ui, id: TrackId) {
|
|||
ui.menu_button("Track Height", |ui| {
|
||||
for h in TrackHeight::ALL {
|
||||
if ui.button(h.label()).clicked() {
|
||||
let _ = app.run("track.height", json!({"tracks": [tid], "height": h.label()}));
|
||||
let tracks = height_targets(app.engine.session(), ui.input(|i| i.modifiers), &[id]);
|
||||
let _ = app.run("track.height", json!({"tracks": tracks, "height": h.label()}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -175,9 +175,17 @@ fn fixed(app: &mut SoundApp, key: Key, m: Modifiers, mac: bool) -> bool {
|
|||
}
|
||||
true
|
||||
}
|
||||
Key::ArrowUp | Key::ArrowDown if m.ctrl && !m.shift => {
|
||||
let id = if key == Key::ArrowUp { "edit.extend_selection_up" } else { "edit.extend_selection_down" };
|
||||
let _ = app.run(id, json!({}));
|
||||
// Pro Tools' Control+Up/Down track height, on Ctrl off macOS too (its Start key isn't available there).
|
||||
Key::ArrowUp | Key::ArrowDown if m.ctrl && (m.alt || !m.shift) => {
|
||||
let s = app.engine.session();
|
||||
let tracks = crate::edit_window::height_targets(s, m, &s.edit.selected_tracks);
|
||||
let id = if key == Key::ArrowUp { "track.height_up" } else { "track.height_down" };
|
||||
let _ = app.run(id, if tracks.is_empty() { json!({}) } else { json!({"tracks": tracks}) });
|
||||
true
|
||||
}
|
||||
// Extend Edit Down is Control+Shift+;, which macOS sends as Control+:.
|
||||
Key::Semicolon | Key::Colon if mac && m.ctrl && m.shift && !m.alt && !m.command => {
|
||||
let _ = app.run("edit.extend_selection_down", json!({}));
|
||||
true
|
||||
}
|
||||
// Ctrl+Shift+Up/Down nudge clip gain off macOS too (the registry's Ctrl is the Mac Control key).
|
||||
|
|
@ -258,15 +266,19 @@ fn tab_clip(app: &mut SoundApp, back: bool) {
|
|||
/// Shortcut text for display. The registry stores macOS-style strings ("Cmd+N"); off macOS the Cmd
|
||||
/// modifier is the Ctrl key. Mac-only Control chords ("Cmd+Ctrl+S") have no key there, so they show nothing.
|
||||
pub fn shortcut_label(s: &str, mac: bool) -> String {
|
||||
if mac || !s.split('+').any(|p| p == "Cmd") {
|
||||
let has = |m: &str| s.split('+').any(|p| p == m);
|
||||
if mac || (!has("Cmd") && !has("Ctrl")) || PC_CTRL_CHORDS.contains(&s) {
|
||||
return s.to_string();
|
||||
}
|
||||
if s.split('+').any(|p| p == "Ctrl") {
|
||||
if has("Ctrl") {
|
||||
return String::new();
|
||||
}
|
||||
s.split('+').map(|p| if p == "Cmd" { "Ctrl" } else { p }).collect::<Vec<_>>().join("+")
|
||||
}
|
||||
|
||||
/// Mac Control chords that `fixed` also maps to Ctrl off macOS.
|
||||
const PC_CTRL_CHORDS: [&str; 4] = ["Ctrl+Up", "Ctrl+Down", "Ctrl+Shift+Up", "Ctrl+Shift+Down"];
|
||||
|
||||
#[cfg(test)]
|
||||
mod label_tests {
|
||||
use super::shortcut_label;
|
||||
|
|
@ -280,6 +292,10 @@ mod label_tests {
|
|||
assert_eq!(shortcut_label("Shift+R", false), "Shift+R");
|
||||
assert_eq!(shortcut_label("Cmd+Ctrl+S", false), "");
|
||||
assert_eq!(shortcut_label("", false), "");
|
||||
assert_eq!(shortcut_label("Ctrl+Up", false), "Ctrl+Up");
|
||||
assert_eq!(shortcut_label("Ctrl+Shift+Down", false), "Ctrl+Shift+Down");
|
||||
assert_eq!(shortcut_label("Ctrl+Shift+P", false), "");
|
||||
assert_eq!(shortcut_label("Ctrl+Alt+L", false), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
use egui::os::OperatingSystem;
|
||||
use egui::{Event, Key, Modifiers, PointerButton, Pos2, RawInput, Rect, pos2, vec2};
|
||||
use serde_json::json;
|
||||
use soundcraft_model::TrackHeight;
|
||||
use soundcraft_ui_egui::{Services, SoundApp, shortcuts};
|
||||
|
||||
fn frame(ctx: &egui::Context, app: &mut SoundApp, events: Vec<Event>) {
|
||||
let raw = RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1600.0, 1000.0))), events, ..Default::default() };
|
||||
ctx.run_ui(raw, |ui| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
})
|
||||
.textures_delta
|
||||
.clear();
|
||||
}
|
||||
|
||||
fn press(ctx: &egui::Context, app: &mut SoundApp, key: Key, modifiers: Modifiers) {
|
||||
let events = [true, false].map(|pressed| Event::Key { key, physical_key: None, pressed, repeat: false, modifiers }).to_vec();
|
||||
ctx.run_ui(RawInput { events, ..Default::default() }, |ui| shortcuts::handle(app, ui.ctx())).textures_delta.clear();
|
||||
}
|
||||
|
||||
fn button(pos: Pos2, pressed: bool) -> Event {
|
||||
Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Default::default() }
|
||||
}
|
||||
|
||||
fn rig() -> (egui::Context, SoundApp) {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
let ctx = egui::Context::default();
|
||||
frame(&ctx, &mut app, vec![]);
|
||||
frame(&ctx, &mut app, vec![]);
|
||||
(ctx, app)
|
||||
}
|
||||
|
||||
fn id(app: &SoundApp, name: &str) -> u64 {
|
||||
app.engine.session().track_by_name(name).unwrap().id.0
|
||||
}
|
||||
|
||||
fn height(app: &SoundApp, name: &str) -> TrackHeight {
|
||||
app.engine.session().track_by_name(name).unwrap().height
|
||||
}
|
||||
|
||||
fn row(app: &SoundApp, name: &str) -> [f32; 4] {
|
||||
let id = id(app, name);
|
||||
app.edit_layout.rows.iter().find(|(t, _)| *t == id).unwrap().1
|
||||
}
|
||||
|
||||
fn select(app: &mut SoundApp, names: &[&str]) {
|
||||
let ids: Vec<u64> = names.iter().map(|n| id(app, n)).collect();
|
||||
app.engine.execute("edit.select", &json!({"tracks": ids})).unwrap();
|
||||
}
|
||||
|
||||
/// Drags `name`'s bottom edge by `dy` with `m` held, holding still for a few frames before letting go.
|
||||
fn drag_edge(ctx: &egui::Context, app: &mut SoundApp, name: &str, dy: f32, m: Modifiers) {
|
||||
let [x, _, _, bottom] = row(app, name);
|
||||
let at = pos2(x + 40.0, bottom - 1.0);
|
||||
frame(ctx, app, vec![Event::ModifiersChanged(m), Event::PointerMoved(at)]);
|
||||
frame(ctx, app, vec![button(at, true)]);
|
||||
frame(ctx, app, vec![Event::PointerMoved(at + vec2(0.0, dy))]);
|
||||
let held: Vec<TrackHeight> = app.engine.session().tracks.iter().map(|t| t.height).collect();
|
||||
for _ in 0..3 {
|
||||
frame(ctx, app, vec![]);
|
||||
assert!(app.engine.session().tracks.iter().map(|t| t.height).eq(held.iter().copied()), "the heights hold while the pointer does");
|
||||
}
|
||||
frame(ctx, app, vec![button(at + vec2(0.0, dy), false), Event::ModifiersChanged(Modifiers::NONE)]);
|
||||
frame(ctx, app, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_a_track_edge_resizes_it_live_in_one_undo_step() {
|
||||
let (ctx, mut app) = rig();
|
||||
assert_eq!(height(&app, "Kick"), TrackHeight::Medium);
|
||||
let [x, _, _, bottom] = row(&app, "Kick");
|
||||
let at = |dy: f32| pos2(x + 40.0, bottom + dy);
|
||||
|
||||
frame(&ctx, &mut app, vec![Event::PointerMoved(at(-1.0))]);
|
||||
frame(&ctx, &mut app, vec![button(at(-1.0), true)]);
|
||||
for (dy, want) in [(20.0, TrackHeight::Medium), (44.0, TrackHeight::Large), (132.0, TrackHeight::Jumbo), (40.0, TrackHeight::Large)] {
|
||||
frame(&ctx, &mut app, vec![Event::PointerMoved(at(dy))]);
|
||||
frame(&ctx, &mut app, vec![]);
|
||||
let [_, y0, _, y1] = row(&app, "Kick");
|
||||
assert_eq!((height(&app, "Kick"), y1 - y0), (want, want.points()), "{dy}");
|
||||
}
|
||||
frame(&ctx, &mut app, vec![button(at(40.0), false)]);
|
||||
frame(&ctx, &mut app, vec![]);
|
||||
|
||||
app.engine.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(height(&app, "Kick"), TrackHeight::Medium);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_drags_to_any_height_alt_resizes_every_track_and_alt_shift_the_selected_ones() {
|
||||
let (ctx, mut app) = rig();
|
||||
drag_edge(&ctx, &mut app, "Kick", 20.0, Modifiers::COMMAND);
|
||||
assert_eq!((height(&app, "Kick"), height(&app, "Snare")), (TrackHeight::Custom(108), TrackHeight::Medium));
|
||||
|
||||
drag_edge(&ctx, &mut app, "Snare", 44.0, Modifiers::ALT);
|
||||
assert!(
|
||||
app.edit_layout.rows.iter().all(|(t, _)| app.engine.session().track(soundcraft_model::TrackId(*t)).unwrap().height == TrackHeight::Large)
|
||||
);
|
||||
|
||||
select(&mut app, &["Hats", "Bass"]);
|
||||
drag_edge(&ctx, &mut app, "Kick", -86.0, Modifiers { alt: true, shift: true, ..Modifiers::NONE });
|
||||
let small = [TrackHeight::Small, TrackHeight::Large, TrackHeight::Small, TrackHeight::Small];
|
||||
assert_eq!(["Kick", "Snare", "Hats", "Bass"].map(|n| height(&app, n)), small);
|
||||
|
||||
app.engine.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(["Kick", "Snare", "Hats", "Bass"].map(|n| height(&app, n)), [TrackHeight::Large; 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_arrows_step_the_selected_tracks_and_ctrl_alt_arrows_every_track() {
|
||||
let pc_ctrl = Modifiers { ctrl: true, command: true, ..Modifiers::NONE };
|
||||
for (os, ctrl) in [(OperatingSystem::Mac, Modifiers::CTRL), (OperatingSystem::Windows, pc_ctrl), (OperatingSystem::Nix, pc_ctrl)] {
|
||||
let (ctx, mut app) = rig();
|
||||
ctx.set_os(os);
|
||||
select(&mut app, &["Kick"]);
|
||||
press(&ctx, &mut app, Key::ArrowUp, ctrl);
|
||||
assert_eq!((height(&app, "Kick"), height(&app, "Snare")), (TrackHeight::Large, TrackHeight::Medium), "{os:?}");
|
||||
press(&ctx, &mut app, Key::ArrowDown, ctrl);
|
||||
press(&ctx, &mut app, Key::ArrowDown, ctrl);
|
||||
assert_eq!(height(&app, "Kick"), TrackHeight::Small, "{os:?}");
|
||||
press(&ctx, &mut app, Key::ArrowUp, Modifiers { alt: true, ..ctrl });
|
||||
assert_eq!((height(&app, "Kick"), height(&app, "Snare")), (TrackHeight::Medium, TrackHeight::Large), "{os:?}");
|
||||
assert_eq!(app.engine.session().edit.selected_tracks.len(), 1, "{os:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_shift_p_and_semicolon_extend_the_edit_on_macos() {
|
||||
let (ctx, mut app) = rig();
|
||||
ctx.set_os(OperatingSystem::Mac);
|
||||
select(&mut app, &["Snare"]);
|
||||
let ctrl_shift = Modifiers { ctrl: true, shift: true, ..Modifiers::NONE };
|
||||
press(&ctx, &mut app, Key::P, ctrl_shift);
|
||||
press(&ctx, &mut app, Key::Colon, ctrl_shift);
|
||||
let selected: Vec<u64> = app.engine.session().edit.selected_tracks.iter().map(|t| t.0).collect();
|
||||
assert_eq!(selected, ["Kick", "Snare", "Hats"].map(|n| id(&app, n)));
|
||||
}
|
||||
Loading…
Reference in New Issue