diff --git a/crates/ui-egui/src/panels/tracks.rs b/crates/ui-egui/src/panels/tracks.rs index b4fd9bb8..1be81dd0 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,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, 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 cd8f758d..b0407b29 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,35 @@ 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![]); + + // "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"); +}