This commit is contained in:
vcoppe committed 2026-10-14 09:22:07 +02:00
1 parent 41b9116c6a
commit 97d597e139
181 files changed
+1970 -2643

No files matched your search

@@ -0,0 +1,447 @@
use crate::{
Apply, Clipboard, CommandError, Destination, FileId, Place, Selection, State, TrackId, transfer,
};
/// Where moved elements go: a list of the file tree, and the position in it. The position is
/// counted among the elements of the list that are not moved, like the position of a dragged
/// element in the list it is dropped in.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MoveTarget {
/// The list of files.
Files { index: usize },
/// The tracks of a file.
Tracks { file_id: FileId, index: usize },
/// The segments of a track.
Segments {
file_id: FileId,
trk_id: TrackId,
index: usize,
},
/// The waypoints of a file.
Waypoints { file_id: FileId, index: usize },
}
/// Moves elements to a place of the file tree (what drag and drop does), with their ids. The
/// moved elements are selected.
///
/// What can go where:
/// - files: among the files;
/// - tracks: among the files (each track becomes a file), or among the tracks of a file;
/// - segments: among the files (each segment becomes a file), among the tracks of a file (each
/// segment becomes a track), or among the segments of a track;
/// - waypoints, or all the waypoints of a file (the waypoints node): among the waypoints of a
/// file.
#[derive(Debug)]
pub struct Move {
pub what: Selection,
pub to: MoveTarget,
}
impl From<MoveTarget> for Destination {
fn from(target: MoveTarget) -> Self {
match target {
MoveTarget::Files { index } => Destination::Files(Place::Index(index)),
MoveTarget::Tracks { file_id, index } => {
Destination::Tracks(file_id, Place::Index(index))
}
MoveTarget::Segments {
file_id,
trk_id,
index,
} => Destination::Segments(file_id, trk_id, Place::Index(index)),
MoveTarget::Waypoints { file_id, index } => {
Destination::Waypoints(file_id, Place::Index(index))
}
}
}
}
impl Apply for Move {
fn apply(self, state: &mut State) -> Result<(), CommandError> {
let clipboard = Clipboard::new(&self.what, state.files, &state.order.0, true)
.ok_or(CommandError::NothingToDo)?;
*state.selection = transfer(state, &clipboard.content, true, self.to.into())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::rc::Rc;
use crate::{
File, Track, TrackInfo, TrackSegment, TrackSegmentId, Waypoint, WaypointChunk, WaypointId,
Waypoints, engine::command::fixture::Fixture, new_file, waypoint_ids,
};
use super::*;
/// A file with the given tracks (name, number of segments) and number of waypoints.
fn file(name: &str, tracks: &[(&str, usize)], waypoints: usize) -> File {
let mut file = new_file(
name,
tracks
.iter()
.map(|(name, segments)| Track {
info: TrackInfo {
name: Some((*name).to_owned()),
..Default::default()
},
trkseg: (0..*segments).map(|_| TrackSegment::default()).collect(),
..Default::default()
})
.collect(),
);
if waypoints > 0 {
file.wpt = Waypoints::new([WaypointChunk {
wpt: (0..waypoints).map(|_| Waypoint::default()).collect(),
..Default::default()
}]);
}
file
}
fn add(fx: &mut Fixture, file: File) -> FileId {
let id = file.id;
fx.files.insert(id, Rc::new(file));
fx.order.0.push(id);
id
}
fn track_ids(fx: &Fixture, id: FileId) -> Vec<TrackId> {
fx.files[&id].trk.iter().map(|trk| trk.id).collect()
}
fn segment_ids(fx: &Fixture, id: FileId, trk: usize) -> Vec<TrackSegmentId> {
fx.files[&id].trk[trk].trkseg.iter().map(|s| s.id).collect()
}
fn wpt_ids(fx: &Fixture, id: FileId) -> Vec<WaypointId> {
waypoint_ids(&fx.files[&id]).collect()
}
fn do_move(fx: &mut Fixture, what: Selection, to: MoveTarget) -> Result<(), CommandError> {
Move { what, to }.apply(&mut fx.state())
}
fn tracks(file_id: FileId, ids: &[TrackId]) -> Selection {
Selection::Track {
file_id,
trk_ids: ids.iter().copied().collect(),
}
}
fn segments(file_id: FileId, trk_id: TrackId, ids: &[TrackSegmentId]) -> Selection {
Selection::TrackSegment {
file_id,
trk_id,
trkseg_ids: ids.iter().copied().collect(),
}
}
fn waypoints(file_id: FileId, ids: &[WaypointId]) -> Selection {
Selection::Waypoint {
file_id,
wpt_ids: ids.iter().copied().collect(),
}
}
fn files(ids: &[FileId]) -> Selection {
Selection::File {
file_ids: ids.iter().copied().collect(),
}
}
#[test]
fn test_reorder_tracks_in_a_file() {
let build = || {
let mut fx = Fixture::default();
let a = add(
&mut fx,
file("a", &[("0", 1), ("1", 1), ("2", 1), ("3", 1)], 0),
);
let t = track_ids(&fx, a);
(fx, a, t)
};
let at = |a, index| MoveTarget::Tracks { file_id: a, index };
// the position counts the tracks that stay
let (mut fx, a, t) = build();
do_move(&mut fx, tracks(a, &[t[0]]), at(a, 1)).unwrap();
assert_eq!(track_ids(&fx, a), vec![t[1], t[0], t[2], t[3]]);
assert_eq!(fx.selection, tracks(a, &[t[0]]));
let (mut fx, a, t) = build();
do_move(&mut fx, tracks(a, &[t[3]]), at(a, 0)).unwrap();
assert_eq!(track_ids(&fx, a), vec![t[3], t[0], t[1], t[2]]);
// several at once, as a block, in their order; past the end is the end
let (mut fx, a, t) = build();
do_move(&mut fx, tracks(a, &[t[1], t[0]]), at(a, 99)).unwrap();
assert_eq!(track_ids(&fx, a), vec![t[2], t[3], t[0], t[1]]);
assert_eq!(fx.selection, tracks(a, &[t[0], t[1]]));
}
#[test]
fn test_move_tracks_to_another_file_or_to_new_files() {
let mut fx = Fixture::default();
let a = add(&mut fx, file("a", &[("t0", 1), ("t1", 1)], 0));
let b = add(&mut fx, file("b", &[("b0", 1), ("b1", 1)], 0));
let c = add(&mut fx, file("c", &[], 0));
let t = track_ids(&fx, a);
let b_tracks = track_ids(&fx, b);
do_move(
&mut fx,
tracks(a, &[t[0]]),
MoveTarget::Tracks {
file_id: b,
index: 1,
},
)
.unwrap();
assert_eq!(track_ids(&fx, a), vec![t[1]]);
assert_eq!(track_ids(&fx, b), vec![b_tracks[0], t[0], b_tracks[1]]);
assert_eq!(fx.selection, tracks(b, &[t[0]]));
// as a new file, at a position of the list of files
do_move(&mut fx, tracks(a, &[t[1]]), MoveTarget::Files { index: 1 }).unwrap();
assert!(track_ids(&fx, a).is_empty());
assert_eq!(fx.order.0.len(), 4);
let new = fx.order.0[1];
assert_eq!(fx.order.0[0], a);
assert_eq!(fx.order.0[2..], [b, c]);
assert_eq!(track_ids(&fx, new), vec![t[1]]);
assert_eq!(fx.files[&new].info.name, "t1");
assert_eq!(fx.selection, files(&[new]));
}
#[test]
fn test_move_segments() {
let build = || {
let mut fx = Fixture::default();
let a = add(&mut fx, file("a", &[("t0", 3), ("t1", 1)], 0));
let t = track_ids(&fx, a);
(fx, a, t)
};
// within a track
let (mut fx, a, t) = build();
let s = segment_ids(&fx, a, 0);
do_move(
&mut fx,
segments(a, t[0], &[s[2]]),
MoveTarget::Segments {
file_id: a,
trk_id: t[0],
index: 0,
},
)
.unwrap();
assert_eq!(segment_ids(&fx, a, 0), vec![s[2], s[0], s[1]]);
assert_eq!(fx.selection, segments(a, t[0], &[s[2]]));
// to another track
let (mut fx, a, t) = build();
let s = segment_ids(&fx, a, 0);
let other = segment_ids(&fx, a, 1);
do_move(
&mut fx,
segments(a, t[0], &[s[0], s[1]]),
MoveTarget::Segments {
file_id: a,
trk_id: t[1],
index: 0,
},
)
.unwrap();
assert_eq!(segment_ids(&fx, a, 0), vec![s[2]]);
assert_eq!(segment_ids(&fx, a, 1), vec![s[0], s[1], other[0]]);
// as new tracks of a file
let (mut fx, a, t) = build();
let s = segment_ids(&fx, a, 0);
do_move(
&mut fx,
segments(a, t[0], &[s[1]]),
MoveTarget::Tracks {
file_id: a,
index: 1,
},
)
.unwrap();
assert_eq!(fx.files[&a].trk.len(), 3);
assert_eq!(segment_ids(&fx, a, 1), vec![s[1]]);
assert_eq!(segment_ids(&fx, a, 0), vec![s[0], s[2]]);
assert_eq!(fx.files[&a].trk[0].id, t[0]);
assert_eq!(fx.files[&a].trk[2].id, t[1]);
// as new files
let (mut fx, a, t) = build();
let s = segment_ids(&fx, a, 0);
do_move(
&mut fx,
segments(a, t[0], &[s[0], s[1]]),
MoveTarget::Files { index: 0 },
)
.unwrap();
assert_eq!(fx.order.0.len(), 3);
assert_eq!(fx.order.0[2], a);
assert_eq!(fx.files[&fx.order.0[0]].info.name, "t0");
assert_eq!(segment_ids(&fx, a, 0), vec![s[2]]);
}
#[test]
fn test_move_waypoints() {
let build = || {
let mut fx = Fixture::default();
let a = add(&mut fx, file("a", &[], 4));
let b = add(&mut fx, file("b", &[], 2));
(fx, a, b)
};
let at = |file_id, index| MoveTarget::Waypoints { file_id, index };
// within a file
let (mut fx, a, _) = build();
let w = wpt_ids(&fx, a);
do_move(&mut fx, waypoints(a, &[w[3]]), at(a, 1)).unwrap();
assert_eq!(wpt_ids(&fx, a), vec![w[0], w[3], w[1], w[2]]);
assert_eq!(fx.selection, waypoints(a, &[w[3]]));
// to the start of another file
let (mut fx, a, b) = build();
let w = wpt_ids(&fx, a);
let before = wpt_ids(&fx, b);
do_move(&mut fx, waypoints(a, &[w[0], w[1]]), at(b, 0)).unwrap();
assert_eq!(wpt_ids(&fx, a), w[2..].to_vec());
assert_eq!(wpt_ids(&fx, b), [&w[..2], &before[..]].concat());
// all the waypoints of a file
let (mut fx, a, b) = build();
let w = wpt_ids(&fx, a);
let before = wpt_ids(&fx, b);
do_move(&mut fx, Selection::Waypoints { file_id: a }, at(b, 1)).unwrap();
assert!(wpt_ids(&fx, a).is_empty());
assert_eq!(
wpt_ids(&fx, b),
[&before[..1], &w[..], &before[1..]].concat()
);
}
#[test]
fn test_move_files() {
let mut fx = Fixture::default();
let a = add(&mut fx, file("a", &[], 0));
let b = add(&mut fx, file("b", &[], 0));
let c = add(&mut fx, file("c", &[], 0));
do_move(&mut fx, files(&[c]), MoveTarget::Files { index: 0 }).unwrap();
assert_eq!(fx.order.0, vec![c, a, b]);
do_move(&mut fx, files(&[c, a]), MoveTarget::Files { index: 1 }).unwrap();
assert_eq!(fx.order.0, vec![b, c, a]);
assert_eq!(fx.selection, files(&[c, a]));
}
#[test]
fn test_what_cannot_be_moved() {
let mut fx = Fixture::default();
let a = add(&mut fx, file("a", &[("t", 1)], 1));
let t = track_ids(&fx, a);
let s = segment_ids(&fx, a, 0);
let w = wpt_ids(&fx, a);
let before = fx.files[&a].clone();
let selection = fx.selection.clone();
let invalid = [
// the wrong kind of list
(
files(&[a]),
MoveTarget::Tracks {
file_id: a,
index: 0,
},
),
(
tracks(a, &t),
MoveTarget::Waypoints {
file_id: a,
index: 0,
},
),
(
tracks(a, &t),
MoveTarget::Segments {
file_id: a,
trk_id: t[0],
index: 0,
},
),
(
segments(a, t[0], &s),
MoveTarget::Waypoints {
file_id: a,
index: 0,
},
),
(
waypoints(a, &w),
MoveTarget::Tracks {
file_id: a,
index: 0,
},
),
(waypoints(a, &w), MoveTarget::Files { index: 0 }),
(
Selection::Waypoints { file_id: a },
MoveTarget::Files { index: 0 },
),
(Selection::Empty, MoveTarget::Files { index: 0 }),
// elements that do not exist
(
tracks(a, &[TrackId::default()]),
MoveTarget::Files { index: 0 },
),
(
waypoints(a, &[WaypointId::default()]),
MoveTarget::Waypoints {
file_id: a,
index: 0,
},
),
(files(&[FileId::default()]), MoveTarget::Files { index: 0 }),
// a place that does not exist
(
tracks(a, &t),
MoveTarget::Tracks {
file_id: FileId::default(),
index: 0,
},
),
(
segments(a, t[0], &s),
MoveTarget::Segments {
file_id: a,
trk_id: TrackId::default(),
index: 0,
},
),
(
waypoints(a, &w),
MoveTarget::Waypoints {
file_id: FileId::default(),
index: 0,
},
),
];
for (what, to) in invalid {
assert_eq!(
do_move(&mut fx, what.clone(), to),
Err(CommandError::NothingToDo),
"{what:?} to {to:?}"
);
// nothing changed
assert!(Rc::ptr_eq(&fx.files[&a], &before));
assert_eq!(fx.order.0, vec![a]);
assert_eq!(fx.selection, selection);
}
}
}