mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-13 23:08:25 +00:00
Merge pull request #219 from Jafz2001/feat/routing-folders
Complete routing folder management
This commit is contained in:
3 files changed
+295
-16
No files matched your search
+237
-10
@@ -3,7 +3,7 @@
|
||||
use super::*;
|
||||
use crate::cmd;
|
||||
use serde_json::json;
|
||||
use soundcraft_model::{ChannelFormat, Group, GroupId, Playlist, Route, TrackHeight, TrackKind};
|
||||
use soundcraft_model::{ChannelFormat, Group, GroupId, Playlist, Route, Session, Track, TrackHeight, TrackKind};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
@@ -45,6 +45,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_selection,
|
||||
move_to_folder
|
||||
),
|
||||
cmd!("track.move_to_folder", "Move to Folder", [], None, "{tracks?, folder}", has_selection, move_into_folder),
|
||||
cmd!("track.remove_from_folder", "Remove from Folder", [], None, "{tracks?}", has_selection, remove_from_folder),
|
||||
cmd!(
|
||||
"track.change_width",
|
||||
"Change Track Width",
|
||||
@@ -256,6 +258,14 @@ fn new_track(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
t.instrument = Some(soundcraft_model::Insert::new(instrument.clone().unwrap_or_else(|| "subtractive_synth".into())));
|
||||
}
|
||||
}
|
||||
if kind == TrackKind::Folder {
|
||||
let folder_name = s.track(id).map(|track| track.name.clone()).unwrap_or_else(|| "Folder".into());
|
||||
let bus_name = unique_folder_bus_name(s, &format!("{folder_name} Bus"), None);
|
||||
let bus = s.add_bus(&bus_name, format);
|
||||
if let Some(track) = s.track_mut(id) {
|
||||
track.mixer.input = Route::Bus(bus);
|
||||
}
|
||||
}
|
||||
ids.push(id);
|
||||
}
|
||||
s.edit.selected_tracks = ids.clone();
|
||||
@@ -402,6 +412,17 @@ fn flag(
|
||||
Ok(json!({"value": target, "tracks": tracks}))
|
||||
}
|
||||
|
||||
/// A routing folder owns its summing bus; never reuse a user bus with the same name.
|
||||
fn unique_folder_bus_name(s: &Session, base: &str, own: Option<soundcraft_model::BusId>) -> String {
|
||||
for suffix in 1..=s.busses.len().saturating_add(2) {
|
||||
let candidate = if suffix == 1 { base.to_string() } else { format!("{base} {suffix}") };
|
||||
if !s.busses.iter().any(|bus| bus.name == candidate && Some(bus.id) != own) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
format!("{base} {}", s.busses.len().saturating_add(3))
|
||||
}
|
||||
|
||||
fn move_to_folder(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let tracks = tracks_required(e, "track.move_to_new_folder", p)?;
|
||||
let name = str_param(p, "name").map(str::to_string);
|
||||
@@ -415,7 +436,8 @@ fn move_to_folder(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
}
|
||||
// A routing folder sums its members through its own bus.
|
||||
let fname = s.track(fid).map(|x| x.name.clone()).unwrap_or_else(|| "Folder".into());
|
||||
let bus = s.add_bus(&format!("{fname} Bus"), ChannelFormat::Stereo);
|
||||
let bus_name = unique_folder_bus_name(s, &format!("{fname} Bus"), None);
|
||||
let bus = s.add_bus(&bus_name, ChannelFormat::Stereo);
|
||||
if let Some(f) = s.track_mut(fid) {
|
||||
f.mixer.input = Route::Bus(bus);
|
||||
}
|
||||
@@ -427,9 +449,118 @@ fn move_to_folder(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
}
|
||||
place_under(s, fid, &tracks);
|
||||
Ok(json!({"folder": fid, "bus": bus}))
|
||||
}
|
||||
|
||||
/// Whether `track` sits inside `folder`, directly or through nested folders.
|
||||
fn in_folder(s: &Session, track: TrackId, folder: TrackId) -> bool {
|
||||
let mut current = s.track(track).and_then(|track| track.folder);
|
||||
// The depth cap guards against a cyclic folder chain in a hostile session file.
|
||||
for _ in 0..64 {
|
||||
match current {
|
||||
Some(parent) if parent == folder => return true,
|
||||
Some(parent) => current = s.track(parent).and_then(|track| track.folder),
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Keep a folder's members together immediately below it, preserving their relative order.
|
||||
fn place_under(s: &mut Session, folder: TrackId, tracks: &[TrackId]) {
|
||||
// Moving a folder also moves its complete subtree as one stable block.
|
||||
let moved: Vec<_> = s
|
||||
.tracks
|
||||
.iter()
|
||||
.filter(|track| track.id != folder && (tracks.contains(&track.id) || tracks.iter().any(|root| in_folder(s, track.id, *root))))
|
||||
.cloned()
|
||||
.collect();
|
||||
let moved_ids: Vec<_> = moved.iter().map(|track| track.id).collect();
|
||||
s.tracks.retain(|track| !moved_ids.contains(&track.id));
|
||||
let ids: Vec<TrackId> = s.tracks.iter().map(|track| track.id).collect();
|
||||
let Some(folder_index) = ids.iter().position(|track| *track == folder) else { return };
|
||||
let mut insert_at = folder_index + 1;
|
||||
while ids.get(insert_at).is_some_and(|track| in_folder(s, *track, folder)) {
|
||||
insert_at += 1;
|
||||
}
|
||||
for (offset, track) in moved.into_iter().enumerate() {
|
||||
s.tracks.insert((insert_at + offset).min(s.tracks.len()), track);
|
||||
}
|
||||
}
|
||||
|
||||
fn folder_bus(s: &Session, folder: TrackId) -> Option<Route> {
|
||||
s.track(folder).map(|folder| folder.mixer.input.clone()).filter(|route| matches!(route, Route::Bus(_)))
|
||||
}
|
||||
|
||||
/// Keep only the selected roots so selecting a folder and one of its children preserves the subtree.
|
||||
fn root_tracks(s: &Session, tracks: Vec<TrackId>) -> Vec<TrackId> {
|
||||
tracks.iter().copied().filter(|track| !tracks.iter().any(|root| root != track && in_folder(s, *track, *root))).collect()
|
||||
}
|
||||
|
||||
fn move_into_folder(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let folder = track_param(e, "track.move_to_folder", p, "folder")?.ok_or_else(|| bad("track.move_to_folder", "`folder` required"))?;
|
||||
let tracks = tracks_required(e, "track.move_to_folder", p)?;
|
||||
let session = e.session_mut();
|
||||
if !session.track(folder).is_some_and(Track::is_folder) {
|
||||
return Err(bad("track.move_to_folder", "`folder` is not a folder track"));
|
||||
}
|
||||
// A folder cannot go inside itself or one of its descendants.
|
||||
let tracks: Vec<TrackId> = tracks.into_iter().filter(|track| *track != folder && !in_folder(session, folder, *track)).collect();
|
||||
let tracks = root_tracks(session, tracks);
|
||||
if tracks.is_empty() {
|
||||
return Err(bad("track.move_to_folder", "nothing to move into that folder"));
|
||||
}
|
||||
let bus = folder_bus(session, folder);
|
||||
for track in &tracks {
|
||||
let old_bus = session.track(*track).and_then(|track| track.folder).and_then(|parent| folder_bus(session, parent));
|
||||
if let Some(moved) = session.track_mut(*track) {
|
||||
moved.folder = Some(folder);
|
||||
if let Some(bus) = &bus
|
||||
&& (moved.mixer.output == Route::Main || Some(&moved.mixer.output) == old_bus.as_ref())
|
||||
{
|
||||
moved.mixer.output = bus.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
place_under(session, folder, &tracks);
|
||||
Ok(json!({"folder": folder, "moved": tracks.len()}))
|
||||
}
|
||||
|
||||
fn remove_from_folder(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let tracks = tracks_required(e, "track.remove_from_folder", p)?;
|
||||
let session = e.session_mut();
|
||||
let tracks = root_tracks(session, tracks);
|
||||
let mut removed = 0;
|
||||
for track in &tracks {
|
||||
let Some(folder) = session.track(*track).and_then(|track| track.folder) else { continue };
|
||||
let bus = folder_bus(session, folder);
|
||||
let parent = session.track(folder).and_then(|folder| folder.folder);
|
||||
let parent_bus = parent.and_then(|parent| folder_bus(session, parent));
|
||||
if let Some(moved) = session.track_mut(*track) {
|
||||
moved.folder = parent;
|
||||
if bus.as_ref() == Some(&moved.mixer.output) {
|
||||
moved.mixer.output = parent_bus.unwrap_or(Route::Main);
|
||||
}
|
||||
removed += 1;
|
||||
}
|
||||
let moved: Vec<_> =
|
||||
session.tracks.iter().filter(|candidate| candidate.id == *track || in_folder(session, candidate.id, *track)).cloned().collect();
|
||||
let moved_ids: Vec<_> = moved.iter().map(|candidate| candidate.id).collect();
|
||||
session.tracks.retain(|candidate| !moved_ids.contains(&candidate.id));
|
||||
if let Some(folder_index) = session.track_index(folder) {
|
||||
let mut insert_at = folder_index + 1;
|
||||
while session.tracks.get(insert_at).is_some_and(|candidate| in_folder(session, candidate.id, folder)) {
|
||||
insert_at += 1;
|
||||
}
|
||||
for (offset, moved) in moved.into_iter().enumerate() {
|
||||
session.tracks.insert((insert_at + offset).min(session.tracks.len()), moved);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(json!({"removed": removed}))
|
||||
}
|
||||
|
||||
fn change_width(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let tracks = tracks_required(e, "track.change_width", p)?;
|
||||
let f = str_param(p, "format").and_then(ChannelFormat::from_id).ok_or_else(|| bad("track.change_width", "`format` required"))?;
|
||||
@@ -446,16 +577,36 @@ fn change_width(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
fn delete(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let tracks = tracks_required(e, "track.delete", p)?;
|
||||
let s = e.session_mut();
|
||||
// Promote children of deleted folders and repair only routes that used the deleted
|
||||
// folder's own bus. Custom outputs remain untouched.
|
||||
let mut promoted = Vec::new();
|
||||
for child in &s.tracks {
|
||||
if tracks.contains(&child.id) {
|
||||
continue;
|
||||
}
|
||||
let Some(folder) = child.folder.filter(|folder| tracks.contains(folder)) else { continue };
|
||||
let mut parent = s.track(folder).and_then(|folder| folder.folder);
|
||||
for _ in 0..64 {
|
||||
match parent {
|
||||
Some(candidate) if tracks.contains(&candidate) => parent = s.track(candidate).and_then(|folder| folder.folder),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
promoted.push((child.id, folder_bus(s, folder), parent, parent.and_then(|parent| folder_bus(s, parent))));
|
||||
}
|
||||
for (child, old_bus, parent, parent_bus) in promoted {
|
||||
if let Some(child) = s.track_mut(child) {
|
||||
child.folder = parent;
|
||||
if old_bus.as_ref() == Some(&child.mixer.output) {
|
||||
child.mixer.output = parent_bus.unwrap_or(Route::Main);
|
||||
}
|
||||
}
|
||||
}
|
||||
s.tracks.retain(|t| !tracks.contains(&t.id));
|
||||
s.edit.selected_tracks.retain(|t| !tracks.contains(t));
|
||||
for g in &mut s.groups {
|
||||
g.members.retain(|t| !tracks.contains(t));
|
||||
}
|
||||
for t in &mut s.tracks {
|
||||
if t.folder.is_some_and(|f| tracks.contains(&f)) {
|
||||
t.folder = None;
|
||||
}
|
||||
}
|
||||
Ok(json!({"deleted": tracks.len()}))
|
||||
}
|
||||
|
||||
@@ -466,8 +617,19 @@ fn rename(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
if s.tracks.iter().any(|x| x.name == name && x.id != t) {
|
||||
return Err(bad("track.rename", format!("a track named `{name}` already exists")));
|
||||
}
|
||||
if let Some(tr) = s.track_mut(t) {
|
||||
tr.name = name.chars().take(64).collect();
|
||||
let name: String = name.chars().take(64).collect();
|
||||
let bus = s.track(t).and_then(|track| match (track.kind, &track.mixer.input) {
|
||||
(TrackKind::Folder, Route::Bus(bus)) => Some(*bus),
|
||||
_ => None,
|
||||
});
|
||||
if let Some(track) = s.track_mut(t) {
|
||||
track.name.clone_from(&name);
|
||||
}
|
||||
if let Some(bus) = bus {
|
||||
let bus_name = unique_folder_bus_name(s, &format!("{name} Bus"), Some(bus));
|
||||
if let Some(bus) = s.busses.iter_mut().find(|candidate| candidate.id == bus) {
|
||||
bus.name = bus_name;
|
||||
}
|
||||
}
|
||||
Ok(json!({"name": name}))
|
||||
}
|
||||
@@ -717,8 +879,73 @@ mod tests {
|
||||
assert!(r["bus"].is_u64());
|
||||
let s = e.session();
|
||||
let kick = s.track_by_name("Kick").unwrap();
|
||||
let snare = s.track_by_name("Snare").unwrap();
|
||||
let folder = s.tracks.iter().find(|track| track.kind == TrackKind::Folder).unwrap();
|
||||
assert!(matches!(kick.mixer.output, Route::Bus(_)));
|
||||
assert!(s.tracks.iter().any(|t| t.kind == TrackKind::Folder && t.mixer.input == kick.mixer.output));
|
||||
assert_eq!(folder.mixer.input, kick.mixer.output);
|
||||
assert_eq!(folder.mixer.input, snare.mixer.output);
|
||||
assert_eq!(s.track_index(folder.id).unwrap() + 1, s.track_index(kick.id).unwrap());
|
||||
assert_eq!(s.track_index(kick.id).unwrap() + 1, s.track_index(snare.id).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_folder_owns_a_unique_bus_and_renames_it() {
|
||||
let mut e = crate::demo::demo_engine();
|
||||
let first = e.execute("track.new", &json!({"kind": "folder", "name": "Guitars"})).unwrap();
|
||||
let second = e.execute("track.new", &json!({"kind": "folder", "name": "Vocals"})).unwrap();
|
||||
let first = TrackId(first["tracks"][0].as_u64().unwrap());
|
||||
let second = TrackId(second["tracks"][0].as_u64().unwrap());
|
||||
let first_bus = match e.session().track(first).unwrap().mixer.input {
|
||||
Route::Bus(bus) => bus,
|
||||
_ => panic!("folder did not receive a bus"),
|
||||
};
|
||||
let second_bus = match e.session().track(second).unwrap().mixer.input {
|
||||
Route::Bus(bus) => bus,
|
||||
_ => panic!("folder did not receive a bus"),
|
||||
};
|
||||
assert_ne!(first_bus, second_bus);
|
||||
e.session_mut().add_bus("Percussion Bus", ChannelFormat::Stereo);
|
||||
e.execute("track.rename", &json!({"track": first.0, "name": "Percussion"})).unwrap();
|
||||
let renamed = e.session().busses.iter().find(|bus| bus.id == first_bus).unwrap();
|
||||
assert_eq!(renamed.name, "Percussion Bus 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracks_can_move_between_nested_folders_and_leave() {
|
||||
let mut e = crate::demo::demo_engine();
|
||||
let outer = e.execute("track.new", &json!({"kind": "folder", "name": "Band"})).unwrap();
|
||||
let inner = e.execute("track.new", &json!({"kind": "folder", "name": "Drums"})).unwrap();
|
||||
let outer = TrackId(outer["tracks"][0].as_u64().unwrap());
|
||||
let inner = TrackId(inner["tracks"][0].as_u64().unwrap());
|
||||
|
||||
e.execute("track.move_to_folder", &json!({"tracks": ["Kick"], "folder": inner.0})).unwrap();
|
||||
e.execute("track.move_to_folder", &json!({"tracks": [inner.0], "folder": outer.0})).unwrap();
|
||||
assert!(e.execute("track.move_to_folder", &json!({"tracks": [outer.0], "folder": inner.0})).is_err());
|
||||
|
||||
let kick = e.session().track_by_name("Kick").unwrap();
|
||||
assert_eq!(kick.folder, Some(inner));
|
||||
assert_eq!(kick.mixer.output, e.session().track(inner).unwrap().mixer.input);
|
||||
assert_eq!(e.session().track_index(outer).unwrap() + 1, e.session().track_index(inner).unwrap());
|
||||
assert_eq!(e.session().track_index(inner).unwrap() + 1, e.session().track_index(kick.id).unwrap());
|
||||
|
||||
e.execute("track.remove_from_folder", &json!({"tracks": ["Kick"]})).unwrap();
|
||||
let kick = e.session().track_by_name("Kick").unwrap();
|
||||
assert_eq!(kick.folder, Some(outer));
|
||||
assert_eq!(kick.mixer.output, e.session().track(outer).unwrap().mixer.input);
|
||||
|
||||
e.execute("track.move_to_folder", &json!({"tracks": ["Kick"], "folder": inner.0})).unwrap();
|
||||
e.execute("track.remove_from_folder", &json!({"tracks": [inner.0]})).unwrap();
|
||||
let inner_track = e.session().track(inner).unwrap();
|
||||
let kick = e.session().track_by_name("Kick").unwrap();
|
||||
assert_eq!(inner_track.folder, None);
|
||||
assert_eq!(inner_track.mixer.output, Route::Main);
|
||||
assert_eq!(kick.folder, Some(inner));
|
||||
assert_eq!(e.session().track_index(inner).unwrap() + 1, e.session().track_index(kick.id).unwrap());
|
||||
|
||||
e.execute("track.delete", &json!({"tracks": [inner.0]})).unwrap();
|
||||
let kick = e.session().track_by_name("Kick").unwrap();
|
||||
assert_eq!(kick.folder, None);
|
||||
assert_eq!(kick.mixer.output, Route::Main);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -472,9 +472,22 @@ fn put_clips(s: &mut Session, placed: Vec<(TrackId, Clip)>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the Edit window shows track `t`: not hidden, and not inside a closed folder.
|
||||
/// Whether the Edit window shows track `t`: not hidden, and not inside a closed folder at any level.
|
||||
pub fn shown(s: &Session, t: &Track) -> bool {
|
||||
!t.hidden && t.folder.and_then(|f| s.track(f)).is_none_or(|f| f.folder_open)
|
||||
!t.hidden && !folder_collapsed(s, t)
|
||||
}
|
||||
|
||||
/// Hidden because a folder at any level above this track is closed.
|
||||
fn folder_collapsed(s: &Session, track: &Track) -> bool {
|
||||
let mut current = track.folder;
|
||||
for _ in 0..64 {
|
||||
let Some(folder) = current.and_then(|folder| s.track(folder)) else { return false };
|
||||
if !folder.folder_open {
|
||||
return true;
|
||||
}
|
||||
current = folder.folder;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Where the tracks of clips `ids` go when track `from` goes to `to`: each keeps its distance
|
||||
|
||||
@@ -299,6 +299,19 @@ pub(crate) fn height_targets(s: &Session, m: egui::Modifiers, own: &[TrackId]) -
|
||||
s.tracks.iter().filter(|t| own.contains(&t.id) || (m.alt && extra(t))).map(|t| t.id.0).collect()
|
||||
}
|
||||
|
||||
/// How many folders this track sits in, for the name's indent (at most 6 shown).
|
||||
fn folder_depth(s: &Session, track: &Track) -> usize {
|
||||
let mut current = track.folder;
|
||||
let mut depth = 0;
|
||||
while let Some(folder) = current.and_then(|folder| s.track(folder))
|
||||
&& depth < 6
|
||||
{
|
||||
depth += 1;
|
||||
current = folder.folder;
|
||||
}
|
||||
depth
|
||||
}
|
||||
|
||||
/// 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();
|
||||
@@ -969,7 +982,8 @@ fn track_header(app: &mut SoundApp, ui: &mut Ui, track: &Track, head: Rect, sele
|
||||
}
|
||||
}
|
||||
// Name, highlighted when the track is selected, and the playlist menu beside it.
|
||||
let name_r = at(16.0, 1.5, 97.0, 15.0);
|
||||
let indent = folder_depth(app.engine.session(), track) as f32 * 10.0;
|
||||
let name_r = at(16.0 + indent, 1.5, 97.0 - indent, 15.0);
|
||||
if selected {
|
||||
ui.painter().rect_filled(name_r, 2.0, t.name_field_sel);
|
||||
}
|
||||
@@ -1408,21 +1422,46 @@ pub(crate) fn track_context_menu(app: &mut SoundApp, ui: &mut Ui, id: TrackId) {
|
||||
("Move to New Folder...", "track.move_to_new_folder"),
|
||||
("Group...", "track.group"),
|
||||
];
|
||||
let selection: Vec<u64> = {
|
||||
let selected = &app.engine.session().edit.selected_tracks;
|
||||
if selected.contains(&id) { selected.iter().map(|track| track.0).collect() } else { vec![tid] }
|
||||
};
|
||||
for (label, cmd) in items {
|
||||
if ui.button(label).clicked() {
|
||||
if cmd == "ui.rename_track" {
|
||||
let name = app.engine.session().track(id).map(|t| t.name.clone()).unwrap_or_default();
|
||||
app.dialogs.open_rename_track(id, &name);
|
||||
} else {
|
||||
let _ = app.run(cmd, json!({"tracks": [tid]}));
|
||||
let _ = app.run(cmd, json!({"tracks": selection}));
|
||||
}
|
||||
}
|
||||
}
|
||||
let folders: Vec<(u64, String)> = app
|
||||
.engine
|
||||
.session()
|
||||
.tracks
|
||||
.iter()
|
||||
.filter(|track| track.is_folder() && !selection.contains(&track.id.0))
|
||||
.map(|track| (track.id.0, track.name.clone()))
|
||||
.collect();
|
||||
if !folders.is_empty() {
|
||||
ui.menu_button("Move to Folder", |ui| {
|
||||
for (folder, name) in &folders {
|
||||
if ui.button(name).clicked() {
|
||||
let _ = app.run("track.move_to_folder", json!({"tracks": selection, "folder": folder}));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if app.engine.session().track(id).is_some_and(|track| track.folder.is_some()) && ui.button("Remove from Folder").clicked() {
|
||||
let _ = app.run("track.remove_from_folder", json!({"tracks": selection}));
|
||||
}
|
||||
ui.separator();
|
||||
ui.menu_button("Track Height", |ui| {
|
||||
for h in TrackHeight::ALL {
|
||||
if ui.button(h.label()).clicked() {
|
||||
let tracks = height_targets(app.engine.session(), ui.input(|i| i.modifiers), &[id]);
|
||||
let own: Vec<TrackId> = selection.iter().map(|&t| TrackId(t)).collect();
|
||||
let tracks = height_targets(app.engine.session(), ui.input(|i| i.modifiers), &own);
|
||||
let _ = app.run("track.height", json!({"tracks": tracks, "height": h.label()}));
|
||||
}
|
||||
}
|
||||
@@ -1432,7 +1471,7 @@ pub(crate) fn track_context_menu(app: &mut SoundApp, ui: &mut Ui, id: TrackId) {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(120.0, 14.0), Sense::click());
|
||||
ui.painter().rect_filled(r, 2.0, rgb(*c));
|
||||
if resp.clicked() {
|
||||
let _ = app.run("track.color", json!({"tracks": [tid], "color": i}));
|
||||
let _ = app.run("track.color", json!({"tracks": selection, "color": i}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user