From 7dfa7e470fd3da51f1716ab8607a9622893891a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20Mittelst=C3=A4dt?= <62167861+TheGalitube@users.noreply.github.com> Date: Mon, 2 Nov 2026 08:25:43 +0000 Subject: [PATCH 1/2] Indent tracks nested in folders Fixes #1252 Co-Authored-By: GPT-5.6-Luna --- crates/ui-egui/src/panels/tracks.rs | 27 +++++++++++++++--- crates/ui-egui/tests/it/track_list.rs | 40 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/panels/tracks.rs b/crates/ui-egui/src/panels/tracks.rs index b4fd9bb8..2c746b1c 100644 --- a/crates/ui-egui/src/panels/tracks.rs +++ b/crates/ui-egui/src/panels/tracks.rs @@ -46,9 +46,28 @@ pub fn tracks_and_groups(app: &mut SoundApp, ui: &mut Ui) { if crate::widgets::icon_button(&mut ui.new_child(egui::UiBuilder::new().max_rect(plus)), plus.size(), "plus", false, &tip).clicked() { let _ = app.dialogs.open_for_command(&app.engine, "track.new"); } - let tracks: Vec<(u64, String, bool, [u8; 3], bool)> = { + let tracks: Vec<(u64, String, bool, [u8; 3], bool, usize)> = { let s = app.engine.session(); - s.tracks.iter().map(|x| (x.id.0, x.name.clone(), !x.hidden, x.color, s.edit.selected_tracks.contains(&x.id))).collect() + s.tracks + .iter() + .map(|x| { + let mut current = x.folder; + let mut depth = 0; + let mut seen = Vec::new(); + while let Some(folder_id) = current { + if seen.contains(&folder_id) { + break; + } + seen.push(folder_id); + let Some(folder) = s.track(folder_id) else { + break; + }; + depth += 1; + current = folder.folder; + } + (x.id.0, x.name.clone(), !x.hidden, x.color, s.edit.selected_tracks.contains(&x.id), depth) + }) + .collect() }; let hidden = egui::scroll_area::ScrollBarVisibility::AlwaysHidden; let lists = with_pending_offset(egui::ScrollArea::vertical().id_salt("tracks_scroll"), ui, "tracks_scroll") @@ -56,7 +75,7 @@ pub fn tracks_and_groups(app: &mut SoundApp, ui: &mut Ui) { .auto_shrink([false, false]) .scroll_bar_visibility(hidden); let out = lists.show(ui, |ui| { - for (id, name, shown, color, sel) in tracks { + for (id, name, shown, color, sel, depth) in tracks { let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 17.0), Sense::click_and_drag()); resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, sel, &name)); if sel { @@ -70,7 +89,7 @@ pub fn tracks_and_groups(app: &mut SoundApp, ui: &mut Ui) { } ui.painter().rect_filled(Rect::from_min_size(pos2(r.min.x + 20.0, r.min.y + 3.0), vec2(4.0, 11.0)), 0.0, rgb(color)); ui.painter().with_clip_rect(r).text( - pos2(r.min.x + 30.0, r.center().y), + pos2(r.min.x + 30.0 + depth as f32 * 10.0, r.center().y), Align2::LEFT_CENTER, &name, regular(11.5), diff --git a/crates/ui-egui/tests/it/track_list.rs b/crates/ui-egui/tests/it/track_list.rs index cd8f758d..c2579962 100644 --- a/crates/ui-egui/tests/it/track_list.rs +++ b/crates/ui-egui/tests/it/track_list.rs @@ -50,6 +50,16 @@ impl Rig { .expect("row not in the Tracks list") } + fn text_x(&self, name: &str) -> f32 { + self.shapes + .iter() + .find_map(|c| match &c.shape { + Shape::Text(t) if t.galley.text() == name && t.pos.x < 150.0 => Some(t.pos.x), + _ => None, + }) + .expect("text not in the Tracks list") + } + fn press(&mut self, at: Pos2, pressed: bool) { self.frame(vec![ Event::PointerMoved(at), @@ -90,3 +100,33 @@ fn clicking_a_track_list_row_still_selects_the_track() { r.press(at, false); assert_eq!(r.app.engine.session().edit.selected_tracks, vec![r.order()[2]]); } + +#[test] +fn a_track_inside_a_folder_is_indented_in_the_track_list() { + let mut r = Rig::new(); + let child = r.order()[1]; + let child_name = r.app.engine.session().track(child).unwrap().name.clone(); + r.app.run("track.move_to_new_folder", json!({"tracks": [child.0], "name": "Folder"})).unwrap(); + r.frame(vec![]); + let folder = r.app.engine.session().tracks.iter().find(|track| track.is_folder()).unwrap(); + + assert!(r.text_x(&child_name) > r.text_x(&folder.name), "child is not indented under the folder"); +} + +#[test] +fn deeply_nested_tracks_keep_indenting_past_six_folders() { + let mut r = Rig::new(); + let child = r.order()[1]; + let child_name = r.app.engine.session().track(child).unwrap().name.clone(); + r.app.run("track.move_to_new_folder", json!({"tracks": [child.0], "name": "Folder 1"})).unwrap(); + + let mut parent = r.app.engine.session().tracks.iter().find(|track| track.name == "Folder 1").unwrap().id; + for depth in 2..=7 { + r.app.run("track.move_to_new_folder", json!({"tracks": [parent.0], "name": format!("Folder {depth}")})).unwrap(); + parent = r.app.engine.session().tracks.iter().find(|track| track.name == format!("Folder {depth}")).unwrap().id; + } + r.frame(vec![]); + + let parent_name = r.app.engine.session().track(parent).unwrap().name.clone(); + assert!(r.text_x(&child_name) > r.text_x(&parent_name), "deeply nested child is not indented under its parent"); +} From 7546ab992b9b33ef22cedd64be706c29e545ea2c Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 3 Nov 2026 09:56:05 -0500 Subject: [PATCH 2/2] Cap the folder indent at eight levels and test against the track's own folder Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/tracks.rs | 3 ++- crates/ui-egui/tests/it/track_list.rs | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/panels/tracks.rs b/crates/ui-egui/src/panels/tracks.rs index 2c746b1c..1be81dd0 100644 --- a/crates/ui-egui/src/panels/tracks.rs +++ b/crates/ui-egui/src/panels/tracks.rs @@ -89,7 +89,8 @@ pub fn tracks_and_groups(app: &mut SoundApp, ui: &mut Ui) { } ui.painter().rect_filled(Rect::from_min_size(pos2(r.min.x + 20.0, r.min.y + 3.0), vec2(4.0, 11.0)), 0.0, rgb(color)); ui.painter().with_clip_rect(r).text( - pos2(r.min.x + 30.0 + depth as f32 * 10.0, r.center().y), + // Indent stops growing at eight levels so a deep name keeps room in the column. + pos2(r.min.x + 30.0 + depth.min(8) as f32 * 10.0, r.center().y), Align2::LEFT_CENTER, &name, regular(11.5), diff --git a/crates/ui-egui/tests/it/track_list.rs b/crates/ui-egui/tests/it/track_list.rs index c2579962..b0407b29 100644 --- a/crates/ui-egui/tests/it/track_list.rs +++ b/crates/ui-egui/tests/it/track_list.rs @@ -127,6 +127,8 @@ fn deeply_nested_tracks_keep_indenting_past_six_folders() { } r.frame(vec![]); - let parent_name = r.app.engine.session().track(parent).unwrap().name.clone(); - assert!(r.text_x(&child_name) > r.text_x(&parent_name), "deeply nested child is not indented under its parent"); + // "Folder 1" is the child's own folder and the seventh level; "Folder 7" is the outermost. + assert_eq!(r.app.engine.session().track(parent).unwrap().name, "Folder 7"); + assert!(r.text_x(&child_name) > r.text_x("Folder 1"), "deeply nested child is not indented under its own folder"); + assert!(r.text_x("Folder 1") > r.text_x("Folder 2"), "each level indents further"); }