mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-28 18:47:18 +00:00
progress
This commit is contained in:
1 parent
41b9116c6a
commit
97d597e139
181 files changed
+1970
-2643
No files matched your search
@@ -0,0 +1,469 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
File, FileId, Files, Selection, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint,
|
||||
WaypointId,
|
||||
};
|
||||
|
||||
/// A track in the clipboard, with the name of the file it comes from (the name of the new file
|
||||
/// when it is pasted on its own).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClipboardTrack {
|
||||
pub track: Track,
|
||||
pub file_name: String,
|
||||
}
|
||||
|
||||
/// A segment in the clipboard, with the name of its track, or of its file when the track has
|
||||
/// none (the name of the new file when it is pasted on its own).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClipboardSegment {
|
||||
pub segment: TrackSegment,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// The elements that were copied or cut, in the order of the files.
|
||||
///
|
||||
/// It holds the elements themselves, not references to them: they are cheap to keep (files are
|
||||
/// shared, and so are the trackpoints of tracks and segments), and the clipboard stays valid
|
||||
/// when the elements are changed or deleted.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ClipboardContent {
|
||||
Files(Vec<Rc<File>>),
|
||||
Tracks(Vec<ClipboardTrack>),
|
||||
Segments(Vec<ClipboardSegment>),
|
||||
Waypoints(Vec<Waypoint>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct ClipboardId(Uuid);
|
||||
|
||||
impl Default for ClipboardId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
/// What was copied or cut, to be pasted, as many times as needed.
|
||||
///
|
||||
/// What is pasted is always the elements as they were when they were copied or cut, whatever
|
||||
/// happened to them since, even if they were deleted. Pasting what was copied makes copies, with
|
||||
/// new ids. What was cut is moved, with its ids, the first time it is pasted (the elements that
|
||||
/// are still there are removed): the clipboard is not cut anymore after that, and the next
|
||||
/// pastes make copies.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Clipboard {
|
||||
id: ClipboardId,
|
||||
pub content: ClipboardContent,
|
||||
pub cut: bool,
|
||||
}
|
||||
|
||||
/// Two clipboards are the same when they come from the same copy, and are cut the same way.
|
||||
impl PartialEq for Clipboard {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id && self.cut == other.cut
|
||||
}
|
||||
}
|
||||
|
||||
impl Clipboard {
|
||||
/// The clipboard for the selected elements, `None` if nothing is selected (or if what is
|
||||
/// selected does not exist).
|
||||
pub fn new(
|
||||
selection: &Selection,
|
||||
files: &Files,
|
||||
order: &[FileId],
|
||||
cut: bool,
|
||||
) -> Option<Clipboard> {
|
||||
let content = match selection {
|
||||
Selection::Empty => return None,
|
||||
Selection::File { file_ids } => ClipboardContent::Files(
|
||||
order
|
||||
.iter()
|
||||
.filter(|id| file_ids.contains(id))
|
||||
.filter_map(|id| files.get(id).cloned())
|
||||
.collect(),
|
||||
),
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = files.get(file_id)?;
|
||||
ClipboardContent::Tracks(
|
||||
file.trk
|
||||
.iter()
|
||||
.filter(|trk| trk_ids.contains(&trk.id))
|
||||
.map(|trk| ClipboardTrack {
|
||||
track: trk.clone(),
|
||||
file_name: file.info.name.clone(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => {
|
||||
let file = files.get(file_id)?;
|
||||
let trk = file.trk.iter().find(|trk| trk.id == *trk_id)?;
|
||||
let name = trk
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| file.info.name.clone());
|
||||
ClipboardContent::Segments(
|
||||
trk.trkseg
|
||||
.iter()
|
||||
.filter(|seg| trkseg_ids.contains(&seg.id))
|
||||
.map(|seg| ClipboardSegment {
|
||||
segment: seg.clone(),
|
||||
name: name.clone(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
Selection::Waypoints { file_id } => {
|
||||
ClipboardContent::Waypoints(files.get(file_id)?.wpt.iter().cloned().collect())
|
||||
}
|
||||
Selection::Waypoint { file_id, wpt_ids } => ClipboardContent::Waypoints(
|
||||
files
|
||||
.get(file_id)?
|
||||
.wpt
|
||||
.iter()
|
||||
.filter(|wpt| wpt_ids.contains(&wpt.id))
|
||||
.cloned()
|
||||
.collect(),
|
||||
),
|
||||
};
|
||||
let clipboard = Clipboard {
|
||||
id: ClipboardId::default(),
|
||||
content,
|
||||
cut,
|
||||
};
|
||||
(!clipboard.is_empty()).then_some(clipboard)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match &self.content {
|
||||
ClipboardContent::Files(files) => files.is_empty(),
|
||||
ClipboardContent::Tracks(tracks) => tracks.is_empty(),
|
||||
ClipboardContent::Segments(segments) => segments.is_empty(),
|
||||
ClipboardContent::Waypoints(waypoints) => waypoints.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the content can be pasted when `target` is selected: files and tracks can be
|
||||
/// pasted anywhere above them in the file tree, and nothing selected stands for the list of
|
||||
/// files.
|
||||
///
|
||||
/// | copied | pasted when selected |
|
||||
/// |-----------|------------------------------------------|
|
||||
/// | files | nothing, a file |
|
||||
/// | tracks | nothing, a file, a track |
|
||||
/// | segments | nothing, a file, a track, a segment |
|
||||
/// | waypoints | a file, the waypoints, a waypoint |
|
||||
pub fn can_paste(&self, target: &Selection) -> bool {
|
||||
use ClipboardContent as C;
|
||||
use Selection::*;
|
||||
matches!(
|
||||
(&self.content, target),
|
||||
(C::Files(_), Empty | File { .. })
|
||||
| (C::Tracks(_), Empty | File { .. } | Track { .. })
|
||||
| (
|
||||
C::Segments(_),
|
||||
Empty | File { .. } | Track { .. } | TrackSegment { .. }
|
||||
)
|
||||
| (
|
||||
C::Waypoints(_),
|
||||
File { .. } | Waypoints { .. } | Waypoint { .. }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The ids of the elements of the content, by kind.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ClipboardIds {
|
||||
Files(Vec<FileId>),
|
||||
Tracks(Vec<TrackId>),
|
||||
Segments(Vec<TrackSegmentId>),
|
||||
Waypoints(Vec<WaypointId>),
|
||||
}
|
||||
|
||||
impl ClipboardContent {
|
||||
pub fn ids(&self) -> ClipboardIds {
|
||||
match self {
|
||||
ClipboardContent::Files(files) => {
|
||||
ClipboardIds::Files(files.iter().map(|file| file.id).collect())
|
||||
}
|
||||
ClipboardContent::Tracks(tracks) => {
|
||||
ClipboardIds::Tracks(tracks.iter().map(|trk| trk.track.id).collect())
|
||||
}
|
||||
ClipboardContent::Segments(segments) => {
|
||||
ClipboardIds::Segments(segments.iter().map(|seg| seg.segment.id).collect())
|
||||
}
|
||||
ClipboardContent::Waypoints(waypoints) => {
|
||||
ClipboardIds::Waypoints(waypoints.iter().map(|wpt| wpt.id).collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{TrackInfo, WaypointChunk};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A file with 3 tracks (named "t0", "t1", "t2") of 2 segments and 3 waypoints.
|
||||
fn file(name: &str) -> File {
|
||||
let mut file = File::default();
|
||||
file.info.name = name.to_owned();
|
||||
for i in 0..3 {
|
||||
file.trk.push(Track {
|
||||
info: TrackInfo {
|
||||
name: (i < 2).then(|| format!("t{i}")),
|
||||
..Default::default()
|
||||
},
|
||||
trkseg: vec![TrackSegment::default(), TrackSegment::default()],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
file.wpt.push(WaypointChunk {
|
||||
wpt: vec![Waypoint::default(), Waypoint::default()],
|
||||
..Default::default()
|
||||
});
|
||||
file.wpt.push(WaypointChunk {
|
||||
wpt: vec![Waypoint::default()],
|
||||
..Default::default()
|
||||
});
|
||||
file
|
||||
}
|
||||
|
||||
fn files_of(files: Vec<File>) -> Files {
|
||||
files
|
||||
.into_iter()
|
||||
.map(|file| (file.id, Rc::new(file)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn order_of(files: &Files) -> Vec<FileId> {
|
||||
files.keys().copied().collect()
|
||||
}
|
||||
|
||||
fn tracks(file: &File, ids: &[usize]) -> Selection {
|
||||
Selection::Track {
|
||||
file_id: file.id,
|
||||
trk_ids: ids.iter().map(|i| file.trk[*i].id).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_clipboard_holds_what_is_selected() {
|
||||
let file = file("source");
|
||||
let (t0, t2) = (file.trk[0].id, file.trk[2].id);
|
||||
let (s0, s1) = (file.trk[1].trkseg[0].id, file.trk[1].trkseg[1].id);
|
||||
let wpt: Vec<_> = file.wpt.iter().map(|w| w.id).collect();
|
||||
let files = files_of(vec![file.clone()]);
|
||||
|
||||
// tracks, in the order of the file, with the name of their file
|
||||
let clipboard =
|
||||
Clipboard::new(&tracks(&file, &[2, 0]), &files, &order_of(&files), false).unwrap();
|
||||
assert!(!clipboard.cut);
|
||||
match &clipboard.content {
|
||||
ClipboardContent::Tracks(copied) => {
|
||||
assert_eq!(
|
||||
copied.iter().map(|c| c.track.id).collect::<Vec<_>>(),
|
||||
vec![t0, t2]
|
||||
);
|
||||
assert!(copied.iter().all(|c| c.file_name == "source"));
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
|
||||
// segments, with the name of their track, or else of their file
|
||||
let segments = |trk: usize, ids: &[TrackSegmentId]| Selection::TrackSegment {
|
||||
file_id: file.id,
|
||||
trk_id: file.trk[trk].id,
|
||||
trkseg_ids: ids.iter().copied().collect(),
|
||||
};
|
||||
match Clipboard::new(&segments(1, &[s1, s0]), &files, &order_of(&files), true)
|
||||
.unwrap()
|
||||
.content
|
||||
{
|
||||
ClipboardContent::Segments(copied) => {
|
||||
assert_eq!(
|
||||
copied.iter().map(|c| c.segment.id).collect::<Vec<_>>(),
|
||||
vec![s0, s1]
|
||||
);
|
||||
assert!(copied.iter().all(|c| c.name == "t1"));
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
let unnamed = file.trk[2].trkseg[0].id;
|
||||
match Clipboard::new(&segments(2, &[unnamed]), &files, &order_of(&files), true)
|
||||
.unwrap()
|
||||
.content
|
||||
{
|
||||
ClipboardContent::Segments(copied) => assert_eq!(copied[0].name, "source"),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
|
||||
// waypoints: some of them, or all of them
|
||||
let some = Selection::Waypoint {
|
||||
file_id: file.id,
|
||||
wpt_ids: [wpt[2], wpt[0]].into(),
|
||||
};
|
||||
assert_eq!(
|
||||
Clipboard::new(&some, &files, &order_of(&files), false)
|
||||
.unwrap()
|
||||
.content
|
||||
.ids(),
|
||||
ClipboardIds::Waypoints(vec![wpt[0], wpt[2]])
|
||||
);
|
||||
assert_eq!(
|
||||
Clipboard::new(
|
||||
&Selection::Waypoints { file_id: file.id },
|
||||
&files,
|
||||
&order_of(&files),
|
||||
false
|
||||
)
|
||||
.unwrap()
|
||||
.content
|
||||
.ids(),
|
||||
ClipboardIds::Waypoints(wpt)
|
||||
);
|
||||
|
||||
// files
|
||||
let selection = Selection::File {
|
||||
file_ids: [file.id].into(),
|
||||
};
|
||||
assert_eq!(
|
||||
Clipboard::new(&selection, &files, &order_of(&files), false)
|
||||
.unwrap()
|
||||
.content
|
||||
.ids(),
|
||||
ClipboardIds::Files(vec![file.id])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nothing_to_copy() {
|
||||
let file = file("f");
|
||||
let files = files_of(vec![file.clone()]);
|
||||
assert!(Clipboard::new(&Selection::Empty, &files, &order_of(&files), false).is_none());
|
||||
// elements that do not exist
|
||||
let unknown = Selection::Track {
|
||||
file_id: FileId::default(),
|
||||
trk_ids: [TrackId::default()].into(),
|
||||
};
|
||||
assert!(Clipboard::new(&unknown, &files, &order_of(&files), false).is_none());
|
||||
let unknown = Selection::Track {
|
||||
file_id: file.id,
|
||||
trk_ids: [TrackId::default()].into(),
|
||||
};
|
||||
assert!(Clipboard::new(&unknown, &files, &order_of(&files), false).is_none());
|
||||
assert!(
|
||||
Clipboard::new(
|
||||
&Selection::File {
|
||||
file_ids: [FileId::default()].into()
|
||||
},
|
||||
&files,
|
||||
&order_of(&files),
|
||||
false
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
// a file without waypoints
|
||||
let empty = files_of(vec![File::default()]);
|
||||
let id = *empty.keys().next().unwrap();
|
||||
assert!(
|
||||
Clipboard::new(
|
||||
&Selection::Waypoints { file_id: id },
|
||||
&empty,
|
||||
&order_of(&empty),
|
||||
true
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_two_clipboards_are_the_same_when_they_come_from_the_same_copy() {
|
||||
let file = file("f");
|
||||
let files = files_of(vec![file.clone()]);
|
||||
let selection = tracks(&file, &[0]);
|
||||
let clipboard = Clipboard::new(&selection, &files, &order_of(&files), false).unwrap();
|
||||
assert_eq!(clipboard, clipboard.clone());
|
||||
// the same elements copied again make another clipboard
|
||||
assert_ne!(
|
||||
clipboard,
|
||||
Clipboard::new(&selection, &files, &order_of(&files), false).unwrap()
|
||||
);
|
||||
// and it is not the same once it is not cut
|
||||
let mut copy = clipboard.clone();
|
||||
copy.cut = true;
|
||||
assert_ne!(clipboard, copy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_clipboard_survives_what_happens_to_the_files() {
|
||||
let file = file("f");
|
||||
let mut files = files_of(vec![file.clone()]);
|
||||
let clipboard =
|
||||
Clipboard::new(&tracks(&file, &[0, 1]), &files, &order_of(&files), true).unwrap();
|
||||
files.clear();
|
||||
assert!(!clipboard.is_empty());
|
||||
assert_eq!(
|
||||
clipboard.content.ids(),
|
||||
ClipboardIds::Tracks(vec![file.trk[0].id, file.trk[1].id])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_where_things_can_be_pasted() {
|
||||
let file = file("f");
|
||||
let files = files_of(vec![file.clone()]);
|
||||
let selection_of = |selection: Selection| {
|
||||
Clipboard::new(&selection, &files, &order_of(&files), false).unwrap()
|
||||
};
|
||||
let file_selection = Selection::File {
|
||||
file_ids: [file.id].into(),
|
||||
};
|
||||
let track = tracks(&file, &[0]);
|
||||
let segment = Selection::TrackSegment {
|
||||
file_id: file.id,
|
||||
trk_id: file.trk[0].id,
|
||||
trkseg_ids: [file.trk[0].trkseg[0].id].into(),
|
||||
};
|
||||
let waypoints = Selection::Waypoints { file_id: file.id };
|
||||
let waypoint = Selection::Waypoint {
|
||||
file_id: file.id,
|
||||
wpt_ids: [file.wpt[0].id].into(),
|
||||
};
|
||||
let targets = [
|
||||
Selection::Empty,
|
||||
file_selection.clone(),
|
||||
track.clone(),
|
||||
segment.clone(),
|
||||
waypoints.clone(),
|
||||
waypoint.clone(),
|
||||
];
|
||||
let expected = [
|
||||
// nothing, file, track, segment, waypoints, waypoint
|
||||
(file_selection, [true, true, false, false, false, false]),
|
||||
(track, [true, true, true, false, false, false]),
|
||||
(segment, [true, true, true, true, false, false]),
|
||||
(waypoints, [false, true, false, false, true, true]),
|
||||
(waypoint, [false, true, false, false, true, true]),
|
||||
];
|
||||
for (copied, allowed) in expected {
|
||||
let clipboard = selection_of(copied.clone());
|
||||
for (target, allowed) in targets.iter().zip(allowed) {
|
||||
assert_eq!(
|
||||
clipboard.can_paste(target),
|
||||
allowed,
|
||||
"{copied:?} on {target:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use crate::{CommandError, File, FileId};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct History {
|
||||
entries: Vec<Files>,
|
||||
index: Option<usize>,
|
||||
/// Whether the first entry is the state the history starts from, which is not undone: files
|
||||
/// restored from the storage.
|
||||
floor: bool,
|
||||
}
|
||||
|
||||
impl History {
|
||||
/// A history whose first entry is `entry`, from which nothing can be undone.
|
||||
pub fn restored(entry: Files) -> Self {
|
||||
Self {
|
||||
entries: vec![entry],
|
||||
index: Some(0),
|
||||
floor: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current(&self) -> Option<&Files> {
|
||||
self.index.map_or_default(|i| self.get(i))
|
||||
}
|
||||
|
||||
/// Applies `f` to a copy of the current entry and makes the copy the new current entry.
|
||||
///
|
||||
/// Nothing is pushed, and `None` is returned, if `f` fails (the error is returned) or if it
|
||||
/// leaves every file as it was: a step that undoes nothing is not worth a place in the
|
||||
/// history. Files are told apart by their identity, not their content.
|
||||
pub fn create_and_push_next<F>(&mut self, f: F) -> Result<Option<Diff>, CommandError>
|
||||
where
|
||||
F: FnOnce(&mut Files) -> Result<(), CommandError>,
|
||||
{
|
||||
let mut next = self.current().map_or_default(|c| c.clone());
|
||||
f(&mut next)?;
|
||||
if self.is_current(&next) {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(self.record_diff(|history| history.push(next)))
|
||||
}
|
||||
|
||||
/// Whether `entry` holds exactly the files of the current entry.
|
||||
fn is_current(&self, entry: &Files) -> bool {
|
||||
let current = self.current();
|
||||
let len = current.map_or(0, |current| current.len());
|
||||
entry.len() == len
|
||||
&& current.is_none_or(|current| {
|
||||
entry
|
||||
.iter()
|
||||
.all(|(id, file)| current.get(id).is_some_and(|c| Rc::ptr_eq(c, file)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn undo(&mut self) -> Option<Diff> {
|
||||
self.record_diff(|history| {
|
||||
if let Some(i) = history.index {
|
||||
if i == 0 {
|
||||
if !history.floor {
|
||||
history.index = None;
|
||||
}
|
||||
} else {
|
||||
history.index = Some(i - 1);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) -> Option<Diff> {
|
||||
self.record_diff(|history| match history.index {
|
||||
Some(i) => {
|
||||
if i + 1 < history.entries.len() {
|
||||
history.index = Some(i + 1);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if !history.entries.is_empty() {
|
||||
history.index = Some(0);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn can_undo(&self) -> bool {
|
||||
self.index.is_some_and(|i| i > 0 || !self.floor)
|
||||
}
|
||||
|
||||
pub fn can_redo(&self) -> bool {
|
||||
match self.index {
|
||||
Some(i) => i + 1 < self.entries.len(),
|
||||
None => !self.entries.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, index: usize) -> Option<&Files> {
|
||||
self.entries.get(index)
|
||||
}
|
||||
|
||||
fn push(&mut self, entry: Files) {
|
||||
if let Some(i) = self.index {
|
||||
self.entries.truncate(i + 1);
|
||||
}
|
||||
|
||||
self.entries.push(entry);
|
||||
self.index = Some(self.entries.len() - 1);
|
||||
}
|
||||
|
||||
fn record_diff<F>(&mut self, f: F) -> Option<Diff>
|
||||
where
|
||||
F: FnOnce(&mut Self),
|
||||
{
|
||||
let prev = self.index;
|
||||
f(self);
|
||||
let cur = self.index;
|
||||
if prev == cur {
|
||||
return None;
|
||||
}
|
||||
Some(Self::diff_between(
|
||||
prev.map_or_default(|i| self.get(i)),
|
||||
cur.map_or_default(|i| self.get(i)),
|
||||
))
|
||||
}
|
||||
|
||||
/// Which files differ between two states.
|
||||
pub fn diff_between(prev: Option<&Files>, cur: Option<&Files>) -> Diff {
|
||||
let prev_ids: HashSet<FileId> = prev.map_or_default(|e| e.keys().copied().collect());
|
||||
let cur_ids: HashSet<FileId> = cur.map_or_default(|e| e.keys().copied().collect());
|
||||
let mut modified = vec![];
|
||||
for id in prev_ids.intersection(&cur_ids) {
|
||||
let before = prev.map_or_default(|e| e.get(id));
|
||||
let after = cur.map_or_default(|e| e.get(id));
|
||||
if !before.zip(after).is_some_and(|(b, a)| Rc::ptr_eq(b, a)) {
|
||||
modified.push(*id);
|
||||
}
|
||||
}
|
||||
Diff {
|
||||
added: cur_ids.difference(&prev_ids).copied().collect(),
|
||||
removed: prev_ids.difference(&cur_ids).copied().collect(),
|
||||
modified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Files = HashMap<FileId, Rc<File>>;
|
||||
|
||||
/// Which files were added, removed or modified by a change of the current history entry.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct Diff {
|
||||
pub added: Vec<FileId>,
|
||||
pub removed: Vec<FileId>,
|
||||
pub modified: Vec<FileId>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
impl History {
|
||||
/// A step that is expected not to fail.
|
||||
fn push_ok(
|
||||
&mut self,
|
||||
f: impl FnOnce(&mut Files) -> Result<(), CommandError>,
|
||||
) -> Option<Diff> {
|
||||
self.create_and_push_next(f).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_file() {
|
||||
let mut history = History::default();
|
||||
|
||||
assert!(!history.can_undo());
|
||||
assert!(!history.can_redo());
|
||||
assert!(history.current().is_none());
|
||||
|
||||
let diff = history.push_ok(|e| {
|
||||
let file = Rc::new(File::default());
|
||||
e.insert(file.id, file);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
assert!(history.can_undo());
|
||||
assert!(!history.can_redo());
|
||||
assert!(history.current().is_some());
|
||||
|
||||
assert!(diff.is_some());
|
||||
|
||||
let diff = diff.unwrap();
|
||||
assert_eq!(diff.added.len(), 1);
|
||||
assert!(diff.removed.is_empty());
|
||||
assert!(diff.modified.is_empty());
|
||||
}
|
||||
|
||||
fn add_file(history: &mut History) -> FileId {
|
||||
let file = Rc::new(File::default());
|
||||
let id = file.id;
|
||||
history
|
||||
.push_ok(|e| {
|
||||
e.insert(id, file);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_failed_command_pushes_nothing() {
|
||||
let mut history = History::default();
|
||||
let diff = history.create_and_push_next(|e| {
|
||||
e.insert(FileId::default(), Rc::new(File::default()));
|
||||
Err(CommandError::InvalidData("nope".to_string()))
|
||||
});
|
||||
assert_eq!(diff, Err(CommandError::InvalidData("nope".to_string())));
|
||||
assert!(!history.can_undo());
|
||||
assert!(history.current().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undo_redo() {
|
||||
let mut history = History::default();
|
||||
assert!(history.undo().is_none());
|
||||
assert!(history.redo().is_none());
|
||||
|
||||
let id = add_file(&mut history);
|
||||
|
||||
let diff = history.undo().unwrap();
|
||||
assert_eq!(diff.removed, vec![id]);
|
||||
assert!(diff.added.is_empty());
|
||||
assert!(history.current().is_none());
|
||||
assert!(!history.can_undo());
|
||||
assert!(history.can_redo());
|
||||
assert!(history.undo().is_none());
|
||||
|
||||
let diff = history.redo().unwrap();
|
||||
assert_eq!(diff.added, vec![id]);
|
||||
assert!(history.current().unwrap().contains_key(&id));
|
||||
assert!(history.can_undo());
|
||||
assert!(!history.can_redo());
|
||||
assert!(history.redo().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modified_and_removed() {
|
||||
let mut history = History::default();
|
||||
let id = add_file(&mut history);
|
||||
|
||||
let diff = history
|
||||
.push_ok(|e| {
|
||||
let mut file = (**e.get(&id).unwrap()).clone();
|
||||
file.info.name = "renamed".to_string();
|
||||
e.insert(id, Rc::new(file));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(diff.modified, vec![id]);
|
||||
assert!(diff.added.is_empty() && diff.removed.is_empty());
|
||||
|
||||
// a step that changes nothing is not a step
|
||||
assert!(history.push_ok(|_| Ok(())).is_none());
|
||||
assert!(
|
||||
history
|
||||
.push_ok(|e| {
|
||||
let file = e[&id].clone();
|
||||
e.insert(id, file);
|
||||
Ok(())
|
||||
})
|
||||
.is_none()
|
||||
);
|
||||
|
||||
let diff = history
|
||||
.push_ok(|e| {
|
||||
e.remove(&id);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(diff.removed, vec![id]);
|
||||
assert!(history.current().unwrap().is_empty());
|
||||
|
||||
// going back restores the renamed file
|
||||
history.undo();
|
||||
assert_eq!(history.current().unwrap()[&id].info.name, "renamed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_a_file_is_modified_when_it_is_not_the_same_one_anymore() {
|
||||
let mut history = History::default();
|
||||
let id = add_file(&mut history);
|
||||
|
||||
// a copy, even if it has the same content, is another file for the history: files are
|
||||
// told apart by their identity, comparing them would read all of them
|
||||
let diff = history
|
||||
.push_ok(|e| {
|
||||
let copy = (**e.get(&id).unwrap()).clone();
|
||||
e.insert(id, Rc::new(copy));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(diff.modified, vec![id]);
|
||||
|
||||
// and the files that were not replaced are not
|
||||
let other = add_file(&mut history);
|
||||
let diff = history
|
||||
.push_ok(|e| {
|
||||
let copy = (**e.get(&other).unwrap()).clone();
|
||||
e.insert(other, Rc::new(copy));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(diff.modified, vec![other]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_after_undo_drops_redo_branch() {
|
||||
let mut history = History::default();
|
||||
let first = add_file(&mut history);
|
||||
let second = add_file(&mut history);
|
||||
|
||||
history.undo();
|
||||
assert!(history.can_redo());
|
||||
assert!(!history.current().unwrap().contains_key(&second));
|
||||
|
||||
let third = add_file(&mut history);
|
||||
assert!(!history.can_redo());
|
||||
assert!(history.redo().is_none());
|
||||
let current = history.current().unwrap();
|
||||
assert!(current.contains_key(&first));
|
||||
assert!(current.contains_key(&third));
|
||||
assert!(!current.contains_key(&second));
|
||||
}
|
||||
|
||||
fn restored_entry() -> (Files, FileId) {
|
||||
let file = Rc::new(File::default());
|
||||
let id = file.id;
|
||||
(Files::from([(id, file)]), id)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_restored_history_starts_from_its_entry() {
|
||||
let (entry, id) = restored_entry();
|
||||
let mut history = History::restored(entry);
|
||||
|
||||
assert!(history.current().unwrap().contains_key(&id));
|
||||
// there is nothing to go back to, nor forward to
|
||||
assert!(!history.can_undo());
|
||||
assert!(!history.can_redo());
|
||||
assert!(history.undo().is_none());
|
||||
assert!(history.redo().is_none());
|
||||
assert!(history.current().unwrap().contains_key(&id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_restored_history_cannot_undo_an_empty_entry_either() {
|
||||
let mut history = History::restored(Files::new());
|
||||
|
||||
assert!(history.current().is_some_and(|files| files.is_empty()));
|
||||
assert!(!history.can_undo());
|
||||
assert!(history.undo().is_none());
|
||||
// the history is not the one of an engine that has done nothing
|
||||
assert!(history.current().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_restored_history_undoes_what_comes_after_it_and_no_further() {
|
||||
let (entry, restored) = restored_entry();
|
||||
let mut history = History::restored(entry);
|
||||
|
||||
let added = add_file(&mut history);
|
||||
assert!(history.can_undo());
|
||||
assert!(!history.can_redo());
|
||||
assert_eq!(history.current().unwrap().len(), 2);
|
||||
|
||||
// back to the restored files
|
||||
let diff = history.undo().unwrap();
|
||||
assert_eq!(diff.removed, vec![added]);
|
||||
assert!(diff.added.is_empty() && diff.modified.is_empty());
|
||||
assert_eq!(history.current().unwrap().len(), 1);
|
||||
assert!(history.current().unwrap().contains_key(&restored));
|
||||
assert!(!history.can_undo());
|
||||
assert!(history.can_redo());
|
||||
assert!(history.undo().is_none());
|
||||
assert!(history.current().unwrap().contains_key(&restored));
|
||||
|
||||
let diff = history.redo().unwrap();
|
||||
assert_eq!(diff.added, vec![added]);
|
||||
assert!(history.can_undo());
|
||||
assert!(!history.can_redo());
|
||||
|
||||
// and again
|
||||
assert!(history.undo().is_some());
|
||||
assert!(history.undo().is_none());
|
||||
assert!(history.current().unwrap().contains_key(&restored));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_restored_history_removing_its_files_can_be_undone() {
|
||||
let (entry, restored) = restored_entry();
|
||||
let mut history = History::restored(entry);
|
||||
|
||||
let diff = history
|
||||
.push_ok(|e| {
|
||||
e.remove(&restored);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(diff.removed, vec![restored]);
|
||||
assert!(history.current().unwrap().is_empty());
|
||||
|
||||
let diff = history.undo().unwrap();
|
||||
assert_eq!(diff.added, vec![restored]);
|
||||
assert!(history.current().unwrap().contains_key(&restored));
|
||||
assert!(!history.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_restored_history_push_after_undo_drops_redo_branch() {
|
||||
let (entry, restored) = restored_entry();
|
||||
let mut history = History::restored(entry);
|
||||
add_file(&mut history);
|
||||
history.undo().unwrap();
|
||||
assert!(history.can_redo());
|
||||
|
||||
let other = add_file(&mut history);
|
||||
assert!(!history.can_redo());
|
||||
assert!(history.can_undo());
|
||||
let diff = history.undo().unwrap();
|
||||
assert_eq!(diff.removed, vec![other]);
|
||||
assert!(history.current().unwrap().contains_key(&restored));
|
||||
assert!(!history.can_undo());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
mod clipboard;
|
||||
mod history;
|
||||
mod order;
|
||||
mod selection;
|
||||
|
||||
pub use clipboard::*;
|
||||
pub use history::*;
|
||||
pub use order::*;
|
||||
pub use selection::*;
|
||||
|
||||
pub struct State<'a> {
|
||||
pub categories: &'a mut crate::TrackpointCategories,
|
||||
pub files: &'a mut Files,
|
||||
pub selection: &'a mut Selection,
|
||||
pub order: &'a mut FileOrder,
|
||||
pub clipboard: &'a mut Option<Clipboard>,
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{FileId, Files};
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct FileOrder(pub Vec<FileId>);
|
||||
|
||||
impl FileOrder {
|
||||
/// Moves the given files, as a block and in the given order, so that they start at `index`
|
||||
/// among the other files (clamped to the end). Unknown ids are ignored. Returns whether the
|
||||
/// order changed.
|
||||
pub fn move_files(&mut self, file_ids: &[FileId], index: usize) -> bool {
|
||||
let mut seen = HashSet::new();
|
||||
let moved: Vec<FileId> = file_ids
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|id| self.0.contains(id) && seen.insert(*id))
|
||||
.collect();
|
||||
let mut order: Vec<FileId> = self
|
||||
.0
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|id| !seen.contains(id))
|
||||
.collect();
|
||||
let index = index.min(order.len());
|
||||
order.splice(index..index, moved);
|
||||
let changed = order != self.0;
|
||||
self.0 = order;
|
||||
changed
|
||||
}
|
||||
|
||||
/// Drops the files that do not exist anymore and appends the new ones at the end.
|
||||
pub fn sync(&mut self, files: &Files) {
|
||||
self.0.retain(|id| files.contains_key(id));
|
||||
let known: HashSet<FileId> = self.0.iter().copied().collect();
|
||||
self.0
|
||||
.extend(files.keys().filter(|id| !known.contains(id)).copied());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn ids() -> [FileId; 3] {
|
||||
[FileId::default(), FileId::default(), FileId::default()]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_files_as_block_in_given_order() {
|
||||
let [a, b, c] = ids();
|
||||
let mut order = FileOrder(vec![a, b, c]);
|
||||
assert!(order.move_files(&[c, a], 1));
|
||||
// index counts among the files that are not moved
|
||||
assert_eq!(order.0, vec![b, c, a]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_files_start_and_clamped_end() {
|
||||
let [a, b, c] = ids();
|
||||
let mut order = FileOrder(vec![a, b, c]);
|
||||
assert!(order.move_files(&[c], 0));
|
||||
assert_eq!(order.0, vec![c, a, b]);
|
||||
assert!(order.move_files(&[c], 100));
|
||||
assert_eq!(order.0, vec![a, b, c]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_files_ignores_unknown_and_duplicate_ids() {
|
||||
let [a, b, c] = ids();
|
||||
let mut order = FileOrder(vec![a, b, c]);
|
||||
assert!(order.move_files(&[FileId::default(), b, b], 0));
|
||||
assert_eq!(order.0, vec![b, a, c]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_files_unchanged() {
|
||||
let [a, b, c] = ids();
|
||||
let mut order = FileOrder(vec![a, b, c]);
|
||||
assert!(!order.move_files(&[], 0));
|
||||
assert!(!order.move_files(&[a, b], 0));
|
||||
assert!(!order.move_files(&[FileId::default()], 0));
|
||||
assert_eq!(order.0, vec![a, b, c]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_drops_missing_and_appends_new_at_the_end() {
|
||||
let [a, b, c] = ids();
|
||||
let mut files = Files::default();
|
||||
for id in [b, c] {
|
||||
files.insert(id, Default::default());
|
||||
}
|
||||
let mut order = FileOrder(vec![a, b]);
|
||||
order.sync(&files);
|
||||
assert_eq!(order.0, vec![b, c]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,826 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{FileId, Files, TrackId, TrackSegmentId, WaypointId};
|
||||
|
||||
/// How a new selection combines with the current one.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum SelectMode {
|
||||
/// The new selection replaces the current one.
|
||||
Replace,
|
||||
/// The elements are added when they are of the same kind and in the same place as the
|
||||
/// current selection (see [`Selection::extend`]), otherwise they replace it.
|
||||
Add,
|
||||
/// Like `Add`, but the elements already selected are removed instead (see
|
||||
/// [`Selection::toggle`]).
|
||||
Toggle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub enum Selection {
|
||||
#[default]
|
||||
Empty,
|
||||
File {
|
||||
file_ids: HashSet<FileId>,
|
||||
},
|
||||
Track {
|
||||
file_id: FileId,
|
||||
trk_ids: HashSet<TrackId>,
|
||||
},
|
||||
TrackSegment {
|
||||
file_id: FileId,
|
||||
trk_id: TrackId,
|
||||
trkseg_ids: HashSet<TrackSegmentId>,
|
||||
},
|
||||
Waypoints {
|
||||
file_id: FileId,
|
||||
},
|
||||
Waypoint {
|
||||
file_id: FileId,
|
||||
wpt_ids: HashSet<WaypointId>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Where a track segment is: its file, then its track and its own position in it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SegmentLocation {
|
||||
pub file_id: FileId,
|
||||
pub trk: usize,
|
||||
pub seg: usize,
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
/// Combines `selection` with this one according to `mode`, ignoring the elements of
|
||||
/// `selection` that do not exist in `files`. Nothing to add or toggle leaves the selection
|
||||
/// as it is, while replacing it with nothing deselects everything.
|
||||
pub fn select(&mut self, files: Option<&Files>, mut selection: Selection, mode: SelectMode) {
|
||||
match files {
|
||||
Some(files) => selection.retain_existing(files),
|
||||
None => selection = Selection::Empty,
|
||||
}
|
||||
match mode {
|
||||
SelectMode::Replace => *self = selection,
|
||||
_ if selection == Selection::Empty => {}
|
||||
SelectMode::Add => self.extend(selection),
|
||||
SelectMode::Toggle => self.toggle(selection),
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects all the elements of the same kind as the selected ones, see
|
||||
/// [`Selection::all_at_level`]. Does nothing when there is nothing more to select.
|
||||
pub fn select_all(&mut self, files: &Files, order: &[FileId]) {
|
||||
if let Some(all) = self.all_at_level(files, order) {
|
||||
*self = all;
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the selection to the next (`down`) or previous element, see
|
||||
/// [`Selection::neighbour`]. With `add`, the element is added to the selection instead of
|
||||
/// replacing it.
|
||||
pub fn select_neighbour(&mut self, files: &Files, order: &[FileId], down: bool, add: bool) {
|
||||
if let Some(next) = self.neighbour(files, order, down) {
|
||||
if add {
|
||||
self.extend(next);
|
||||
} else {
|
||||
*self = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
/// The track segments covered by the selection, in order: the segments of several selected
|
||||
/// files follow `order` (the files missing from it come last), then the tracks and segments
|
||||
/// follow the file. Waypoints are not segments: selecting only waypoints covers none.
|
||||
pub fn segment_locations(&self, files: &Files, order: &[FileId]) -> Vec<SegmentLocation> {
|
||||
let mut locations = vec![];
|
||||
let mut add = |file_id: FileId,
|
||||
trk_filter: &dyn Fn(&crate::Track) -> bool,
|
||||
seg_filter: &dyn Fn(&crate::TrackSegment) -> bool| {
|
||||
let Some(file) = files.get(&file_id) else {
|
||||
return;
|
||||
};
|
||||
for (trk, track) in file.trk.iter().enumerate().filter(|(_, t)| trk_filter(t)) {
|
||||
for (seg, segment) in track.trkseg.iter().enumerate() {
|
||||
if seg_filter(segment) {
|
||||
locations.push(SegmentLocation { file_id, trk, seg });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
match self {
|
||||
Selection::Empty | Selection::Waypoints { .. } | Selection::Waypoint { .. } => (),
|
||||
Selection::File { file_ids } => {
|
||||
let ordered = order
|
||||
.iter()
|
||||
.filter(|id| file_ids.contains(id))
|
||||
.chain(file_ids.iter().filter(|id| !order.contains(id)));
|
||||
for id in ordered {
|
||||
add(*id, &|_| true, &|_| true);
|
||||
}
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
add(*file_id, &|trk| trk_ids.contains(&trk.id), &|_| true)
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => add(*file_id, &|trk| trk.id == *trk_id, &|seg| {
|
||||
trkseg_ids.contains(&seg.id)
|
||||
}),
|
||||
}
|
||||
locations
|
||||
}
|
||||
|
||||
/// Drops what does not exist anymore (e.g. after an undo): files, and the tracks, segments
|
||||
/// and waypoints of the files that remain. A selection left with nothing becomes empty.
|
||||
pub fn retain_existing(&mut self, files: &Files) {
|
||||
let empty = match self {
|
||||
Selection::Empty => false,
|
||||
Selection::File { file_ids } => {
|
||||
file_ids.retain(|id| files.contains_key(id));
|
||||
file_ids.is_empty()
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => match files.get(file_id) {
|
||||
Some(file) => {
|
||||
trk_ids.retain(|id| file.trk.iter().any(|trk| trk.id == *id));
|
||||
trk_ids.is_empty()
|
||||
}
|
||||
None => true,
|
||||
},
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => match files
|
||||
.get(file_id)
|
||||
.and_then(|file| file.trk.iter().find(|trk| trk.id == *trk_id))
|
||||
{
|
||||
Some(trk) => {
|
||||
trkseg_ids.retain(|id| trk.trkseg.iter().any(|seg| seg.id == *id));
|
||||
trkseg_ids.is_empty()
|
||||
}
|
||||
None => true,
|
||||
},
|
||||
Selection::Waypoints { file_id } => !files.contains_key(file_id),
|
||||
Selection::Waypoint { file_id, wpt_ids } => match files.get(file_id) {
|
||||
Some(file) => {
|
||||
wpt_ids.retain(|id| file.wpt.iter().any(|wpt| wpt.id == *id));
|
||||
wpt_ids.is_empty()
|
||||
}
|
||||
None => true,
|
||||
},
|
||||
};
|
||||
if empty {
|
||||
*self = Selection::Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds `other` to the selection: the elements are merged when they are of the same kind and
|
||||
/// in the same place (same file, same track for segments), otherwise `other` replaces the
|
||||
/// selection.
|
||||
pub fn extend(&mut self, other: Selection) {
|
||||
match (&mut *self, other) {
|
||||
(Selection::File { file_ids }, Selection::File { file_ids: other }) => {
|
||||
file_ids.extend(other)
|
||||
}
|
||||
(
|
||||
Selection::Track { file_id, trk_ids },
|
||||
Selection::Track {
|
||||
file_id: other_file,
|
||||
trk_ids: other,
|
||||
},
|
||||
) if *file_id == other_file => trk_ids.extend(other),
|
||||
(
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
},
|
||||
Selection::TrackSegment {
|
||||
file_id: other_file,
|
||||
trk_id: other_trk,
|
||||
trkseg_ids: other,
|
||||
},
|
||||
) if *file_id == other_file && *trk_id == other_trk => trkseg_ids.extend(other),
|
||||
(Selection::Waypoints { file_id }, Selection::Waypoints { file_id: other })
|
||||
if *file_id == other => {}
|
||||
(
|
||||
Selection::Waypoint { file_id, wpt_ids },
|
||||
Selection::Waypoint {
|
||||
file_id: other_file,
|
||||
wpt_ids: other,
|
||||
},
|
||||
) if *file_id == other_file => wpt_ids.extend(other),
|
||||
(selection, other) => *selection = other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`Selection::extend`], but the elements of `other` that are already selected are
|
||||
/// removed from the selection instead. A selection left with nothing is empty.
|
||||
pub fn toggle(&mut self, other: Selection) {
|
||||
fn toggle_ids<T: std::hash::Hash + Eq>(ids: &mut HashSet<T>, other: HashSet<T>) {
|
||||
for id in other {
|
||||
if !ids.remove(&id) {
|
||||
ids.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
let empty = match (&mut *self, other) {
|
||||
(Selection::File { file_ids }, Selection::File { file_ids: other }) => {
|
||||
toggle_ids(file_ids, other);
|
||||
file_ids.is_empty()
|
||||
}
|
||||
(
|
||||
Selection::Track { file_id, trk_ids },
|
||||
Selection::Track {
|
||||
file_id: other_file,
|
||||
trk_ids: other,
|
||||
},
|
||||
) if *file_id == other_file => {
|
||||
toggle_ids(trk_ids, other);
|
||||
trk_ids.is_empty()
|
||||
}
|
||||
(
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
},
|
||||
Selection::TrackSegment {
|
||||
file_id: other_file,
|
||||
trk_id: other_trk,
|
||||
trkseg_ids: other,
|
||||
},
|
||||
) if *file_id == other_file && *trk_id == other_trk => {
|
||||
toggle_ids(trkseg_ids, other);
|
||||
trkseg_ids.is_empty()
|
||||
}
|
||||
(Selection::Waypoints { file_id }, Selection::Waypoints { file_id: other })
|
||||
if *file_id == other =>
|
||||
{
|
||||
true
|
||||
}
|
||||
(
|
||||
Selection::Waypoint { file_id, wpt_ids },
|
||||
Selection::Waypoint {
|
||||
file_id: other_file,
|
||||
wpt_ids: other,
|
||||
},
|
||||
) if *file_id == other_file => {
|
||||
toggle_ids(wpt_ids, other);
|
||||
wpt_ids.is_empty()
|
||||
}
|
||||
(selection, other) => {
|
||||
*selection = other;
|
||||
false
|
||||
}
|
||||
};
|
||||
if empty {
|
||||
*self = Selection::Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Position of the first and last of the `selected` ids among the `all` ids.
|
||||
fn selected_range<T: std::hash::Hash + Eq>(
|
||||
all: impl Iterator<Item = T>,
|
||||
selected: &HashSet<T>,
|
||||
) -> Option<(usize, usize)> {
|
||||
let mut range: Option<(usize, usize)> = None;
|
||||
for (i, id) in all.enumerate() {
|
||||
if selected.contains(&id) {
|
||||
range = Some(range.map_or((i, i), |(first, _)| (first, i)));
|
||||
}
|
||||
}
|
||||
range
|
||||
}
|
||||
|
||||
/// The id before or after the selected ones among `ids`, without wrapping around.
|
||||
fn neighbour<T: Copy + PartialEq + std::hash::Hash + Eq>(
|
||||
ids: &[T],
|
||||
selected: &HashSet<T>,
|
||||
down: bool,
|
||||
) -> Option<T> {
|
||||
let (first, last) = selected_range(ids.iter().copied(), selected)?;
|
||||
if down {
|
||||
ids.get(last + 1).copied()
|
||||
} else {
|
||||
first.checked_sub(1).map(|i| ids[i])
|
||||
}
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
/// The files holding the selected elements, as a selection of files.
|
||||
pub fn to_files(&self) -> Selection {
|
||||
match self {
|
||||
Selection::Empty => Selection::Empty,
|
||||
Selection::File { .. } => self.clone(),
|
||||
Selection::Track { file_id, .. }
|
||||
| Selection::TrackSegment { file_id, .. }
|
||||
| Selection::Waypoints { file_id }
|
||||
| Selection::Waypoint { file_id, .. } => Selection::File {
|
||||
file_ids: [*file_id].into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects all the elements of the same kind as the selected ones, in the same place: all
|
||||
/// the files, all the tracks of the file, all the segments of the track, all the waypoints
|
||||
/// of the file. All the files when nothing is selected. `None` when there is nothing to
|
||||
/// select (the waypoints node, or a file that does not exist).
|
||||
pub fn all_at_level(&self, files: &Files, order: &[FileId]) -> Option<Selection> {
|
||||
match self {
|
||||
Selection::Empty | Selection::File { .. } => Some(Selection::File {
|
||||
file_ids: order.iter().copied().collect(),
|
||||
}),
|
||||
Selection::Track { file_id, .. } => Some(Selection::Track {
|
||||
file_id: *file_id,
|
||||
trk_ids: files.get(file_id)?.trk.iter().map(|trk| trk.id).collect(),
|
||||
}),
|
||||
Selection::TrackSegment {
|
||||
file_id, trk_id, ..
|
||||
} => {
|
||||
let trk = files
|
||||
.get(file_id)?
|
||||
.trk
|
||||
.iter()
|
||||
.find(|trk| trk.id == *trk_id)?;
|
||||
Some(Selection::TrackSegment {
|
||||
file_id: *file_id,
|
||||
trk_id: *trk_id,
|
||||
trkseg_ids: trk.trkseg.iter().map(|seg| seg.id).collect(),
|
||||
})
|
||||
}
|
||||
Selection::Waypoints { .. } => None,
|
||||
Selection::Waypoint { file_id, .. } => Some(Selection::Waypoint {
|
||||
file_id: *file_id,
|
||||
wpt_ids: files.get(file_id)?.wpt.iter().map(|wpt| wpt.id).collect(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// The element next to the selected ones, as a selection of that one element: the next one
|
||||
/// (`down`) or the previous one among the elements of the same kind, in the same place.
|
||||
/// The tracks, segments and waypoints stop at their ends, the files wrap around and skip
|
||||
/// the ones that are already selected. `None` when there is no such element.
|
||||
pub fn neighbour(&self, files: &Files, order: &[FileId], down: bool) -> Option<Selection> {
|
||||
match self {
|
||||
Selection::Empty | Selection::Waypoints { .. } => None,
|
||||
Selection::File { file_ids } => {
|
||||
let (first, last) = selected_range(order.iter(), &file_ids.iter().collect())?;
|
||||
let limit = if down { last } else { first };
|
||||
let n = order.len();
|
||||
let mut i = limit;
|
||||
loop {
|
||||
i = if down { (i + 1) % n } else { (i + n - 1) % n };
|
||||
if i == limit {
|
||||
return None;
|
||||
}
|
||||
if !file_ids.contains(&order[i]) {
|
||||
return Some(Selection::File {
|
||||
file_ids: [order[i]].into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let ids: Vec<TrackId> = files.get(file_id)?.trk.iter().map(|t| t.id).collect();
|
||||
Some(Selection::Track {
|
||||
file_id: *file_id,
|
||||
trk_ids: [neighbour(&ids, trk_ids, down)?].into(),
|
||||
})
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => {
|
||||
let trk = files
|
||||
.get(file_id)?
|
||||
.trk
|
||||
.iter()
|
||||
.find(|trk| trk.id == *trk_id)?;
|
||||
let ids: Vec<TrackSegmentId> = trk.trkseg.iter().map(|seg| seg.id).collect();
|
||||
Some(Selection::TrackSegment {
|
||||
file_id: *file_id,
|
||||
trk_id: *trk_id,
|
||||
trkseg_ids: [neighbour(&ids, trkseg_ids, down)?].into(),
|
||||
})
|
||||
}
|
||||
Selection::Waypoint { file_id, wpt_ids } => {
|
||||
let ids: Vec<WaypointId> =
|
||||
files.get(file_id)?.wpt.iter().map(|wpt| wpt.id).collect();
|
||||
Some(Selection::Waypoint {
|
||||
file_id: *file_id,
|
||||
wpt_ids: [neighbour(&ids, wpt_ids, down)?].into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{File, Track, TrackSegment, Waypoint, WaypointChunk};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_retain_existing() {
|
||||
let file = File::default();
|
||||
let (kept, gone) = (file.id, FileId::default());
|
||||
let mut files = Files::default();
|
||||
files.insert(kept, Rc::new(file));
|
||||
|
||||
let mut selection = Selection::File {
|
||||
file_ids: [kept, gone].into(),
|
||||
};
|
||||
selection.retain_existing(&files);
|
||||
assert!(
|
||||
matches!(&selection, Selection::File { file_ids } if file_ids.len() == 1 && file_ids.contains(&kept))
|
||||
);
|
||||
|
||||
let mut selection = Selection::File {
|
||||
file_ids: [gone].into(),
|
||||
};
|
||||
selection.retain_existing(&files);
|
||||
assert!(matches!(selection, Selection::Empty));
|
||||
|
||||
let mut selection = Selection::Waypoints { file_id: kept };
|
||||
selection.retain_existing(&files);
|
||||
assert!(matches!(selection, Selection::Waypoints { .. }));
|
||||
let mut selection = Selection::Waypoints { file_id: gone };
|
||||
selection.retain_existing(&files);
|
||||
assert!(matches!(selection, Selection::Empty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retain_existing_elements() {
|
||||
let mut file = File::default();
|
||||
file.trk.push(Track {
|
||||
trkseg: vec![TrackSegment::default()],
|
||||
..Default::default()
|
||||
});
|
||||
file.wpt.push(WaypointChunk {
|
||||
wpt: vec![Waypoint::default()],
|
||||
..Default::default()
|
||||
});
|
||||
let (file_id, trk_id) = (file.id, file.trk[0].id);
|
||||
let (seg_id, wpt_id) = (file.trk[0].trkseg[0].id, file.wpt[0].id);
|
||||
let mut files = Files::default();
|
||||
files.insert(file_id, Rc::new(file));
|
||||
|
||||
let mut selection = Selection::Track {
|
||||
file_id,
|
||||
trk_ids: [trk_id, TrackId::default()].into(),
|
||||
};
|
||||
selection.retain_existing(&files);
|
||||
assert_eq!(
|
||||
selection,
|
||||
Selection::Track {
|
||||
file_id,
|
||||
trk_ids: [trk_id].into()
|
||||
}
|
||||
);
|
||||
|
||||
let mut selection = Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: [seg_id, TrackSegmentId::default()].into(),
|
||||
};
|
||||
selection.retain_existing(&files);
|
||||
assert!(
|
||||
matches!(&selection, Selection::TrackSegment { trkseg_ids, .. } if trkseg_ids.len() == 1)
|
||||
);
|
||||
// a segment under the wrong track
|
||||
let mut selection = Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id: TrackId::default(),
|
||||
trkseg_ids: [seg_id].into(),
|
||||
};
|
||||
selection.retain_existing(&files);
|
||||
assert_eq!(selection, Selection::Empty);
|
||||
|
||||
let mut selection = Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: [wpt_id, WaypointId::default()].into(),
|
||||
};
|
||||
selection.retain_existing(&files);
|
||||
assert!(matches!(&selection, Selection::Waypoint { wpt_ids, .. } if wpt_ids.len() == 1));
|
||||
let mut selection = Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: [WaypointId::default()].into(),
|
||||
};
|
||||
selection.retain_existing(&files);
|
||||
assert_eq!(selection, Selection::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend() {
|
||||
let file_id = FileId::default();
|
||||
let (a, b) = (TrackId::default(), TrackId::default());
|
||||
let tracks = |ids: &[TrackId]| Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids.iter().copied().collect(),
|
||||
};
|
||||
|
||||
// same kind and place: merged
|
||||
let mut selection = tracks(&[a]);
|
||||
selection.extend(tracks(&[b]));
|
||||
assert_eq!(selection, tracks(&[a, b]));
|
||||
|
||||
// another file, or another kind: replaced
|
||||
let mut selection = tracks(&[a]);
|
||||
let other = Selection::Track {
|
||||
file_id: FileId::default(),
|
||||
trk_ids: [b].into(),
|
||||
};
|
||||
selection.extend(other.clone());
|
||||
assert_eq!(selection, other);
|
||||
selection.extend(Selection::Waypoints { file_id });
|
||||
assert_eq!(selection, Selection::Waypoints { file_id });
|
||||
|
||||
// segments of another track: replaced
|
||||
let seg = TrackSegmentId::default();
|
||||
let segments = |trk_id| Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: [seg].into(),
|
||||
};
|
||||
let mut selection = segments(a);
|
||||
selection.extend(segments(b));
|
||||
assert_eq!(selection, segments(b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle() {
|
||||
let file_id = FileId::default();
|
||||
let (a, b) = (TrackId::default(), TrackId::default());
|
||||
let tracks = |ids: &[TrackId]| Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids.iter().copied().collect(),
|
||||
};
|
||||
|
||||
// same kind and place: toggled one by one
|
||||
let mut selection = tracks(&[a]);
|
||||
selection.toggle(tracks(&[b]));
|
||||
assert_eq!(selection, tracks(&[a, b]));
|
||||
selection.toggle(tracks(&[a]));
|
||||
assert_eq!(selection, tracks(&[b]));
|
||||
selection.toggle(tracks(&[a, b]));
|
||||
assert_eq!(selection, tracks(&[a]));
|
||||
|
||||
// nothing left: empty
|
||||
selection.toggle(tracks(&[a]));
|
||||
assert_eq!(selection, Selection::Empty);
|
||||
|
||||
// another file, or another kind: replaced
|
||||
let mut selection = tracks(&[a]);
|
||||
selection.toggle(Selection::Waypoints { file_id });
|
||||
assert_eq!(selection, Selection::Waypoints { file_id });
|
||||
// the waypoints node toggles itself off
|
||||
selection.toggle(Selection::Waypoints { file_id });
|
||||
assert_eq!(selection, Selection::Empty);
|
||||
|
||||
// files
|
||||
let (f1, f2) = (FileId::default(), FileId::default());
|
||||
let files = |ids: &[FileId]| Selection::File {
|
||||
file_ids: ids.iter().copied().collect(),
|
||||
};
|
||||
let mut selection = files(&[f1]);
|
||||
selection.toggle(files(&[f2]));
|
||||
assert_eq!(selection, files(&[f1, f2]));
|
||||
selection.toggle(files(&[f1]));
|
||||
assert_eq!(selection, files(&[f2]));
|
||||
}
|
||||
|
||||
/// A file with 3 tracks of 2 segments each and 3 waypoints, with the ids of everything.
|
||||
struct Tree {
|
||||
files: Files,
|
||||
order: Vec<FileId>,
|
||||
file: FileId,
|
||||
tracks: Vec<TrackId>,
|
||||
segments: Vec<Vec<TrackSegmentId>>,
|
||||
waypoints: Vec<WaypointId>,
|
||||
}
|
||||
|
||||
fn tree() -> Tree {
|
||||
let mut file = File::default();
|
||||
for _ in 0..3 {
|
||||
file.trk.push(Track {
|
||||
trkseg: vec![TrackSegment::default(), TrackSegment::default()],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
file.wpt.push(WaypointChunk {
|
||||
wpt: vec![Waypoint::default(), Waypoint::default()],
|
||||
..Default::default()
|
||||
});
|
||||
file.wpt.push(WaypointChunk {
|
||||
wpt: vec![Waypoint::default()],
|
||||
..Default::default()
|
||||
});
|
||||
Tree {
|
||||
file: file.id,
|
||||
tracks: file.trk.iter().map(|t| t.id).collect(),
|
||||
segments: file
|
||||
.trk
|
||||
.iter()
|
||||
.map(|t| t.trkseg.iter().map(|s| s.id).collect())
|
||||
.collect(),
|
||||
waypoints: file.wpt.iter().map(|w| w.id).collect(),
|
||||
order: vec![file.id],
|
||||
files: Files::from([(file.id, Rc::new(file))]),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_at_level() {
|
||||
let t = tree();
|
||||
let other = FileId::default();
|
||||
let order = [other, t.file];
|
||||
let all = |selection: &Selection| selection.all_at_level(&t.files, &order);
|
||||
|
||||
// the files, in the order
|
||||
let all_files = Selection::File {
|
||||
file_ids: [other, t.file].into(),
|
||||
};
|
||||
assert_eq!(all(&Selection::Empty), Some(all_files.clone()));
|
||||
assert_eq!(
|
||||
all(&Selection::File {
|
||||
file_ids: [t.file].into()
|
||||
}),
|
||||
Some(all_files)
|
||||
);
|
||||
|
||||
// the siblings of the selected elements
|
||||
assert_eq!(
|
||||
all(&Selection::Track {
|
||||
file_id: t.file,
|
||||
trk_ids: [t.tracks[1]].into()
|
||||
}),
|
||||
Some(Selection::Track {
|
||||
file_id: t.file,
|
||||
trk_ids: t.tracks.iter().copied().collect()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
all(&Selection::TrackSegment {
|
||||
file_id: t.file,
|
||||
trk_id: t.tracks[2],
|
||||
trkseg_ids: [t.segments[2][0]].into()
|
||||
}),
|
||||
Some(Selection::TrackSegment {
|
||||
file_id: t.file,
|
||||
trk_id: t.tracks[2],
|
||||
trkseg_ids: t.segments[2].iter().copied().collect()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
all(&Selection::Waypoint {
|
||||
file_id: t.file,
|
||||
wpt_ids: [t.waypoints[0]].into()
|
||||
}),
|
||||
Some(Selection::Waypoint {
|
||||
file_id: t.file,
|
||||
wpt_ids: t.waypoints.iter().copied().collect()
|
||||
})
|
||||
);
|
||||
|
||||
// nothing to select
|
||||
assert_eq!(all(&Selection::Waypoints { file_id: t.file }), None);
|
||||
let unknown = FileId::default();
|
||||
assert_eq!(
|
||||
all(&Selection::Track {
|
||||
file_id: unknown,
|
||||
trk_ids: HashSet::new()
|
||||
}),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neighbour_of_tracks_stops_at_the_ends() {
|
||||
let t = tree();
|
||||
let tracks = |ids: &[TrackId]| Selection::Track {
|
||||
file_id: t.file,
|
||||
trk_ids: ids.iter().copied().collect(),
|
||||
};
|
||||
let next = |selection: &Selection, down| selection.neighbour(&t.files, &t.order, down);
|
||||
|
||||
assert_eq!(
|
||||
next(&tracks(&[t.tracks[1]]), true),
|
||||
Some(tracks(&[t.tracks[2]]))
|
||||
);
|
||||
assert_eq!(
|
||||
next(&tracks(&[t.tracks[1]]), false),
|
||||
Some(tracks(&[t.tracks[0]]))
|
||||
);
|
||||
assert_eq!(next(&tracks(&[t.tracks[2]]), true), None);
|
||||
assert_eq!(next(&tracks(&[t.tracks[0]]), false), None);
|
||||
// from the end of the selected range, whatever its holes
|
||||
let range = tracks(&[t.tracks[0], t.tracks[1]]);
|
||||
assert_eq!(next(&range, true), Some(tracks(&[t.tracks[2]])));
|
||||
assert_eq!(next(&range, false), None);
|
||||
let holes = tracks(&[t.tracks[0], t.tracks[2]]);
|
||||
assert_eq!(next(&holes, true), None);
|
||||
assert_eq!(next(&holes, false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neighbour_of_segments_and_waypoints() {
|
||||
let t = tree();
|
||||
let segments = |trk: usize, seg: usize| Selection::TrackSegment {
|
||||
file_id: t.file,
|
||||
trk_id: t.tracks[trk],
|
||||
trkseg_ids: [t.segments[trk][seg]].into(),
|
||||
};
|
||||
let next = |selection: &Selection, down| selection.neighbour(&t.files, &t.order, down);
|
||||
// the segments stay in their track
|
||||
assert_eq!(next(&segments(1, 0), true), Some(segments(1, 1)));
|
||||
assert_eq!(next(&segments(1, 1), true), None);
|
||||
assert_eq!(next(&segments(1, 1), false), Some(segments(1, 0)));
|
||||
assert_eq!(next(&segments(1, 0), false), None);
|
||||
|
||||
// the waypoints are in the order of the file, across its chunks
|
||||
let waypoint = |i: usize| Selection::Waypoint {
|
||||
file_id: t.file,
|
||||
wpt_ids: [t.waypoints[i]].into(),
|
||||
};
|
||||
assert_eq!(next(&waypoint(1), true), Some(waypoint(2)));
|
||||
assert_eq!(next(&waypoint(2), true), None);
|
||||
assert_eq!(next(&waypoint(0), false), None);
|
||||
assert_eq!(next(&waypoint(2), false), Some(waypoint(1)));
|
||||
|
||||
assert_eq!(next(&Selection::Waypoints { file_id: t.file }, true), None);
|
||||
assert_eq!(next(&Selection::Empty, true), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neighbour_of_files_wraps_around_and_skips_the_selected() {
|
||||
let files = Files::default();
|
||||
let [a, b, c] = [FileId::default(), FileId::default(), FileId::default()];
|
||||
let order = [a, b, c];
|
||||
let select = |ids: &[FileId]| Selection::File {
|
||||
file_ids: ids.iter().copied().collect(),
|
||||
};
|
||||
let next = |ids: &[FileId], down| select(ids).neighbour(&files, &order, down);
|
||||
|
||||
assert_eq!(next(&[a], true), Some(select(&[b])));
|
||||
assert_eq!(next(&[c], true), Some(select(&[a])));
|
||||
assert_eq!(next(&[a], false), Some(select(&[c])));
|
||||
assert_eq!(next(&[b], false), Some(select(&[a])));
|
||||
// from the end of the selection, skipping the files already selected
|
||||
assert_eq!(next(&[a, b], true), Some(select(&[c])));
|
||||
assert_eq!(next(&[a, b], false), Some(select(&[c])));
|
||||
assert_eq!(next(&[c, a], true), Some(select(&[b])));
|
||||
// everything is selected
|
||||
assert_eq!(next(&[a, b, c], true), None);
|
||||
// none of them is in the order
|
||||
assert_eq!(next(&[FileId::default()], true), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_files() {
|
||||
let (file_id, trk_id) = (FileId::default(), TrackId::default());
|
||||
let file = Selection::File {
|
||||
file_ids: [file_id].into(),
|
||||
};
|
||||
assert_eq!(Selection::Empty.to_files(), Selection::Empty);
|
||||
assert_eq!(file.to_files(), file);
|
||||
assert_eq!(
|
||||
Selection::Track {
|
||||
file_id,
|
||||
trk_ids: [trk_id].into()
|
||||
}
|
||||
.to_files(),
|
||||
file
|
||||
);
|
||||
assert_eq!(
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: HashSet::new()
|
||||
}
|
||||
.to_files(),
|
||||
file
|
||||
);
|
||||
assert_eq!(Selection::Waypoints { file_id }.to_files(), file);
|
||||
assert_eq!(
|
||||
Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: HashSet::new()
|
||||
}
|
||||
.to_files(),
|
||||
file
|
||||
);
|
||||
let several = Selection::File {
|
||||
file_ids: [file_id, FileId::default()].into(),
|
||||
};
|
||||
assert_eq!(several.to_files(), several);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user