This commit is contained in:
vcoppe
2026-10-04 15:26:58 +02:00
parent 9ff9b5475a
commit bf946f179a
10 changed files with 1281 additions and 484 deletions
+3 -1
View File
@@ -1,6 +1,6 @@
use crate::{Apply, CommandError, State};
use crate::{
Clean, Crop, Delete, DeleteAll, Duplicate, Elevation, Extract, Load, Merge, Metadata,
Clean, Crop, Delete, DeleteAll, Duplicate, Elevation, Extract, Load, Merge, Metadata, Move,
MoveWaypoint, New, NewTrack, NewTrackSegment, NewWaypoint, Paste, Reverse, SpliceTrackpoints,
Split, Style, Time,
};
@@ -17,6 +17,7 @@ pub enum Command<'a> {
Duplicate(Duplicate),
Delete(Delete),
Paste(Paste),
Move(Move),
DeleteAll(DeleteAll),
Metadata(Metadata<'a>),
Style(Style<'a>),
@@ -43,6 +44,7 @@ impl Apply for Command<'_> {
Command::Duplicate(c) => c.apply(state),
Command::Delete(c) => c.apply(state),
Command::Paste(c) => c.apply(state),
Command::Move(c) => c.apply(state),
Command::DeleteAll(c) => c.apply(state),
Command::Metadata(c) => c.apply(state),
Command::Style(c) => c.apply(state),
@@ -2,12 +2,16 @@ mod delete;
mod delete_all;
mod duplicate;
mod load;
mod move_elements;
mod new;
mod paste;
mod transfer;
pub use delete::*;
pub use delete_all::*;
pub use duplicate::*;
pub use load::*;
pub use move_elements::*;
pub use new::*;
pub use paste::*;
pub use transfer::*;
@@ -0,0 +1,485 @@
use crate::{
Apply, CommandError, FileId, Place, SegmentsTo, Selection, State, TrackId, TracksTo,
transfer_files, transfer_segments, transfer_tracks, transfer_waypoints,
};
/// 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 Apply for Move {
fn apply(self, state: &mut State) -> Result<(), CommandError> {
use MoveTarget as To;
let next = match (&self.what, self.to) {
(Selection::File { file_ids }, To::Files { index }) => {
transfer_files(state, file_ids, true, Place::Index(index))?
}
(Selection::Track { file_id, trk_ids }, To::Files { index }) => transfer_tracks(
state,
*file_id,
trk_ids,
true,
TracksTo::Files(Place::Index(index)),
)?,
(Selection::Track { file_id, trk_ids }, To::Tracks { file_id: to, index }) => {
transfer_tracks(
state,
*file_id,
trk_ids,
true,
TracksTo::File(to, Place::Index(index)),
)?
}
(
Selection::TrackSegment {
file_id,
trk_id,
trkseg_ids,
},
to,
) => {
let to = match to {
To::Files { index } => SegmentsTo::Files(Place::Index(index)),
To::Tracks { file_id, index } => {
SegmentsTo::Tracks(file_id, Place::Index(index))
}
To::Segments {
file_id,
trk_id,
index,
} => SegmentsTo::Track(file_id, trk_id, Place::Index(index)),
To::Waypoints { .. } => return Err(CommandError::NothingToDo),
};
transfer_segments(state, *file_id, *trk_id, trkseg_ids, true, to)?
}
(Selection::Waypoints { file_id }, To::Waypoints { file_id: to, index }) => {
transfer_waypoints(state, *file_id, None, true, to, Place::Index(index))?
}
(Selection::Waypoint { file_id, wpt_ids }, To::Waypoints { file_id: to, index }) => {
transfer_waypoints(
state,
*file_id,
Some(wpt_ids),
true,
to,
Place::Index(index),
)?
}
_ => return Err(CommandError::NothingToDo),
};
*state.selection = next;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::rc::Rc;
use crate::{
File, Track, TrackInfo, TrackSegment, TrackSegmentId, Waypoint, WaypointChunk, WaypointId,
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 = vec![Rc::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);
}
}
}
+82 -384
View File
@@ -1,9 +1,8 @@
use std::{collections::HashSet, hash::Hash, rc::Rc};
use std::{collections::HashSet, hash::Hash};
use crate::{
Apply, CommandError, File, FileId, Selection, State, Track, TrackId, TrackSegment,
TrackSegmentId, Waypoint, WaypointId, copy_file, copy_segment, copy_track, copy_waypoint,
edit_waypoint_chunks, insert_waypoints,
Apply, CommandError, Place, SegmentsTo, Selection, State, TracksTo, transfer_files,
transfer_segments, transfer_tracks, transfer_waypoints, waypoint_ids,
};
/// Pastes what was copied or cut (see [`crate::Clipboard`]), and empties the clipboard.
@@ -31,16 +30,83 @@ impl Apply for Paste {
.ok_or(CommandError::NothingToDo)?;
let cut = clipboard.cut;
let next = match &clipboard.selection {
Selection::File { file_ids } => paste_files(state, file_ids, cut)?,
Selection::Track { file_id, trk_ids } => paste_tracks(state, *file_id, trk_ids, cut)?,
Selection::File { file_ids } => transfer_files(state, file_ids, cut, Place::End)?,
Selection::Track { file_id, trk_ids } => {
let to = match &*state.selection {
Selection::Empty => TracksTo::Files(Place::End),
Selection::File { file_ids } => TracksTo::File(
last_selected(state.order.0.iter().copied(), file_ids)
.ok_or(CommandError::NothingToDo)?,
Place::End,
),
Selection::Track { file_id, trk_ids } => {
let file = state.files.get(file_id).ok_or(CommandError::NothingToDo)?;
let last = last_selected(file.trk.iter().map(|trk| trk.id), trk_ids)
.ok_or(CommandError::NothingToDo)?;
TracksTo::File(*file_id, Place::After(last))
}
_ => return Err(CommandError::NothingToDo),
};
transfer_tracks(state, *file_id, trk_ids, cut, to)?
}
Selection::TrackSegment {
file_id,
trk_id,
trkseg_ids,
} => paste_segments(state, *file_id, *trk_id, trkseg_ids, cut)?,
Selection::Waypoints { file_id } => paste_waypoints(state, *file_id, None, cut)?,
Selection::Waypoint { file_id, wpt_ids } => {
paste_waypoints(state, *file_id, Some(wpt_ids), cut)?
} => {
let to = match &*state.selection {
Selection::Empty => SegmentsTo::Files(Place::End),
Selection::File { file_ids } => SegmentsTo::Tracks(
last_selected(state.order.0.iter().copied(), file_ids)
.ok_or(CommandError::NothingToDo)?,
Place::End,
),
Selection::Track { file_id, trk_ids } => {
let file = state.files.get(file_id).ok_or(CommandError::NothingToDo)?;
let last = last_selected(file.trk.iter().map(|trk| trk.id), trk_ids)
.ok_or(CommandError::NothingToDo)?;
SegmentsTo::Track(*file_id, last, Place::End)
}
Selection::TrackSegment {
file_id,
trk_id,
trkseg_ids,
} => {
let trk = state
.files
.get(file_id)
.and_then(|file| file.trk.iter().find(|trk| trk.id == *trk_id))
.ok_or(CommandError::NothingToDo)?;
let last = last_selected(trk.trkseg.iter().map(|seg| seg.id), trkseg_ids)
.ok_or(CommandError::NothingToDo)?;
SegmentsTo::Track(*file_id, *trk_id, Place::After(last))
}
_ => return Err(CommandError::NothingToDo),
};
transfer_segments(state, *file_id, *trk_id, trkseg_ids, cut, to)?
}
Selection::Waypoints { .. } | Selection::Waypoint { .. } => {
let (file_id, source) = match &clipboard.selection {
Selection::Waypoints { file_id } => (*file_id, None),
Selection::Waypoint { file_id, wpt_ids } => (*file_id, Some(wpt_ids)),
_ => unreachable!(),
};
let (to, place) = match &*state.selection {
Selection::File { file_ids } => (
last_selected(state.order.0.iter().copied(), file_ids)
.ok_or(CommandError::NothingToDo)?,
Place::End,
),
Selection::Waypoints { file_id } => (*file_id, Place::End),
Selection::Waypoint { file_id, wpt_ids } => {
let file = state.files.get(file_id).ok_or(CommandError::NothingToDo)?;
let last = last_selected(waypoint_ids(file), wpt_ids)
.ok_or(CommandError::NothingToDo)?;
(*file_id, Place::After(last))
}
_ => return Err(CommandError::NothingToDo),
};
transfer_waypoints(state, file_id, source, cut, to, place)?
}
Selection::Empty => return Err(CommandError::NothingToDo),
};
@@ -58,382 +124,14 @@ fn last_selected<T: Copy + Eq + Hash>(
all.filter(|id| selected.contains(id)).last()
}
fn waypoint_ids(file: &File) -> impl Iterator<Item = WaypointId> + '_ {
file.wpt
.iter()
.flat_map(|chunk| &chunk.wpt)
.map(|wpt| wpt.id)
}
fn file_mut<'a>(state: &'a mut State, id: FileId) -> Result<&'a mut File, CommandError> {
state
.files
.get_mut(&id)
.map(Rc::make_mut)
.ok_or(CommandError::NothingToDo)
}
/// Adds the file to the files, at the end of the order.
fn add_file(state: &mut State, file: File) -> FileId {
let id = file.id;
state.files.insert(id, Rc::new(file));
state.order.0.push(id);
id
}
fn new_file(name: &str, trk: Vec<Track>) -> File {
let mut file = File {
trk,
..Default::default()
};
file.info.name = name.to_owned();
file
}
fn paste_files(
state: &mut State,
source: &HashSet<FileId>,
cut: bool,
) -> Result<Selection, CommandError> {
let ids: Vec<FileId> = state
.order
.0
.iter()
.copied()
.filter(|id| source.contains(id) && state.files.contains_key(id))
.collect();
if ids.is_empty() {
return Err(CommandError::NothingToDo);
}
let pasted = if cut {
// moved to the end of the list
state.order.move_files(&ids, usize::MAX);
ids
} else {
let copies: Vec<File> = ids.iter().map(|id| copy_file(&state.files[id])).collect();
copies
.into_iter()
.map(|file| add_file(state, file))
.collect()
};
Ok(Selection::File {
file_ids: pasted.into_iter().collect(),
})
}
fn paste_tracks(
state: &mut State,
file_id: FileId,
source: &HashSet<TrackId>,
cut: bool,
) -> Result<Selection, CommandError> {
let source_file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
let source_name = source_file.info.name.clone();
let tracks: Vec<Track> = source_file
.trk
.iter()
.filter(|trk| source.contains(&trk.id))
.cloned()
.collect();
if tracks.is_empty() {
return Err(CommandError::NothingToDo);
}
// where to paste, checked before anything is changed
enum To {
Files,
End(FileId),
After(FileId, TrackId),
}
let to = match &*state.selection {
Selection::Empty => To::Files,
Selection::File { file_ids } => To::End(
last_selected(state.order.0.iter().copied(), file_ids)
.ok_or(CommandError::NothingToDo)?,
),
Selection::Track { file_id, trk_ids } => {
let file = state.files.get(file_id).ok_or(CommandError::NothingToDo)?;
To::After(
*file_id,
last_selected(file.trk.iter().map(|trk| trk.id), trk_ids)
.ok_or(CommandError::NothingToDo)?,
)
}
_ => return Err(CommandError::NothingToDo),
};
if let To::End(id) | To::After(id, _) = &to
&& !state.files.contains_key(id)
{
return Err(CommandError::NothingToDo);
}
if cut {
file_mut(state, file_id)?
.trk
.retain(|trk| !source.contains(&trk.id));
}
let tracks: Vec<Track> = if cut {
tracks
} else {
tracks.iter().map(copy_track).collect()
};
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.id).collect();
Ok(match to {
To::Files => {
let files = tracks
.into_iter()
.map(|track| {
let name = track
.info
.name
.clone()
.unwrap_or_else(|| source_name.clone());
new_file(&name, vec![track])
})
.collect::<Vec<_>>();
Selection::File {
file_ids: files
.into_iter()
.map(|file| add_file(state, file))
.collect(),
}
}
To::End(id) => {
file_mut(state, id)?.trk.extend(tracks);
Selection::Track {
file_id: id,
trk_ids: ids,
}
}
To::After(id, after) => {
let file = file_mut(state, id)?;
// the track may have just been cut away
let at = file
.trk
.iter()
.position(|trk| trk.id == after)
.map_or(file.trk.len(), |i| i + 1);
file.trk.splice(at..at, tracks);
Selection::Track {
file_id: id,
trk_ids: ids,
}
}
})
}
fn paste_segments(
state: &mut State,
file_id: FileId,
trk_id: TrackId,
source: &HashSet<TrackSegmentId>,
cut: bool,
) -> Result<Selection, CommandError> {
let source_file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
let source_track = source_file
.trk
.iter()
.find(|trk| trk.id == trk_id)
.ok_or(CommandError::NothingToDo)?;
let source_name = source_track
.info
.name
.clone()
.unwrap_or_else(|| source_file.info.name.clone());
let segments: Vec<TrackSegment> = source_track
.trkseg
.iter()
.filter(|seg| source.contains(&seg.id))
.cloned()
.collect();
if segments.is_empty() {
return Err(CommandError::NothingToDo);
}
enum To {
Files,
Tracks(FileId),
End(FileId, TrackId),
After(FileId, TrackId, TrackSegmentId),
}
let to = match &*state.selection {
Selection::Empty => To::Files,
Selection::File { file_ids } => To::Tracks(
last_selected(state.order.0.iter().copied(), file_ids)
.ok_or(CommandError::NothingToDo)?,
),
Selection::Track { file_id, trk_ids } => {
let file = state.files.get(file_id).ok_or(CommandError::NothingToDo)?;
To::End(
*file_id,
last_selected(file.trk.iter().map(|trk| trk.id), trk_ids)
.ok_or(CommandError::NothingToDo)?,
)
}
Selection::TrackSegment {
file_id,
trk_id,
trkseg_ids,
} => {
let trk = state
.files
.get(file_id)
.and_then(|file| file.trk.iter().find(|trk| trk.id == *trk_id))
.ok_or(CommandError::NothingToDo)?;
To::After(
*file_id,
*trk_id,
last_selected(trk.trkseg.iter().map(|seg| seg.id), trkseg_ids)
.ok_or(CommandError::NothingToDo)?,
)
}
_ => return Err(CommandError::NothingToDo),
};
if let To::Tracks(id) = &to
&& !state.files.contains_key(id)
{
return Err(CommandError::NothingToDo);
}
if cut {
let file = file_mut(state, file_id)?;
if let Some(trk) = file.trk.iter_mut().find(|trk| trk.id == trk_id) {
trk.trkseg.retain(|seg| !source.contains(&seg.id));
}
}
let segments: Vec<TrackSegment> = if cut {
segments
} else {
segments.iter().map(copy_segment).collect()
};
let ids: HashSet<TrackSegmentId> = segments.iter().map(|seg| seg.id).collect();
// a segment on its own needs a track
let own_track = |segment: TrackSegment| Track {
trkseg: vec![segment],
..Default::default()
};
let in_track = |state: &mut State,
file_id: FileId,
trk_id: TrackId,
after: Option<TrackSegmentId>,
segments: Vec<TrackSegment>|
-> Result<(), CommandError> {
let trk = file_mut(state, file_id)?
.trk
.iter_mut()
.find(|trk| trk.id == trk_id)
.ok_or(CommandError::NothingToDo)?;
// the segment may have just been cut away
let at = after
.and_then(|after| trk.trkseg.iter().position(|seg| seg.id == after))
.map_or(trk.trkseg.len(), |i| i + 1);
trk.trkseg.splice(at..at, segments);
Ok(())
};
Ok(match to {
To::Files => {
let files = segments
.into_iter()
.map(|segment| new_file(&source_name, vec![own_track(segment)]))
.collect::<Vec<_>>();
Selection::File {
file_ids: files
.into_iter()
.map(|file| add_file(state, file))
.collect(),
}
}
To::Tracks(id) => {
let tracks: Vec<Track> = segments.into_iter().map(own_track).collect();
let trk_ids = tracks.iter().map(|trk| trk.id).collect();
file_mut(state, id)?.trk.extend(tracks);
Selection::Track {
file_id: id,
trk_ids,
}
}
To::End(file_id, trk_id) => {
in_track(state, file_id, trk_id, None, segments)?;
Selection::TrackSegment {
file_id,
trk_id,
trkseg_ids: ids,
}
}
To::After(file_id, trk_id, after) => {
in_track(state, file_id, trk_id, Some(after), segments)?;
Selection::TrackSegment {
file_id,
trk_id,
trkseg_ids: ids,
}
}
})
}
fn paste_waypoints(
state: &mut State,
file_id: FileId,
source: Option<&HashSet<WaypointId>>,
cut: bool,
) -> Result<Selection, CommandError> {
// all the waypoints of the file when no waypoints are given
let selected = |wpt: &Waypoint| source.is_none_or(|ids| ids.contains(&wpt.id));
let source_file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
let waypoints: Vec<Waypoint> = source_file
.wpt
.iter()
.flat_map(|chunk| &chunk.wpt)
.filter(|wpt| selected(wpt))
.cloned()
.collect();
if waypoints.is_empty() {
return Err(CommandError::NothingToDo);
}
let (to_file, after) = match &*state.selection {
Selection::File { file_ids } => (
last_selected(state.order.0.iter().copied(), file_ids)
.ok_or(CommandError::NothingToDo)?,
None,
),
Selection::Waypoints { file_id } => (*file_id, None),
Selection::Waypoint { file_id, wpt_ids } => {
let file = state.files.get(file_id).ok_or(CommandError::NothingToDo)?;
(*file_id, last_selected(waypoint_ids(file), wpt_ids))
}
_ => return Err(CommandError::NothingToDo),
};
if !state.files.contains_key(&to_file) {
return Err(CommandError::NothingToDo);
}
if cut {
edit_waypoint_chunks(file_mut(state, file_id)?, selected, |wpts| {
wpts.retain(|wpt| !selected(wpt));
true
});
}
let waypoints: Vec<Waypoint> = if cut {
waypoints
} else {
waypoints.iter().map(copy_waypoint).collect()
};
let ids = waypoints.iter().map(|wpt| wpt.id).collect();
insert_waypoints(file_mut(state, to_file)?, after, waypoints);
Ok(Selection::Waypoint {
file_id: to_file,
wpt_ids: ids,
})
}
#[cfg(test)]
mod tests {
use crate::{Clipboard, TrackInfo, engine::command::fixture::Fixture};
use std::rc::Rc;
use crate::{
Clipboard, File, FileId, Track, TrackId, TrackInfo, TrackSegment, TrackSegmentId, Waypoint,
WaypointId, engine::command::fixture::Fixture, new_file,
};
use super::*;
@@ -0,0 +1,358 @@
//! Copying or moving elements of the files to a place of the file tree: what paste and move are
//! made of. The elements come from where they are found by id, and the place says where they
//! go among the elements already there.
use std::{collections::HashSet, rc::Rc};
use crate::{
CommandError, File, FileId, Selection, State, Track, TrackId, TrackSegment, TrackSegmentId,
Waypoint, WaypointId, copy_file, copy_segment, copy_track, copy_waypoint, edit_waypoint_chunks,
insert_waypoints_at,
};
/// Where elements are put, among the others of the same list.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Place<T> {
/// After all of them.
End,
/// Right after the one with this id, or at the end if it is not there (anymore).
After(T),
/// At this position (at the end if it is past it).
Index(usize),
}
impl<T: Copy + PartialEq> Place<T> {
/// The position in the list of `ids`.
pub fn position(&self, ids: &[T]) -> usize {
match self {
Place::End => ids.len(),
Place::After(id) => ids
.iter()
.position(|other| other == id)
.map_or(ids.len(), |i| i + 1),
Place::Index(index) => (*index).min(ids.len()),
}
}
}
pub fn waypoint_ids(file: &File) -> impl Iterator<Item = WaypointId> + '_ {
file.wpt
.iter()
.flat_map(|chunk| &chunk.wpt)
.map(|wpt| wpt.id)
}
fn file_mut<'a>(state: &'a mut State, id: FileId) -> Result<&'a mut File, CommandError> {
state
.files
.get_mut(&id)
.map(Rc::make_mut)
.ok_or(CommandError::NothingToDo)
}
pub fn new_file(name: &str, trk: Vec<Track>) -> File {
let mut file = File {
trk,
..Default::default()
};
file.info.name = name.to_owned();
file
}
/// Adds the files to the state, at `place` in the order of the files.
fn add_files(state: &mut State, files: Vec<File>, place: Place<FileId>) -> HashSet<FileId> {
let at = place.position(&state.order.0);
let ids: Vec<FileId> = files.iter().map(|file| file.id).collect();
for file in files {
state.files.insert(file.id, Rc::new(file));
}
state.order.0.splice(at..at, ids.iter().copied());
ids.into_iter().collect()
}
/// Copies the files, or moves them in the order of the files when `cut`.
pub fn transfer_files(
state: &mut State,
source: &HashSet<FileId>,
cut: bool,
place: Place<FileId>,
) -> Result<Selection, CommandError> {
let ids: Vec<FileId> = state
.order
.0
.iter()
.copied()
.filter(|id| source.contains(id) && state.files.contains_key(id))
.collect();
if ids.is_empty() {
return Err(CommandError::NothingToDo);
}
let file_ids = if cut {
// the position is among the files that stay
let others: Vec<FileId> = state
.order
.0
.iter()
.copied()
.filter(|id| !ids.contains(id))
.collect();
state.order.move_files(&ids, place.position(&others));
ids.into_iter().collect()
} else {
let copies = ids.iter().map(|id| copy_file(&state.files[id])).collect();
add_files(state, copies, place)
};
Ok(Selection::File { file_ids })
}
/// Where tracks go.
#[derive(Debug, Clone, Copy)]
pub enum TracksTo {
/// Each track becomes a new file, named like the track, or like the file it comes from.
Files(Place<FileId>),
/// The tracks go in a file.
File(FileId, Place<TrackId>),
}
/// Copies the tracks, or moves them (with their ids) when `cut`.
pub fn transfer_tracks(
state: &mut State,
file_id: FileId,
source: &HashSet<TrackId>,
cut: bool,
to: TracksTo,
) -> Result<Selection, CommandError> {
let source_file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
let source_name = source_file.info.name.clone();
let tracks: Vec<Track> = source_file
.trk
.iter()
.filter(|trk| source.contains(&trk.id))
.cloned()
.collect();
if tracks.is_empty() {
return Err(CommandError::NothingToDo);
}
if let TracksTo::File(id, _) = to
&& !state.files.contains_key(&id)
{
return Err(CommandError::NothingToDo);
}
if cut {
file_mut(state, file_id)?
.trk
.retain(|trk| !source.contains(&trk.id));
}
let tracks: Vec<Track> = if cut {
tracks
} else {
tracks.iter().map(copy_track).collect()
};
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.id).collect();
Ok(match to {
TracksTo::Files(place) => {
let files = tracks
.into_iter()
.map(|track| {
let name = track
.info
.name
.clone()
.unwrap_or_else(|| source_name.clone());
new_file(&name, vec![track])
})
.collect();
Selection::File {
file_ids: add_files(state, files, place),
}
}
TracksTo::File(id, place) => {
let file = file_mut(state, id)?;
let existing: Vec<TrackId> = file.trk.iter().map(|trk| trk.id).collect();
let at = place.position(&existing);
file.trk.splice(at..at, tracks);
Selection::Track {
file_id: id,
trk_ids: ids,
}
}
})
}
/// Where segments go.
#[derive(Debug, Clone, Copy)]
pub enum SegmentsTo {
/// Each segment becomes a new file with a track, named like the track it comes from.
Files(Place<FileId>),
/// Each segment becomes a new track of the file.
Tracks(FileId, Place<TrackId>),
/// The segments go in a track of a file.
Track(FileId, TrackId, Place<TrackSegmentId>),
}
/// Copies the segments of a track, or moves them (with their ids) when `cut`.
pub fn transfer_segments(
state: &mut State,
file_id: FileId,
trk_id: TrackId,
source: &HashSet<TrackSegmentId>,
cut: bool,
to: SegmentsTo,
) -> Result<Selection, CommandError> {
let source_file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
let source_track = source_file
.trk
.iter()
.find(|trk| trk.id == trk_id)
.ok_or(CommandError::NothingToDo)?;
let source_name = source_track
.info
.name
.clone()
.unwrap_or_else(|| source_file.info.name.clone());
let segments: Vec<TrackSegment> = source_track
.trkseg
.iter()
.filter(|seg| source.contains(&seg.id))
.cloned()
.collect();
if segments.is_empty() {
return Err(CommandError::NothingToDo);
}
let destination_exists = match to {
SegmentsTo::Files(_) => true,
SegmentsTo::Tracks(id, _) => state.files.contains_key(&id),
SegmentsTo::Track(id, trk, _) => state
.files
.get(&id)
.is_some_and(|file| file.trk.iter().any(|t| t.id == trk)),
};
if !destination_exists {
return Err(CommandError::NothingToDo);
}
if cut {
let file = file_mut(state, file_id)?;
if let Some(trk) = file.trk.iter_mut().find(|trk| trk.id == trk_id) {
trk.trkseg.retain(|seg| !source.contains(&seg.id));
}
}
let segments: Vec<TrackSegment> = if cut {
segments
} else {
segments.iter().map(copy_segment).collect()
};
let ids: HashSet<TrackSegmentId> = segments.iter().map(|seg| seg.id).collect();
// a segment on its own needs a track
let own_track = |segment: TrackSegment| Track {
trkseg: vec![segment],
..Default::default()
};
Ok(match to {
SegmentsTo::Files(place) => {
let files = segments
.into_iter()
.map(|segment| new_file(&source_name, vec![own_track(segment)]))
.collect();
Selection::File {
file_ids: add_files(state, files, place),
}
}
SegmentsTo::Tracks(id, place) => {
let tracks: Vec<Track> = segments.into_iter().map(own_track).collect();
let trk_ids = tracks.iter().map(|trk| trk.id).collect();
let file = file_mut(state, id)?;
let existing: Vec<TrackId> = file.trk.iter().map(|trk| trk.id).collect();
let at = place.position(&existing);
file.trk.splice(at..at, tracks);
Selection::Track {
file_id: id,
trk_ids,
}
}
SegmentsTo::Track(id, trk_id, place) => {
let trk = file_mut(state, id)?
.trk
.iter_mut()
.find(|trk| trk.id == trk_id)
.ok_or(CommandError::NothingToDo)?;
let existing: Vec<TrackSegmentId> = trk.trkseg.iter().map(|seg| seg.id).collect();
let at = place.position(&existing);
trk.trkseg.splice(at..at, segments);
Selection::TrackSegment {
file_id: id,
trk_id,
trkseg_ids: ids,
}
}
})
}
/// Copies waypoints of a file (all of them when `source` is `None`) to `place` among the
/// waypoints of the file `to`, or moves them (with their ids) when `cut`.
pub fn transfer_waypoints(
state: &mut State,
file_id: FileId,
source: Option<&HashSet<WaypointId>>,
cut: bool,
to: FileId,
place: Place<WaypointId>,
) -> Result<Selection, CommandError> {
let selected = |wpt: &Waypoint| source.is_none_or(|ids| ids.contains(&wpt.id));
let source_file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
let waypoints: Vec<Waypoint> = source_file
.wpt
.iter()
.flat_map(|chunk| &chunk.wpt)
.filter(|wpt| selected(wpt))
.cloned()
.collect();
if waypoints.is_empty() || !state.files.contains_key(&to) {
return Err(CommandError::NothingToDo);
}
if cut {
edit_waypoint_chunks(file_mut(state, file_id)?, selected, |wpts| {
wpts.retain(|wpt| !selected(wpt));
true
});
}
let waypoints: Vec<Waypoint> = if cut {
waypoints
} else {
waypoints.iter().map(copy_waypoint).collect()
};
let ids = waypoints.iter().map(|wpt| wpt.id).collect();
let file = file_mut(state, to)?;
let existing: Vec<WaypointId> = waypoint_ids(file).collect();
insert_waypoints_at(file, place.position(&existing), waypoints);
Ok(Selection::Waypoint {
file_id: to,
wpt_ids: ids,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_place_position() {
let ids = [1, 2, 3];
assert_eq!(Place::End.position(&ids), 3);
assert_eq!(Place::After(1).position(&ids), 1);
assert_eq!(Place::After(3).position(&ids), 3);
// not there: at the end
assert_eq!(Place::After(9).position(&ids), 3);
assert_eq!(Place::Index(0).position(&ids), 0);
assert_eq!(Place::Index(2).position(&ids), 2);
assert_eq!(Place::Index(10).position(&ids), 3);
assert_eq!(Place::<i32>::End.position(&[]), 0);
assert_eq!(Place::<i32>::Index(4).position(&[]), 0);
}
}
@@ -5,9 +5,25 @@ use crate::{File, Waypoint, WaypointChunk, WaypointId};
/// Inserts waypoints in the file, right after the waypoint `after`, or at the end if there is
/// none (or if it is not in the file).
///
/// Only the chunk holding `after` is cut (when waypoints follow it), all the other chunks are
/// kept as they are.
/// See [`insert_waypoints_at`].
pub fn insert_waypoints(file: &mut File, after: Option<WaypointId>, waypoints: Vec<Waypoint>) {
let index = after
.and_then(|after| {
file.wpt
.iter()
.flat_map(|chunk| &chunk.wpt)
.position(|wpt| wpt.id == after)
})
.map_or(usize::MAX, |i| i + 1);
insert_waypoints_at(file, index, waypoints);
}
/// Inserts waypoints in the file, so that the first one is at `index` among the waypoints of
/// the file (at the end if the index is past it).
///
/// Only the chunk holding the waypoint that was at `index` is cut (when the waypoints are not
/// inserted between two chunks), all the other chunks are kept as they are.
pub fn insert_waypoints_at(file: &mut File, index: usize, waypoints: Vec<Waypoint>) {
if waypoints.is_empty() {
return;
}
@@ -24,36 +40,32 @@ pub fn insert_waypoints(file: &mut File, after: Option<WaypointId>, waypoints: V
inserted.push(Rc::new(chunk));
}
let position = after.and_then(|after| {
file.wpt.iter().enumerate().find_map(|(i, chunk)| {
chunk
.wpt
.iter()
.position(|wpt| wpt.id == after)
.map(|j| (i, j + 1))
})
// the chunk holding the waypoint at `index`, and the position of that waypoint in it
let mut start = 0;
let position = file.wpt.iter().enumerate().find_map(|(i, chunk)| {
let offset = index
.checked_sub(start)
.filter(|offset| *offset < chunk.wpt.len());
start += chunk.wpt.len();
offset.map(|offset| (i, offset))
});
match position {
None => file.wpt.extend(inserted),
Some((i, split)) => {
Some((i, 0)) => {
file.wpt.splice(i..i, inserted);
}
Some((i, offset)) => {
let cut = &file.wpt[i];
let replacement = if split == cut.wpt.len() {
let mut chunks = vec![cut.clone()];
chunks.extend(inserted);
chunks
} else {
let part = |wpt: &[Waypoint]| {
Rc::new(WaypointChunk {
wpt: wpt.to_vec(),
..Default::default()
})
};
let mut chunks = vec![part(&cut.wpt[..split])];
chunks.extend(inserted);
chunks.push(part(&cut.wpt[split..]));
chunks
let part = |wpt: &[Waypoint]| {
Rc::new(WaypointChunk {
wpt: wpt.to_vec(),
..Default::default()
})
};
file.wpt.splice(i..=i, replacement);
let mut chunks = vec![part(&cut.wpt[..offset])];
chunks.extend(inserted);
chunks.push(part(&cut.wpt[offset..]));
file.wpt.splice(i..=i, chunks);
}
}
file.wpt_rev_id = Default::default();
@@ -155,4 +167,52 @@ mod tests {
assert_eq!(file.wpt.len(), 3);
assert!(file.wpt[..2].iter().all(|chunk| chunk.is_full()));
}
#[test]
fn test_insert_at_an_index() {
// at the start
let (mut file, before) = file_with(&[2, 1]);
let first = file.wpt[0].clone();
let one = waypoint();
let one_id = one.id;
insert_waypoints_at(&mut file, 0, vec![one]);
assert_eq!(ids(&file), [vec![one_id], before.clone()].concat());
// the existing chunks are kept
assert_eq!(file.wpt.len(), 3);
assert!(Rc::ptr_eq(&file.wpt[1], &first));
// between two chunks
let (mut file, before) = file_with(&[2, 1]);
let (first, last) = (file.wpt[0].clone(), file.wpt[1].clone());
let one = waypoint();
let one_id = one.id;
insert_waypoints_at(&mut file, 2, vec![one]);
assert_eq!(ids(&file), [&before[..2], &[one_id], &before[2..]].concat());
assert_eq!(file.wpt.len(), 3);
assert!(Rc::ptr_eq(&file.wpt[0], &first) && Rc::ptr_eq(&file.wpt[2], &last));
// inside a chunk
let (mut file, before) = file_with(&[3]);
let one = waypoint();
let one_id = one.id;
insert_waypoints_at(&mut file, 1, vec![one]);
assert_eq!(ids(&file), [&before[..1], &[one_id], &before[1..]].concat());
assert_eq!(file.wpt.len(), 3);
// at the end, or past it
for index in [3, 100] {
let (mut file, before) = file_with(&[3]);
let one = waypoint();
let one_id = one.id;
insert_waypoints_at(&mut file, index, vec![one]);
assert_eq!(ids(&file), [before, vec![one_id]].concat());
}
// in a file without waypoints
let mut file = File::default();
let one = waypoint();
let one_id = one.id;
insert_waypoints_at(&mut file, 0, vec![one]);
assert_eq!(ids(&file), vec![one_id]);
}
}
+40
View File
@@ -1021,4 +1021,44 @@ mod tests {
assert!(engine.execute(Action::Undo));
assert!(!engine.can_undo());
}
#[test]
fn test_move_elements_is_an_edit_and_selects_what_moved() {
use crate::{Move, MoveTarget};
let mut engine = Engine::default();
let a = file_with_tracks(&mut engine, "a", 3);
let tracks = track_ids(&engine, a);
let moved = Selection::Track {
file_id: a,
trk_ids: [tracks[2]].into(),
};
assert!(edit(
&mut engine,
Command::Move(Move {
what: moved.clone(),
to: MoveTarget::Tracks {
file_id: a,
index: 0
},
})
));
assert_eq!(track_ids(&engine, a), vec![tracks[2], tracks[0], tracks[1]]);
assert_eq!(engine.selection(), &moved);
// invalid moves change nothing
assert!(!edit(
&mut engine,
Command::Move(Move {
what: moved,
to: MoveTarget::Waypoints {
file_id: a,
index: 0
},
})
));
assert!(engine.execute(Action::Undo));
assert_eq!(track_ids(&engine, a), tracks);
}
}