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

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
use crate::{Apply, CommandError, Edit, Editor, File, State, Track, update_selected};
#[derive(Debug)]
pub struct Metadata<'a> {
pub name: &'a str,
pub desc: &'a str,
}
impl Apply for Metadata<'_> {
fn apply(mut self, state: &mut State) -> Result<(), CommandError> {
update_selected(state, &mut self);
Ok(())
}
}
impl Editor for Metadata<'_> {
fn file(&mut self, file: &mut File) -> Edit {
file.info.name = self.name.to_owned();
file.info.desc = Some(self.desc.to_owned());
// a single track is named like the file
if let [track] = file.trk.as_mut_slice() {
track.info.name = Some(self.name.to_owned());
}
Edit::Changed
}
fn track(&mut self, track: &mut Track) -> Edit {
track.info.name = Some(self.name.to_owned());
track.info.desc = Some(self.desc.to_owned());
Edit::Changed
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::{New, NewTrack, Selection, engine::command::fixture::Fixture};
use super::*;
#[test]
fn test_metadata_applies_to_selected_file() {
let mut fx = Fixture::default();
New {
name: "first",
trackpoint: None,
}
.apply(&mut fx.state())
.unwrap();
New {
name: "second",
trackpoint: None,
}
.apply(&mut fx.state())
.unwrap(); // now selected
let selected = *fx.selected_files().iter().next().unwrap();
Metadata {
name: "renamed",
desc: "description",
}
.apply(&mut fx.state())
.unwrap();
for (id, file) in fx.files.iter() {
if *id == selected {
assert_eq!(file.info.name, "renamed");
assert_eq!(file.info.desc.as_deref(), Some("description"));
} else {
assert_eq!(file.info.name, "first");
assert_eq!(file.info.desc, None);
}
}
}
#[test]
fn test_metadata_applies_to_selected_track() {
let mut fx = Fixture::default();
let data = std::fs::read("data/with_tracks.gpx").unwrap();
crate::Load {
data: &data,
name: "file",
}
.apply(&mut fx.state())
.unwrap();
let file = fx.files.values().next().unwrap().clone();
fx.selection = Selection::Track {
file_id: file.id,
trk_ids: HashSet::from([file.trk[0].id]),
};
Metadata {
name: "renamed",
desc: "",
}
.apply(&mut fx.state())
.unwrap();
let after = &fx.files[&file.id];
assert_eq!(after.info.name, file.info.name);
assert_eq!(after.trk[0].info.name.as_deref(), Some("renamed"));
assert_eq!(after.trk[1].info, file.trk[1].info);
}
/// A new file with `nb_tracks` tracks (without name), selected.
fn file_with_tracks(fx: &mut Fixture, nb_tracks: usize) {
New {
name: "file",
trackpoint: None,
}
.apply(&mut fx.state())
.unwrap();
for _ in 0..nb_tracks {
NewTrack.apply(&mut fx.state()).unwrap();
}
}
fn rename(fx: &mut Fixture) {
Metadata {
name: "renamed",
desc: "",
}
.apply(&mut fx.state())
.unwrap();
}
#[test]
fn test_single_track_is_renamed_like_the_file() {
let mut fx = Fixture::default();
file_with_tracks(&mut fx, 1);
rename(&mut fx);
let file = fx.files.values().next().unwrap();
assert_eq!(file.info.name, "renamed");
assert_eq!(file.trk[0].info.name.as_deref(), Some("renamed"));
}
#[test]
fn test_single_track_with_a_name_is_renamed_too() {
let mut fx = Fixture::default();
file_with_tracks(&mut fx, 1);
let id = *fx.files.keys().next().unwrap();
std::rc::Rc::make_mut(fx.files.get_mut(&id).unwrap()).trk[0]
.info
.name = Some("track".to_owned());
rename(&mut fx);
let file = &fx.files[&id];
assert_eq!(file.info.name, "renamed");
assert_eq!(file.trk[0].info.name.as_deref(), Some("renamed"));
}
#[test]
fn test_several_tracks_are_not_renamed() {
let mut fx = Fixture::default();
file_with_tracks(&mut fx, 2);
rename(&mut fx);
let file = fx.files.values().next().unwrap();
assert_eq!(file.info.name, "renamed");
assert!(file.trk.iter().all(|trk| trk.info.name.is_none()));
}
}
+9
View File
@@ -0,0 +1,9 @@
mod metadata;
mod new_track;
mod new_track_segment;
mod style;
pub use metadata::*;
pub use new_track::*;
pub use new_track_segment::*;
pub use style::*;
@@ -0,0 +1,75 @@
use crate::{Apply, CommandError, Edit, Editor, File, State, Track, update_selected};
#[derive(Debug)]
pub struct NewTrack;
impl Apply for NewTrack {
fn apply(mut self, state: &mut State) -> Result<(), CommandError> {
update_selected(state, &mut self);
Ok(())
}
}
impl Editor for NewTrack {
fn file(&mut self, file: &mut File) -> Edit {
let mut track = Track::default();
track.info.color = common(file, |t| &t.info.color);
track.info.opacity = common(file, |t| &t.info.opacity);
track.info.width = common(file, |t| &t.info.width);
file.trk.push(track);
Edit::Changed
}
}
/// Returns the value of a style attribute if all existing tracks agree on it.
fn common<T: Clone + PartialEq>(file: &File, get: impl Fn(&Track) -> &Option<T>) -> Option<T> {
let mut tracks = file.trk.iter();
let first = get(tracks.next()?);
tracks.all(|t| get(t) == first).then(|| first.clone())?
}
#[cfg(test)]
mod tests {
use crate::{New, engine::command::fixture::Fixture};
use super::*;
#[test]
fn test_new_track() {
let mut fx = Fixture::default();
New {
name: "file",
trackpoint: None,
}
.apply(&mut fx.state())
.unwrap();
NewTrack.apply(&mut fx.state()).unwrap();
NewTrack.apply(&mut fx.state()).unwrap();
let file = fx.files.values().next().unwrap();
assert_eq!(file.trk.len(), 2);
assert_ne!(file.trk[0].id, file.trk[1].id);
}
#[test]
fn test_new_track_inherits_agreeing_style_only() {
let mut fx = Fixture::default();
New {
name: "file",
trackpoint: None,
}
.apply(&mut fx.state())
.unwrap();
NewTrack.apply(&mut fx.state()).unwrap();
NewTrack.apply(&mut fx.state()).unwrap();
let file = std::rc::Rc::make_mut(fx.files.values_mut().next().unwrap());
for (i, trk) in file.trk.iter_mut().enumerate() {
trk.info.color = Some("ff0000".into());
trk.info.opacity = Some(0.5 + i as f64 * 0.1);
}
NewTrack.apply(&mut fx.state()).unwrap();
let info = &fx.files.values().next().unwrap().trk[2].info;
assert_eq!(info.color.as_deref(), Some("ff0000"));
assert_eq!(info.opacity, None);
assert_eq!(info.width, None);
}
}
@@ -0,0 +1,41 @@
use crate::{Apply, CommandError, Edit, Editor, State, Track, TrackSegment, update_selected};
#[derive(Debug)]
pub struct NewTrackSegment;
impl Apply for NewTrackSegment {
fn apply(mut self, state: &mut State) -> Result<(), CommandError> {
update_selected(state, &mut self);
Ok(())
}
}
impl Editor for NewTrackSegment {
fn track(&mut self, track: &mut Track) -> Edit {
track.trkseg.push(TrackSegment::default());
Edit::Changed
}
}
#[cfg(test)]
mod tests {
use crate::{New, NewTrack, engine::command::fixture::Fixture};
use super::*;
#[test]
fn test_new_track_segment_on_each_track_of_selected_file() {
let mut fx = Fixture::default();
New {
name: "file",
trackpoint: None,
}
.apply(&mut fx.state())
.unwrap();
NewTrack.apply(&mut fx.state()).unwrap();
NewTrack.apply(&mut fx.state()).unwrap();
NewTrackSegment.apply(&mut fx.state()).unwrap();
let file = fx.files.values().next().unwrap();
assert!(file.trk.iter().all(|t| t.trkseg.len() == 1));
}
}
+71
View File
@@ -0,0 +1,71 @@
use crate::{Apply, CommandError, Edit, Editor, State, Track, update_selected};
#[derive(Debug)]
pub struct Style<'a> {
pub color: Option<&'a str>,
pub opacity: Option<f64>,
pub width: Option<f64>,
}
impl Apply for Style<'_> {
fn apply(mut self, state: &mut State) -> Result<(), CommandError> {
update_selected(state, &mut self);
Ok(())
}
}
impl Editor for Style<'_> {
fn track(&mut self, track: &mut Track) -> Edit {
if let Some(color) = self.color {
track.info.color = Some(color.to_owned());
}
if self.opacity.is_some() {
track.info.opacity = self.opacity;
}
if self.width.is_some() {
track.info.width = self.width;
}
Edit::Changed
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::{Selection, engine::command::fixture::Fixture};
use super::*;
#[test]
fn test_style_applies_to_all_tracks_of_selected_file_and_keeps_unset_fields() {
let mut fx = Fixture::default();
let data = std::fs::read("data/with_style.gpx").unwrap();
crate::Load {
data: &data,
name: "file",
}
.apply(&mut fx.state())
.unwrap();
let id = *fx.files.keys().next().unwrap();
let before = fx.files[&id].clone();
assert!(!before.trk.is_empty());
fx.selection = Selection::File {
file_ids: HashSet::from([id]),
};
Style {
color: Some("ff0000"),
opacity: None,
width: Some(7.0),
}
.apply(&mut fx.state())
.unwrap();
for (b, a) in before.trk.iter().zip(fx.files[&id].trk.iter()) {
assert_eq!(a.info.color.as_deref(), Some("ff0000"));
assert_eq!(a.info.width, Some(7.0));
assert_eq!(a.info.opacity, b.info.opacity);
}
}
}