mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-07 06:14:07 +00:00
progress
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use crate::{Command, FileId};
|
||||
use crate::{Command, FileId, Selection};
|
||||
|
||||
/// Everything the frontend can ask the engine to do.
|
||||
///
|
||||
@@ -9,13 +9,14 @@ pub enum Action<'a> {
|
||||
Edit(Command<'a>),
|
||||
Undo,
|
||||
Redo,
|
||||
/// Selects files, tracks, segments or waypoints (or the waypoints node of one file),
|
||||
/// ignoring what does not exist: a selection left with nothing is empty, which deselects
|
||||
/// everything. With `add`, the elements are added to the current selection when they can
|
||||
/// be merged with it (see [`Selection::extend`]), otherwise they replace it.
|
||||
Select {
|
||||
file_ids: Vec<FileId>,
|
||||
selection: Selection,
|
||||
add: bool,
|
||||
},
|
||||
AddSelect {
|
||||
file_ids: Vec<FileId>,
|
||||
},
|
||||
SelectAll,
|
||||
/// Moves the files to `index` among the other files, in the given order. Not undoable.
|
||||
Reorder {
|
||||
file_ids: Vec<FileId>,
|
||||
|
||||
@@ -81,20 +81,18 @@ impl Engine {
|
||||
Action::Edit(command) => self.edit(command),
|
||||
Action::Undo => self.stack.undo(),
|
||||
Action::Redo => self.stack.redo(),
|
||||
Action::Select { file_ids } => {
|
||||
self.selection = Selection::File {
|
||||
file_ids: file_ids.into_iter().collect(),
|
||||
};
|
||||
None
|
||||
}
|
||||
Action::AddSelect { file_ids } => {
|
||||
match &mut self.selection {
|
||||
Selection::File { file_ids: ids } => ids.extend(file_ids),
|
||||
selection => {
|
||||
*selection = Selection::File {
|
||||
file_ids: file_ids.into_iter().collect(),
|
||||
}
|
||||
Action::Select { mut selection, add } => {
|
||||
match self.stack.current() {
|
||||
Some(files) => selection.retain_existing(files),
|
||||
None => selection = Selection::Empty,
|
||||
}
|
||||
if add {
|
||||
// nothing to add: keep the selection
|
||||
if selection != Selection::Empty {
|
||||
self.selection.extend(selection);
|
||||
}
|
||||
} else {
|
||||
self.selection = selection;
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -102,12 +100,6 @@ impl Engine {
|
||||
self.order.move_files(&file_ids, index);
|
||||
None
|
||||
}
|
||||
Action::SelectAll => {
|
||||
self.selection = Selection::File {
|
||||
file_ids: self.order.0.iter().copied().collect(),
|
||||
};
|
||||
None
|
||||
}
|
||||
};
|
||||
let changed = self.diff.is_some()
|
||||
|| self.selection != selection_before
|
||||
@@ -156,7 +148,7 @@ impl Engine {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Load, Metadata, New, NewTrack, Style};
|
||||
use crate::{Load, Metadata, New, NewTrack, Style, TrackId, TrackSegmentId};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -402,12 +394,12 @@ mod tests {
|
||||
let a = engine.order()[0];
|
||||
// a new file is selected by the edit
|
||||
assert!(engine.selection_changed());
|
||||
assert!(!engine.execute(Action::Select { file_ids: vec![a] }));
|
||||
assert!(!select_files(&mut engine, &[a]));
|
||||
assert!(!engine.selection_changed());
|
||||
assert!(engine.execute(Action::Select { file_ids: vec![] }));
|
||||
assert!(select_files(&mut engine, &[]));
|
||||
assert!(engine.selection_changed());
|
||||
// undoing drops the selected file from the selection
|
||||
engine.execute(Action::Select { file_ids: vec![a] });
|
||||
select_files(&mut engine, &[a]);
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert!(engine.selection_changed());
|
||||
assert_eq!(engine.selection(), &Selection::Empty);
|
||||
@@ -421,7 +413,7 @@ mod tests {
|
||||
engine.execute(Action::Undo);
|
||||
assert!(engine.statistics().total_distance.is_empty());
|
||||
engine.execute(Action::Redo);
|
||||
engine.execute(Action::SelectAll);
|
||||
select_all(&mut engine);
|
||||
assert_eq!(engine.statistics().total_distance.len(), 80);
|
||||
}
|
||||
|
||||
@@ -434,14 +426,14 @@ mod tests {
|
||||
let b = selected(&engine)[0];
|
||||
assert_eq!(selected(&engine), vec![b]);
|
||||
|
||||
assert!(engine.execute(Action::Select { file_ids: vec![a] }));
|
||||
assert!(select_files(&mut engine, &[a]));
|
||||
assert_eq!(selected(&engine), vec![a]);
|
||||
|
||||
assert!(engine.execute(Action::AddSelect { file_ids: vec![b] }));
|
||||
assert!(select_elements(&mut engine, files(&[b]), true));
|
||||
assert_eq!(selected(&engine).len(), 2);
|
||||
|
||||
engine.execute(Action::Select { file_ids: vec![] });
|
||||
assert!(engine.execute(Action::SelectAll));
|
||||
select_files(&mut engine, &[]);
|
||||
assert!(select_all(&mut engine));
|
||||
assert_eq!(selected(&engine).len(), 2);
|
||||
}
|
||||
|
||||
@@ -449,8 +441,119 @@ mod tests {
|
||||
fn test_selection_does_not_create_undo_steps() {
|
||||
let mut engine = Engine::default();
|
||||
new(&mut engine, "a");
|
||||
engine.execute(Action::SelectAll);
|
||||
select_all(&mut engine);
|
||||
engine.execute(Action::Undo);
|
||||
assert!(engine.stack.current().is_none());
|
||||
}
|
||||
|
||||
fn select_elements(engine: &mut Engine, selection: Selection, add: bool) -> bool {
|
||||
engine.execute(Action::Select { selection, add })
|
||||
}
|
||||
|
||||
fn files(ids: &[FileId]) -> Selection {
|
||||
Selection::File {
|
||||
file_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn select_files(engine: &mut Engine, ids: &[FileId]) -> bool {
|
||||
select_elements(engine, files(ids), false)
|
||||
}
|
||||
|
||||
fn select_all(engine: &mut Engine) -> bool {
|
||||
let ids = engine.order().to_vec();
|
||||
select_files(engine, &ids)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_elements() {
|
||||
let mut engine = Engine::default();
|
||||
load(&mut engine, "data/with_tracks_and_segments.gpx");
|
||||
let file = engine
|
||||
.stack
|
||||
.current()
|
||||
.unwrap()
|
||||
.values()
|
||||
.next()
|
||||
.unwrap()
|
||||
.clone();
|
||||
assert!(file.trk.len() >= 2 && file.trk[0].trkseg.len() >= 2);
|
||||
let (t0, t1) = (file.trk[0].id, file.trk[1].id);
|
||||
let (s0, s1) = (file.trk[0].trkseg[0].id, file.trk[0].trkseg[1].id);
|
||||
|
||||
// tracks, then more tracks
|
||||
let tracks = |ids: &[TrackId]| Selection::Track {
|
||||
file_id: file.id,
|
||||
trk_ids: ids.iter().copied().collect(),
|
||||
};
|
||||
assert!(select_elements(&mut engine, tracks(&[t0]), false));
|
||||
assert_eq!(engine.selection(), &tracks(&[t0]));
|
||||
assert!(select_elements(&mut engine, tracks(&[t1]), true));
|
||||
assert_eq!(engine.selection(), &tracks(&[t0, t1]));
|
||||
|
||||
// segments of a track, added only within the same track
|
||||
let segments = |trk_id, ids: &[TrackSegmentId]| Selection::TrackSegment {
|
||||
file_id: file.id,
|
||||
trk_id,
|
||||
trkseg_ids: ids.iter().copied().collect(),
|
||||
};
|
||||
assert!(select_elements(&mut engine, segments(t0, &[s0]), true));
|
||||
assert_eq!(engine.selection(), &segments(t0, &[s0]));
|
||||
assert!(select_elements(&mut engine, segments(t0, &[s1]), true));
|
||||
assert_eq!(engine.selection(), &segments(t0, &[s0, s1]));
|
||||
// the statistics follow the selected segments
|
||||
assert_eq!(
|
||||
engine.statistics().total_distance.len(),
|
||||
file.trk[0].trkseg[0].len() + file.trk[0].trkseg[1].len()
|
||||
);
|
||||
|
||||
// the waypoints node
|
||||
let node = Selection::Waypoints { file_id: file.id };
|
||||
assert!(select_elements(&mut engine, node.clone(), false));
|
||||
assert!(!select_elements(&mut engine, node.clone(), true));
|
||||
assert_eq!(engine.selection(), &node);
|
||||
assert!(engine.selection_changed() == false);
|
||||
|
||||
// unknown elements and files are ignored when adding
|
||||
let unknown = tracks(&[TrackId::default()]);
|
||||
assert!(!select_elements(&mut engine, unknown.clone(), true));
|
||||
let unknown_file = Selection::Waypoints {
|
||||
file_id: FileId::default(),
|
||||
};
|
||||
assert!(!select_elements(&mut engine, unknown_file.clone(), true));
|
||||
assert!(!select_elements(&mut engine, Selection::Empty, true));
|
||||
assert_eq!(engine.selection(), &node);
|
||||
|
||||
// and select nothing otherwise
|
||||
assert!(select_elements(&mut engine, unknown, false));
|
||||
assert_eq!(engine.selection(), &Selection::Empty);
|
||||
select_elements(&mut engine, node, false);
|
||||
assert!(select_elements(&mut engine, unknown_file, false));
|
||||
assert_eq!(engine.selection(), &Selection::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_selected_elements_are_dropped_when_they_disappear() {
|
||||
let mut engine = Engine::default();
|
||||
load(&mut engine, "data/with_tracks_and_segments.gpx");
|
||||
let file = engine
|
||||
.stack
|
||||
.current()
|
||||
.unwrap()
|
||||
.values()
|
||||
.next()
|
||||
.unwrap()
|
||||
.clone();
|
||||
let trk_id = file.trk[0].id;
|
||||
select_elements(
|
||||
&mut engine,
|
||||
Selection::Track {
|
||||
file_id: file.id,
|
||||
trk_ids: [trk_id].into(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
engine.execute(Action::Undo);
|
||||
assert_eq!(engine.selection(), &Selection::Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,24 +28,92 @@ pub enum Selection {
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
/// Drops the files that do not exist anymore (e.g. after an undo).
|
||||
/// 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: &StackEntry) {
|
||||
match self {
|
||||
Selection::Empty => {}
|
||||
let empty = match self {
|
||||
Selection::Empty => false,
|
||||
Selection::File { file_ids } => {
|
||||
file_ids.retain(|id| files.contains_key(id));
|
||||
if file_ids.is_empty() {
|
||||
*self = Selection::Empty;
|
||||
}
|
||||
file_ids.is_empty()
|
||||
}
|
||||
Selection::Track { file_id, .. }
|
||||
| Selection::TrackSegment { file_id, .. }
|
||||
| Selection::Waypoints { file_id }
|
||||
| Selection::Waypoint { file_id, .. } => {
|
||||
if !files.contains_key(file_id) {
|
||||
*self = Selection::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()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,7 +122,7 @@ impl Selection {
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::File;
|
||||
use crate::{File, Track, TrackSegment, Waypoint, WaypointChunk};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -86,4 +154,102 @@ mod tests {
|
||||
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(Rc::new(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].wpt[0].id);
|
||||
let mut files = StackEntry::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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user