diff --git a/src/tui/app/selection/album.rs b/src/tui/app/selection/album.rs new file mode 100644 index 0000000..06cdb16 --- /dev/null +++ b/src/tui/app/selection/album.rs @@ -0,0 +1,306 @@ +use std::cmp; + +use musichoard::collection::{album::{Album, AlbumId}, track::Track}; + +use crate::tui::app::selection::{ + track::{IdSelectTrack, TrackSelection}, + Delta, SelectionState, WidgetState, +}; + +#[derive(Clone, Debug, PartialEq)] +pub struct AlbumSelection { + pub state: WidgetState, + pub track: TrackSelection, +} + +impl AlbumSelection { + pub fn initialise(albums: &[Album]) -> Self { + let mut selection = AlbumSelection { + state: WidgetState::default(), + track: TrackSelection::initialise(&[]), + }; + selection.reinitialise(albums, None); + selection + } + + pub fn reinitialise(&mut self, albums: &[Album], album: Option) { + if let Some(album) = album { + let result = albums.binary_search_by(|a| a.get_sort_key().cmp(&album.album_id)); + match result { + Ok(index) => self.reinitialise_with_index(albums, index, album.track), + Err(index) => self.reinitialise_with_index(albums, index, None), + } + } else { + self.reinitialise_with_index(albums, 0, None) + } + } + + fn reinitialise_with_index( + &mut self, + albums: &[Album], + index: usize, + active_track: Option, + ) { + if albums.is_empty() { + self.state.list.select(None); + self.track = TrackSelection::initialise(&[]); + } else if index >= albums.len() { + let end = albums.len() - 1; + self.state.list.select(Some(end)); + self.track = TrackSelection::initialise(&albums[end].tracks); + } else { + self.state.list.select(Some(index)); + self.track.reinitialise(&albums[index].tracks, active_track); + } + } + + pub fn selected(&self) -> Option { + self.state.list.selected() + } + + pub fn selected_track(&self) -> Option { + self.track.selected() + } + + pub fn select(&mut self, albums: &[Album], to: Option) { + match to { + Some(to) => self.select_to(albums, to), + None => self.state.list.select(None), + } + } + + pub fn select_track(&mut self, albums: &[Album], to: Option) { + if let Some(index) = self.state.list.selected() { + self.track.select(&albums[index].tracks, to); + } + } + + fn select_to(&mut self, albums: &[Album], mut to: usize) { + to = cmp::min(to, albums.len() - 1); + if self.state.list.selected() != Some(to) { + self.state.list.select(Some(to)); + self.track = TrackSelection::initialise(&albums[to].tracks); + } + } + + pub fn selection_state<'a>(&self, list: &'a [Album]) -> Option> { + let selected = self.state.list.selected(); + selected.map(|index| SelectionState { list, index }) + } + + pub fn state_tracks<'a>(&self, albums: &'a [Album]) -> Option> { + let selected = self.state.list.selected(); + selected.and_then(|index| self.track.selection_state(&albums[index].tracks)) + } + + pub fn reset(&mut self, albums: &[Album]) { + if self.state.list.selected() != Some(0) { + self.reinitialise(albums, None); + } + } + + pub fn reset_track(&mut self, albums: &[Album]) { + if let Some(index) = self.state.list.selected() { + self.track.reset(&albums[index].tracks); + } + } + + pub fn increment(&mut self, albums: &[Album], delta: Delta) { + self.increment_by(albums, delta.as_usize(&self.state)); + } + + pub fn increment_track(&mut self, albums: &[Album], delta: Delta) { + if let Some(index) = self.state.list.selected() { + self.track.increment(&albums[index].tracks, delta); + } + } + + fn increment_by(&mut self, albums: &[Album], by: usize) { + if let Some(index) = self.state.list.selected() { + let mut result = index.saturating_add(by); + if result >= albums.len() { + result = albums.len() - 1; + } + if self.state.list.selected() != Some(result) { + self.state.list.select(Some(result)); + self.track = TrackSelection::initialise(&albums[result].tracks); + } + } + } + + pub fn decrement(&mut self, albums: &[Album], delta: Delta) { + self.decrement_by(albums, delta.as_usize(&self.state)); + } + + pub fn decrement_track(&mut self, albums: &[Album], delta: Delta) { + if let Some(index) = self.state.list.selected() { + self.track.decrement(&albums[index].tracks, delta); + } + } + + fn decrement_by(&mut self, albums: &[Album], by: usize) { + if let Some(index) = self.state.list.selected() { + let result = index.saturating_sub(by); + if self.state.list.selected() != Some(result) { + self.state.list.select(Some(result)); + self.track = TrackSelection::initialise(&albums[result].tracks); + } + } + } +} + +pub struct IdSelectAlbum { + album_id: AlbumId, + track: Option, +} + +impl IdSelectAlbum { + pub fn get(albums: &[Album], selection: &AlbumSelection) -> Option { + selection.state.list.selected().map(|index| { + let album = &albums[index]; + IdSelectAlbum { + album_id: album.get_sort_key().clone(), + track: IdSelectTrack::get(&album.tracks, &selection.track), + } + }) + } +} + +#[cfg(test)] +mod tests { + use crate::tui::testmod::COLLECTION; + + use super::*; + + + #[test] + fn album_selection() { + let albums = &COLLECTION[0].albums; + assert!(albums.len() > 1); + + let mut empty = AlbumSelection::initialise(&[]); + assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.track.state.list.selected(), None); + + empty.increment(albums, Delta::Line); + assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.track.state.list.selected(), None); + + empty.decrement(albums, Delta::Line); + assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.track.state.list.selected(), None); + + let mut sel = AlbumSelection::initialise(albums); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.track.state.list.selected(), Some(0)); + + sel.increment_track(albums, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.track.state.list.selected(), Some(1)); + + // Verify that decrement that doesn't change index does not reset track. + sel.decrement(albums, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.track.state.list.selected(), Some(1)); + + sel.increment(albums, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(1)); + assert_eq!(sel.track.state.list.selected(), Some(0)); + + sel.decrement(albums, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.track.state.list.selected(), Some(0)); + + for _ in 0..(albums.len() + 5) { + sel.increment(albums, Delta::Line); + } + assert_eq!(sel.state.list.selected(), Some(albums.len() - 1)); + assert_eq!(sel.track.state.list.selected(), Some(0)); + + sel.increment_track(albums, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(albums.len() - 1)); + assert_eq!(sel.track.state.list.selected(), Some(1)); + + // Verify that increment that doesn't change index does not reset track. + sel.increment(albums, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(albums.len() - 1)); + assert_eq!(sel.track.state.list.selected(), Some(1)); + } + + #[test] + fn album_delta_page() { + let albums = &COLLECTION[1].albums; + assert!(albums.len() > 1); + + let empty = AlbumSelection::initialise(&[]); + assert_eq!(empty.state.list.selected(), None); + + let mut sel = AlbumSelection::initialise(albums); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.track.state.list.selected(), Some(0)); + + assert!(albums.len() >= 4); + sel.state.height = 3; + + sel.increment_track(albums, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.track.state.list.selected(), Some(1)); + + // Verify that decrement that doesn't change index does not reset track. + sel.decrement(albums, Delta::Page); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.track.state.list.selected(), Some(1)); + + sel.increment(albums, Delta::Page); + assert_eq!(sel.state.list.selected(), Some(2)); + assert_eq!(sel.track.state.list.selected(), Some(0)); + + sel.decrement(albums, Delta::Page); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.track.state.list.selected(), Some(0)); + + for _ in 0..(albums.len() + 5) { + sel.increment(albums, Delta::Page); + } + assert_eq!(sel.state.list.selected(), Some(albums.len() - 1)); + assert_eq!(sel.track.state.list.selected(), Some(0)); + + sel.increment_track(albums, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(albums.len() - 1)); + assert_eq!(sel.track.state.list.selected(), Some(1)); + + // Verify that increment that doesn't change index does not reset track. + sel.increment(albums, Delta::Page); + assert_eq!(sel.state.list.selected(), Some(albums.len() - 1)); + assert_eq!(sel.track.state.list.selected(), Some(1)); + } + + #[test] + fn album_reinitialise() { + let albums = &COLLECTION[0].albums; + assert!(albums.len() > 1); + + let mut sel = AlbumSelection::initialise(albums); + sel.state.list.select(Some(albums.len() - 1)); + sel.track.state.list.select(Some(1)); + + // Re-initialise. + let expected = sel.clone(); + let active_album = IdSelectAlbum::get(albums, &sel); + sel.reinitialise(albums, active_album); + assert_eq!(sel, expected); + + // Re-initialise out-of-bounds. + let mut expected = sel.clone(); + expected.decrement(albums, Delta::Line); + let active_album = IdSelectAlbum::get(albums, &sel); + sel.reinitialise(&albums[..(albums.len() - 1)], active_album); + assert_eq!(sel, expected); + + // Re-initialise empty. + let expected = AlbumSelection::initialise(&[]); + let active_album = IdSelectAlbum::get(albums, &sel); + sel.reinitialise(&[], active_album); + assert_eq!(sel, expected); + } +} diff --git a/src/tui/app/selection/artist.rs b/src/tui/app/selection/artist.rs new file mode 100644 index 0000000..02547b9 --- /dev/null +++ b/src/tui/app/selection/artist.rs @@ -0,0 +1,338 @@ +use std::cmp; + +use musichoard::collection::{ + album::Album, + artist::{Artist, ArtistId}, + track::Track, +}; + +use crate::tui::app::selection::{ + album::{AlbumSelection, IdSelectAlbum}, + Delta, SelectionState, WidgetState, +}; + +#[derive(Clone, Debug, PartialEq)] +pub struct ArtistSelection { + pub state: WidgetState, + pub album: AlbumSelection, +} + +impl ArtistSelection { + pub fn initialise(artists: &[Artist]) -> Self { + let mut selection = ArtistSelection { + state: WidgetState::default(), + album: AlbumSelection::initialise(&[]), + }; + selection.reinitialise(artists, None); + selection + } + + pub fn reinitialise(&mut self, artists: &[Artist], active: Option) { + if let Some(active) = active { + let result = artists.binary_search_by(|a| a.get_sort_key().cmp(&active.artist_id)); + match result { + Ok(index) => self.reinitialise_with_index(artists, index, active.album), + Err(index) => self.reinitialise_with_index(artists, index, None), + } + } else { + self.reinitialise_with_index(artists, 0, None) + } + } + + fn reinitialise_with_index( + &mut self, + artists: &[Artist], + index: usize, + active_album: Option, + ) { + if artists.is_empty() { + self.state.list.select(None); + self.album = AlbumSelection::initialise(&[]); + } else if index >= artists.len() { + let end = artists.len() - 1; + self.state.list.select(Some(end)); + self.album = AlbumSelection::initialise(&artists[end].albums); + } else { + self.state.list.select(Some(index)); + self.album + .reinitialise(&artists[index].albums, active_album); + } + } + + pub fn selected(&self) -> Option { + self.state.list.selected() + } + + pub fn selected_album(&self) -> Option { + self.album.selected() + } + + pub fn selected_track(&self) -> Option { + self.album.selected_track() + } + + pub fn select(&mut self, artists: &[Artist], to: Option) { + match to { + Some(to) => self.select_to(artists, to), + None => self.state.list.select(None), + } + } + + pub fn select_album(&mut self, artists: &[Artist], to: Option) { + if let Some(index) = self.state.list.selected() { + self.album.select(&artists[index].albums, to); + } + } + + pub fn select_track(&mut self, artists: &[Artist], to: Option) { + if let Some(index) = self.state.list.selected() { + self.album.select_track(&artists[index].albums, to); + } + } + + fn select_to(&mut self, artists: &[Artist], mut to: usize) { + to = cmp::min(to, artists.len() - 1); + if self.state.list.selected() != Some(to) { + self.state.list.select(Some(to)); + self.album = AlbumSelection::initialise(&artists[to].albums); + } + } + + pub fn selection_state<'a>(&self, list: &'a [Artist]) -> Option> { + let selected = self.state.list.selected(); + selected.map(|index| SelectionState { list, index }) + } + + pub fn state_album<'a>(&self, artists: &'a [Artist]) -> Option> { + let selected = self.state.list.selected(); + selected.and_then(|index| self.album.selection_state(&artists[index].albums)) + } + + pub fn state_track<'a>(&self, artists: &'a [Artist]) -> Option> { + let selected = self.state.list.selected(); + selected.and_then(|index| self.album.state_tracks(&artists[index].albums)) + } + + pub fn reset(&mut self, artists: &[Artist]) { + if self.state.list.selected() != Some(0) { + self.reinitialise(artists, None); + } + } + + pub fn reset_album(&mut self, artists: &[Artist]) { + if let Some(index) = self.state.list.selected() { + self.album.reset(&artists[index].albums); + } + } + + pub fn reset_track(&mut self, artists: &[Artist]) { + if let Some(index) = self.state.list.selected() { + self.album.reset_track(&artists[index].albums); + } + } + + pub fn increment(&mut self, artists: &[Artist], delta: Delta) { + self.increment_by(artists, delta.as_usize(&self.state)); + } + + pub fn increment_album(&mut self, artists: &[Artist], delta: Delta) { + if let Some(index) = self.state.list.selected() { + self.album.increment(&artists[index].albums, delta); + } + } + + pub fn increment_track(&mut self, artists: &[Artist], delta: Delta) { + if let Some(index) = self.state.list.selected() { + self.album.increment_track(&artists[index].albums, delta); + } + } + + fn increment_by(&mut self, artists: &[Artist], by: usize) { + if let Some(index) = self.state.list.selected() { + let result = index.saturating_add(by); + self.select_to(artists, result); + } + } + + pub fn decrement(&mut self, artists: &[Artist], delta: Delta) { + self.decrement_by(artists, delta.as_usize(&self.state)); + } + + pub fn decrement_album(&mut self, artists: &[Artist], delta: Delta) { + if let Some(index) = self.state.list.selected() { + self.album.decrement(&artists[index].albums, delta); + } + } + + pub fn decrement_track(&mut self, artists: &[Artist], delta: Delta) { + if let Some(index) = self.state.list.selected() { + self.album.decrement_track(&artists[index].albums, delta); + } + } + + fn decrement_by(&mut self, artists: &[Artist], by: usize) { + if let Some(index) = self.state.list.selected() { + let result = index.saturating_sub(by); + if self.state.list.selected() != Some(result) { + self.state.list.select(Some(result)); + self.album = AlbumSelection::initialise(&artists[result].albums); + } + } + } +} + +pub struct IdSelectArtist { + artist_id: ArtistId, + album: Option, +} + +impl IdSelectArtist { + pub fn get(artists: &[Artist], selection: &ArtistSelection) -> Option { + selection.state.list.selected().map(|index| { + let artist = &artists[index]; + IdSelectArtist { + artist_id: artist.get_sort_key().clone(), + album: IdSelectAlbum::get(&artist.albums, &selection.album), + } + }) + } +} + +#[cfg(test)] +mod tests { + use crate::tui::testmod::COLLECTION; + + use super::*; + + + #[test] + fn artist_selection() { + let artists = &COLLECTION; + assert!(artists.len() > 1); + + let mut empty = ArtistSelection::initialise(&[]); + assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.album.state.list.selected(), None); + + empty.increment(artists, Delta::Line); + assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.album.state.list.selected(), None); + + empty.decrement(artists, Delta::Line); + assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.album.state.list.selected(), None); + + let mut sel = ArtistSelection::initialise(artists); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.album.state.list.selected(), Some(0)); + + sel.increment_album(artists, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.album.state.list.selected(), Some(1)); + + // Verify that decrement that doesn't change index does not reset album. + sel.decrement(artists, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.album.state.list.selected(), Some(1)); + + sel.increment(artists, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(1)); + assert_eq!(sel.album.state.list.selected(), Some(0)); + + sel.decrement(artists, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.album.state.list.selected(), Some(0)); + + for _ in 0..(artists.len() + 5) { + sel.increment(artists, Delta::Line); + } + assert_eq!(sel.state.list.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.state.list.selected(), Some(0)); + + sel.increment_album(artists, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.state.list.selected(), Some(1)); + + // Verify that increment that doesn't change index does not reset album. + sel.increment(artists, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.state.list.selected(), Some(1)); + } + + #[test] + fn artist_delta_page() { + let artists = &COLLECTION; + assert!(artists.len() > 1); + + let empty = ArtistSelection::initialise(&[]); + assert_eq!(empty.state.list.selected(), None); + + let mut sel = ArtistSelection::initialise(artists); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.album.state.list.selected(), Some(0)); + + assert!(artists.len() >= 4); + sel.state.height = 3; + + sel.increment_album(artists, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.album.state.list.selected(), Some(1)); + + // Verify that decrement that doesn't change index does not reset album. + sel.decrement(artists, Delta::Page); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.album.state.list.selected(), Some(1)); + + sel.increment(artists, Delta::Page); + assert_eq!(sel.state.list.selected(), Some(2)); + assert_eq!(sel.album.state.list.selected(), Some(0)); + + sel.decrement(artists, Delta::Page); + assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.album.state.list.selected(), Some(0)); + + for _ in 0..(artists.len() + 5) { + sel.increment(artists, Delta::Page); + } + assert_eq!(sel.state.list.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.state.list.selected(), Some(0)); + + sel.increment_album(artists, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.state.list.selected(), Some(1)); + + // Verify that increment that doesn't change index does not reset album. + sel.increment(artists, Delta::Page); + assert_eq!(sel.state.list.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.state.list.selected(), Some(1)); + } + + #[test] + fn artist_reinitialise() { + let artists = &COLLECTION; + assert!(artists.len() > 1); + + let mut sel = ArtistSelection::initialise(artists); + sel.state.list.select(Some(artists.len() - 1)); + sel.album.state.list.select(Some(1)); + + // Re-initialise. + let expected = sel.clone(); + let active_artist = IdSelectArtist::get(artists, &sel); + sel.reinitialise(artists, active_artist); + assert_eq!(sel, expected); + + // Re-initialise out-of-bounds. + let mut expected = sel.clone(); + expected.decrement(artists, Delta::Line); + let active_artist = IdSelectArtist::get(artists, &sel); + sel.reinitialise(&artists[..(artists.len() - 1)], active_artist); + assert_eq!(sel, expected); + + // Re-initialise empty. + let expected = ArtistSelection::initialise(&[]); + let active_artist = IdSelectArtist::get(artists, &sel); + sel.reinitialise(&[], active_artist); + assert_eq!(sel, expected); + } +} diff --git a/src/tui/app/selection/mod.rs b/src/tui/app/selection/mod.rs index 6ffd615..27c8c2c 100644 --- a/src/tui/app/selection/mod.rs +++ b/src/tui/app/selection/mod.rs @@ -1,11 +1,11 @@ -use musichoard::collection::{ - album::{Album, AlbumId}, - artist::{Artist, ArtistId}, - track::{Track, TrackId}, - Collection, -}; +mod album; +mod artist; +mod track; + +use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection}; use ratatui::widgets::ListState; -use std::cmp; + +use artist::{ArtistSelection, IdSelectArtist}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Category { @@ -31,21 +31,9 @@ pub struct Selection { pub artist: ArtistSelection, } -#[derive(Clone, Debug, PartialEq)] -pub struct ArtistSelection { - pub state: WidgetState, - pub album: AlbumSelection, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct AlbumSelection { - pub state: WidgetState, - pub track: TrackSelection, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct TrackSelection { - pub state: WidgetState, +pub struct SelectionState<'a, T> { + pub list: &'a [T], + pub index: usize, } pub enum Delta { @@ -62,11 +50,6 @@ impl Delta { } } -pub struct SelectionState<'a, T> { - pub list: &'a [T], - pub index: usize, -} - impl Selection { pub fn new(artists: &[Artist]) -> Self { Selection { @@ -214,393 +197,6 @@ impl Selection { } } -impl ArtistSelection { - fn initialise(artists: &[Artist]) -> Self { - let mut selection = ArtistSelection { - state: WidgetState::default(), - album: AlbumSelection::initialise(&[]), - }; - selection.reinitialise(artists, None); - selection - } - - fn reinitialise(&mut self, artists: &[Artist], active: Option) { - if let Some(active) = active { - let result = artists.binary_search_by(|a| a.get_sort_key().cmp(&active.artist_id)); - match result { - Ok(index) => self.reinitialise_with_index(artists, index, active.album), - Err(index) => self.reinitialise_with_index(artists, index, None), - } - } else { - self.reinitialise_with_index(artists, 0, None) - } - } - - fn reinitialise_with_index( - &mut self, - artists: &[Artist], - index: usize, - active_album: Option, - ) { - if artists.is_empty() { - self.state.list.select(None); - self.album = AlbumSelection::initialise(&[]); - } else if index >= artists.len() { - let end = artists.len() - 1; - self.state.list.select(Some(end)); - self.album = AlbumSelection::initialise(&artists[end].albums); - } else { - self.state.list.select(Some(index)); - self.album - .reinitialise(&artists[index].albums, active_album); - } - } - - fn selected(&self) -> Option { - self.state.list.selected() - } - - fn selected_album(&self) -> Option { - self.album.selected() - } - - fn selected_track(&self) -> Option { - self.album.selected_track() - } - - fn select(&mut self, artists: &[Artist], to: Option) { - match to { - Some(to) => self.select_to(artists, to), - None => self.state.list.select(None), - } - } - - fn select_album(&mut self, artists: &[Artist], to: Option) { - if let Some(index) = self.state.list.selected() { - self.album.select(&artists[index].albums, to); - } - } - - fn select_track(&mut self, artists: &[Artist], to: Option) { - if let Some(index) = self.state.list.selected() { - self.album.select_track(&artists[index].albums, to); - } - } - - fn select_to(&mut self, artists: &[Artist], mut to: usize) { - to = cmp::min(to, artists.len() - 1); - if self.state.list.selected() != Some(to) { - self.state.list.select(Some(to)); - self.album = AlbumSelection::initialise(&artists[to].albums); - } - } - - fn selection_state<'a>(&self, list: &'a [Artist]) -> Option> { - let selected = self.state.list.selected(); - selected.map(|index| SelectionState { list, index }) - } - - fn state_album<'a>(&self, artists: &'a [Artist]) -> Option> { - let selected = self.state.list.selected(); - selected.and_then(|index| self.album.selection_state(&artists[index].albums)) - } - - fn state_track<'a>(&self, artists: &'a [Artist]) -> Option> { - let selected = self.state.list.selected(); - selected.and_then(|index| self.album.state_tracks(&artists[index].albums)) - } - - fn reset(&mut self, artists: &[Artist]) { - if self.state.list.selected() != Some(0) { - self.reinitialise(artists, None); - } - } - - fn reset_album(&mut self, artists: &[Artist]) { - if let Some(index) = self.state.list.selected() { - self.album.reset(&artists[index].albums); - } - } - - fn reset_track(&mut self, artists: &[Artist]) { - if let Some(index) = self.state.list.selected() { - self.album.reset_track(&artists[index].albums); - } - } - - fn increment_by(&mut self, artists: &[Artist], by: usize) { - if let Some(index) = self.state.list.selected() { - let result = index.saturating_add(by); - self.select_to(artists, result); - } - } - - fn increment(&mut self, artists: &[Artist], delta: Delta) { - self.increment_by(artists, delta.as_usize(&self.state)); - } - - fn increment_album(&mut self, artists: &[Artist], delta: Delta) { - if let Some(index) = self.state.list.selected() { - self.album.increment(&artists[index].albums, delta); - } - } - - fn increment_track(&mut self, artists: &[Artist], delta: Delta) { - if let Some(index) = self.state.list.selected() { - self.album.increment_track(&artists[index].albums, delta); - } - } - - fn decrement_by(&mut self, artists: &[Artist], by: usize) { - if let Some(index) = self.state.list.selected() { - let result = index.saturating_sub(by); - if self.state.list.selected() != Some(result) { - self.state.list.select(Some(result)); - self.album = AlbumSelection::initialise(&artists[result].albums); - } - } - } - - fn decrement(&mut self, artists: &[Artist], delta: Delta) { - self.decrement_by(artists, delta.as_usize(&self.state)); - } - - fn decrement_album(&mut self, artists: &[Artist], delta: Delta) { - if let Some(index) = self.state.list.selected() { - self.album.decrement(&artists[index].albums, delta); - } - } - - fn decrement_track(&mut self, artists: &[Artist], delta: Delta) { - if let Some(index) = self.state.list.selected() { - self.album.decrement_track(&artists[index].albums, delta); - } - } -} - -impl AlbumSelection { - fn initialise(albums: &[Album]) -> Self { - let mut selection = AlbumSelection { - state: WidgetState::default(), - track: TrackSelection::initialise(&[]), - }; - selection.reinitialise(albums, None); - selection - } - - fn reinitialise(&mut self, albums: &[Album], album: Option) { - if let Some(album) = album { - let result = albums.binary_search_by(|a| a.get_sort_key().cmp(&album.album_id)); - match result { - Ok(index) => self.reinitialise_with_index(albums, index, album.track), - Err(index) => self.reinitialise_with_index(albums, index, None), - } - } else { - self.reinitialise_with_index(albums, 0, None) - } - } - - fn reinitialise_with_index( - &mut self, - albums: &[Album], - index: usize, - active_track: Option, - ) { - if albums.is_empty() { - self.state.list.select(None); - self.track = TrackSelection::initialise(&[]); - } else if index >= albums.len() { - let end = albums.len() - 1; - self.state.list.select(Some(end)); - self.track = TrackSelection::initialise(&albums[end].tracks); - } else { - self.state.list.select(Some(index)); - self.track.reinitialise(&albums[index].tracks, active_track); - } - } - - fn selected(&self) -> Option { - self.state.list.selected() - } - - fn selected_track(&self) -> Option { - self.track.selected() - } - - fn select(&mut self, albums: &[Album], to: Option) { - match to { - Some(to) => self.select_to(albums, to), - None => self.state.list.select(None), - } - } - - fn select_track(&mut self, albums: &[Album], to: Option) { - if let Some(index) = self.state.list.selected() { - self.track.select(&albums[index].tracks, to); - } - } - - fn select_to(&mut self, albums: &[Album], mut to: usize) { - to = cmp::min(to, albums.len() - 1); - if self.state.list.selected() != Some(to) { - self.state.list.select(Some(to)); - self.track = TrackSelection::initialise(&albums[to].tracks); - } - } - - fn selection_state<'a>(&self, list: &'a [Album]) -> Option> { - let selected = self.state.list.selected(); - selected.map(|index| SelectionState { list, index }) - } - - fn state_tracks<'a>(&self, albums: &'a [Album]) -> Option> { - let selected = self.state.list.selected(); - selected.and_then(|index| self.track.selection_state(&albums[index].tracks)) - } - - fn reset(&mut self, albums: &[Album]) { - if self.state.list.selected() != Some(0) { - self.reinitialise(albums, None); - } - } - - fn reset_track(&mut self, albums: &[Album]) { - if let Some(index) = self.state.list.selected() { - self.track.reset(&albums[index].tracks); - } - } - - fn increment_by(&mut self, albums: &[Album], by: usize) { - if let Some(index) = self.state.list.selected() { - let mut result = index.saturating_add(by); - if result >= albums.len() { - result = albums.len() - 1; - } - if self.state.list.selected() != Some(result) { - self.state.list.select(Some(result)); - self.track = TrackSelection::initialise(&albums[result].tracks); - } - } - } - - fn increment(&mut self, albums: &[Album], delta: Delta) { - self.increment_by(albums, delta.as_usize(&self.state)); - } - - fn increment_track(&mut self, albums: &[Album], delta: Delta) { - if let Some(index) = self.state.list.selected() { - self.track.increment(&albums[index].tracks, delta); - } - } - - fn decrement_by(&mut self, albums: &[Album], by: usize) { - if let Some(index) = self.state.list.selected() { - let result = index.saturating_sub(by); - if self.state.list.selected() != Some(result) { - self.state.list.select(Some(result)); - self.track = TrackSelection::initialise(&albums[result].tracks); - } - } - } - - fn decrement(&mut self, albums: &[Album], delta: Delta) { - self.decrement_by(albums, delta.as_usize(&self.state)); - } - - fn decrement_track(&mut self, albums: &[Album], delta: Delta) { - if let Some(index) = self.state.list.selected() { - self.track.decrement(&albums[index].tracks, delta); - } - } -} - -impl TrackSelection { - fn initialise(tracks: &[Track]) -> Self { - let mut selection = TrackSelection { - state: WidgetState::default(), - }; - selection.reinitialise(tracks, None); - selection - } - - fn reinitialise(&mut self, tracks: &[Track], track: Option) { - if let Some(track) = track { - let result = tracks.binary_search_by(|t| t.get_sort_key().cmp(&track.track_id)); - match result { - Ok(index) | Err(index) => self.reinitialise_with_index(tracks, index), - } - } else { - self.reinitialise_with_index(tracks, 0) - } - } - - fn reinitialise_with_index(&mut self, tracks: &[Track], index: usize) { - if tracks.is_empty() { - self.state.list.select(None); - } else if index >= tracks.len() { - self.state.list.select(Some(tracks.len() - 1)); - } else { - self.state.list.select(Some(index)); - } - } - - fn selected(&self) -> Option { - self.state.list.selected() - } - - fn select(&mut self, tracks: &[Track], to: Option) { - match to { - Some(to) => self.select_to(tracks, to), - None => self.state.list.select(None), - } - } - - fn select_to(&mut self, tracks: &[Track], mut to: usize) { - to = cmp::min(to, tracks.len() - 1); - self.state.list.select(Some(to)); - } - - fn selection_state<'a>(&self, list: &'a [Track]) -> Option> { - let selected = self.state.list.selected(); - selected.map(|index| SelectionState { list, index }) - } - - fn reset(&mut self, tracks: &[Track]) { - if self.state.list.selected() != Some(0) { - self.reinitialise(tracks, None); - } - } - - fn increment_by(&mut self, tracks: &[Track], by: usize) { - if let Some(index) = self.state.list.selected() { - let mut result = index.saturating_add(by); - if result >= tracks.len() { - result = tracks.len() - 1; - } - if self.state.list.selected() != Some(result) { - self.state.list.select(Some(result)); - } - } - } - - fn increment(&mut self, tracks: &[Track], delta: Delta) { - self.increment_by(tracks, delta.as_usize(&self.state)); - } - - fn decrement_by(&mut self, _tracks: &[Track], by: usize) { - if let Some(index) = self.state.list.selected() { - let result = index.saturating_sub(by); - if self.state.list.selected() != Some(result) { - self.state.list.select(Some(result)); - } - } - } - - fn decrement(&mut self, tracks: &[Track], delta: Delta) { - self.decrement_by(tracks, delta.as_usize(&self.state)); - } -} - pub struct ListSelection { pub artist: ListState, pub album: ListState, @@ -621,20 +217,6 @@ pub struct IdSelection { artist: Option, } -struct IdSelectArtist { - artist_id: ArtistId, - album: Option, -} - -struct IdSelectAlbum { - album_id: AlbumId, - track: Option, -} - -struct IdSelectTrack { - track_id: TrackId, -} - impl IdSelection { pub fn get(collection: &Collection, selection: &Selection) -> Self { IdSelection { @@ -643,398 +225,12 @@ impl IdSelection { } } -impl IdSelectArtist { - fn get(artists: &[Artist], selection: &ArtistSelection) -> Option { - selection.state.list.selected().map(|index| { - let artist = &artists[index]; - IdSelectArtist { - artist_id: artist.get_sort_key().clone(), - album: IdSelectAlbum::get(&artist.albums, &selection.album), - } - }) - } -} - -impl IdSelectAlbum { - fn get(albums: &[Album], selection: &AlbumSelection) -> Option { - selection.state.list.selected().map(|index| { - let album = &albums[index]; - IdSelectAlbum { - album_id: album.get_sort_key().clone(), - track: IdSelectTrack::get(&album.tracks, &selection.track), - } - }) - } -} - -impl IdSelectTrack { - fn get(tracks: &[Track], selection: &TrackSelection) -> Option { - selection.state.list.selected().map(|index| { - let track = &tracks[index]; - IdSelectTrack { - track_id: track.get_sort_key().clone(), - } - }) - } -} - #[cfg(test)] mod tests { use crate::tui::testmod::COLLECTION; use super::*; - #[test] - fn track_selection() { - let tracks = &COLLECTION[0].albums[0].tracks; - assert!(tracks.len() > 1); - - let mut empty = TrackSelection::initialise(&[]); - assert_eq!(empty.state.list.selected(), None); - - empty.increment(tracks, Delta::Line); - assert_eq!(empty.state.list.selected(), None); - - empty.decrement(tracks, Delta::Line); - assert_eq!(empty.state.list.selected(), None); - - let mut sel = TrackSelection::initialise(tracks); - assert_eq!(sel.state.list.selected(), Some(0)); - - sel.decrement(tracks, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(0)); - - sel.increment(tracks, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(1)); - - sel.decrement(tracks, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(0)); - - for _ in 0..(tracks.len() + 5) { - sel.increment(tracks, Delta::Line); - } - assert_eq!(sel.state.list.selected(), Some(tracks.len() - 1)); - } - - #[test] - fn track_delta_page() { - let tracks = &COLLECTION[0].albums[0].tracks; - assert!(tracks.len() > 1); - - let empty = TrackSelection::initialise(&[]); - assert_eq!(empty.state.list.selected(), None); - - let mut sel = TrackSelection::initialise(tracks); - assert_eq!(sel.state.list.selected(), Some(0)); - - assert!(tracks.len() >= 4); - sel.state.height = 3; - - sel.decrement(tracks, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(0)); - - sel.increment(tracks, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(2)); - - sel.decrement(tracks, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(0)); - - for _ in 0..(tracks.len() + 5) { - sel.increment(tracks, Delta::Page); - } - assert_eq!(sel.state.list.selected(), Some(tracks.len() - 1)); - } - - #[test] - fn track_reinitialise() { - let tracks = &COLLECTION[0].albums[0].tracks; - assert!(tracks.len() > 1); - - let mut sel = TrackSelection::initialise(tracks); - sel.state.list.select(Some(tracks.len() - 1)); - - // Re-initialise. - let expected = sel.clone(); - let active_track = IdSelectTrack::get(tracks, &sel); - sel.reinitialise(tracks, active_track); - assert_eq!(sel, expected); - - // Re-initialise out-of-bounds. - let mut expected = sel.clone(); - expected.decrement(tracks, Delta::Line); - let active_track = IdSelectTrack::get(tracks, &sel); - sel.reinitialise(&tracks[..(tracks.len() - 1)], active_track); - assert_eq!(sel, expected); - - // Re-initialise empty. - let expected = TrackSelection::initialise(&[]); - let active_track = IdSelectTrack::get(tracks, &sel); - sel.reinitialise(&[], active_track); - assert_eq!(sel, expected); - } - - #[test] - fn album_selection() { - let albums = &COLLECTION[0].albums; - assert!(albums.len() > 1); - - let mut empty = AlbumSelection::initialise(&[]); - assert_eq!(empty.state.list.selected(), None); - assert_eq!(empty.track.state.list.selected(), None); - - empty.increment(albums, Delta::Line); - assert_eq!(empty.state.list.selected(), None); - assert_eq!(empty.track.state.list.selected(), None); - - empty.decrement(albums, Delta::Line); - assert_eq!(empty.state.list.selected(), None); - assert_eq!(empty.track.state.list.selected(), None); - - let mut sel = AlbumSelection::initialise(albums); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.track.state.list.selected(), Some(0)); - - sel.increment_track(albums, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.track.state.list.selected(), Some(1)); - - // Verify that decrement that doesn't change index does not reset track. - sel.decrement(albums, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.track.state.list.selected(), Some(1)); - - sel.increment(albums, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(1)); - assert_eq!(sel.track.state.list.selected(), Some(0)); - - sel.decrement(albums, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.track.state.list.selected(), Some(0)); - - for _ in 0..(albums.len() + 5) { - sel.increment(albums, Delta::Line); - } - assert_eq!(sel.state.list.selected(), Some(albums.len() - 1)); - assert_eq!(sel.track.state.list.selected(), Some(0)); - - sel.increment_track(albums, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(albums.len() - 1)); - assert_eq!(sel.track.state.list.selected(), Some(1)); - - // Verify that increment that doesn't change index does not reset track. - sel.increment(albums, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(albums.len() - 1)); - assert_eq!(sel.track.state.list.selected(), Some(1)); - } - - #[test] - fn album_delta_page() { - let albums = &COLLECTION[1].albums; - assert!(albums.len() > 1); - - let empty = AlbumSelection::initialise(&[]); - assert_eq!(empty.state.list.selected(), None); - - let mut sel = AlbumSelection::initialise(albums); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.track.state.list.selected(), Some(0)); - - assert!(albums.len() >= 4); - sel.state.height = 3; - - sel.increment_track(albums, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.track.state.list.selected(), Some(1)); - - // Verify that decrement that doesn't change index does not reset track. - sel.decrement(albums, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.track.state.list.selected(), Some(1)); - - sel.increment(albums, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(2)); - assert_eq!(sel.track.state.list.selected(), Some(0)); - - sel.decrement(albums, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.track.state.list.selected(), Some(0)); - - for _ in 0..(albums.len() + 5) { - sel.increment(albums, Delta::Page); - } - assert_eq!(sel.state.list.selected(), Some(albums.len() - 1)); - assert_eq!(sel.track.state.list.selected(), Some(0)); - - sel.increment_track(albums, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(albums.len() - 1)); - assert_eq!(sel.track.state.list.selected(), Some(1)); - - // Verify that increment that doesn't change index does not reset track. - sel.increment(albums, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(albums.len() - 1)); - assert_eq!(sel.track.state.list.selected(), Some(1)); - } - - #[test] - fn album_reinitialise() { - let albums = &COLLECTION[0].albums; - assert!(albums.len() > 1); - - let mut sel = AlbumSelection::initialise(albums); - sel.state.list.select(Some(albums.len() - 1)); - sel.track.state.list.select(Some(1)); - - // Re-initialise. - let expected = sel.clone(); - let active_album = IdSelectAlbum::get(albums, &sel); - sel.reinitialise(albums, active_album); - assert_eq!(sel, expected); - - // Re-initialise out-of-bounds. - let mut expected = sel.clone(); - expected.decrement(albums, Delta::Line); - let active_album = IdSelectAlbum::get(albums, &sel); - sel.reinitialise(&albums[..(albums.len() - 1)], active_album); - assert_eq!(sel, expected); - - // Re-initialise empty. - let expected = AlbumSelection::initialise(&[]); - let active_album = IdSelectAlbum::get(albums, &sel); - sel.reinitialise(&[], active_album); - assert_eq!(sel, expected); - } - - #[test] - fn artist_selection() { - let artists = &COLLECTION; - assert!(artists.len() > 1); - - let mut empty = ArtistSelection::initialise(&[]); - assert_eq!(empty.state.list.selected(), None); - assert_eq!(empty.album.state.list.selected(), None); - - empty.increment(artists, Delta::Line); - assert_eq!(empty.state.list.selected(), None); - assert_eq!(empty.album.state.list.selected(), None); - - empty.decrement(artists, Delta::Line); - assert_eq!(empty.state.list.selected(), None); - assert_eq!(empty.album.state.list.selected(), None); - - let mut sel = ArtistSelection::initialise(artists); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.album.state.list.selected(), Some(0)); - - sel.increment_album(artists, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.album.state.list.selected(), Some(1)); - - // Verify that decrement that doesn't change index does not reset album. - sel.decrement(artists, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.album.state.list.selected(), Some(1)); - - sel.increment(artists, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(1)); - assert_eq!(sel.album.state.list.selected(), Some(0)); - - sel.decrement(artists, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.album.state.list.selected(), Some(0)); - - for _ in 0..(artists.len() + 5) { - sel.increment(artists, Delta::Line); - } - assert_eq!(sel.state.list.selected(), Some(artists.len() - 1)); - assert_eq!(sel.album.state.list.selected(), Some(0)); - - sel.increment_album(artists, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(artists.len() - 1)); - assert_eq!(sel.album.state.list.selected(), Some(1)); - - // Verify that increment that doesn't change index does not reset album. - sel.increment(artists, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(artists.len() - 1)); - assert_eq!(sel.album.state.list.selected(), Some(1)); - } - - #[test] - fn artist_delta_page() { - let artists = &COLLECTION; - assert!(artists.len() > 1); - - let empty = ArtistSelection::initialise(&[]); - assert_eq!(empty.state.list.selected(), None); - - let mut sel = ArtistSelection::initialise(artists); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.album.state.list.selected(), Some(0)); - - assert!(artists.len() >= 4); - sel.state.height = 3; - - sel.increment_album(artists, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.album.state.list.selected(), Some(1)); - - // Verify that decrement that doesn't change index does not reset album. - sel.decrement(artists, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.album.state.list.selected(), Some(1)); - - sel.increment(artists, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(2)); - assert_eq!(sel.album.state.list.selected(), Some(0)); - - sel.decrement(artists, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.album.state.list.selected(), Some(0)); - - for _ in 0..(artists.len() + 5) { - sel.increment(artists, Delta::Page); - } - assert_eq!(sel.state.list.selected(), Some(artists.len() - 1)); - assert_eq!(sel.album.state.list.selected(), Some(0)); - - sel.increment_album(artists, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(artists.len() - 1)); - assert_eq!(sel.album.state.list.selected(), Some(1)); - - // Verify that increment that doesn't change index does not reset album. - sel.increment(artists, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(artists.len() - 1)); - assert_eq!(sel.album.state.list.selected(), Some(1)); - } - - #[test] - fn artist_reinitialise() { - let artists = &COLLECTION; - assert!(artists.len() > 1); - - let mut sel = ArtistSelection::initialise(artists); - sel.state.list.select(Some(artists.len() - 1)); - sel.album.state.list.select(Some(1)); - - // Re-initialise. - let expected = sel.clone(); - let active_artist = IdSelectArtist::get(artists, &sel); - sel.reinitialise(artists, active_artist); - assert_eq!(sel, expected); - - // Re-initialise out-of-bounds. - let mut expected = sel.clone(); - expected.decrement(artists, Delta::Line); - let active_artist = IdSelectArtist::get(artists, &sel); - sel.reinitialise(&artists[..(artists.len() - 1)], active_artist); - assert_eq!(sel, expected); - - // Re-initialise empty. - let expected = ArtistSelection::initialise(&[]); - let active_artist = IdSelectArtist::get(artists, &sel); - sel.reinitialise(&[], active_artist); - assert_eq!(sel, expected); - } - #[test] fn selection() { let mut selection = Selection::new(&COLLECTION); diff --git a/src/tui/app/selection/track.rs b/src/tui/app/selection/track.rs new file mode 100644 index 0000000..7a5b5a8 --- /dev/null +++ b/src/tui/app/selection/track.rs @@ -0,0 +1,208 @@ +use std::cmp; + +use musichoard::collection::track::{Track, TrackId}; + +use crate::tui::app::selection::{Delta, SelectionState, WidgetState}; + +#[derive(Clone, Debug, PartialEq)] +pub struct TrackSelection { + pub state: WidgetState, +} + +impl TrackSelection { + pub fn initialise(tracks: &[Track]) -> Self { + let mut selection = TrackSelection { + state: WidgetState::default(), + }; + selection.reinitialise(tracks, None); + selection + } + + pub fn reinitialise(&mut self, tracks: &[Track], track: Option) { + if let Some(track) = track { + let result = tracks.binary_search_by(|t| t.get_sort_key().cmp(&track.track_id)); + match result { + Ok(index) | Err(index) => self.reinitialise_with_index(tracks, index), + } + } else { + self.reinitialise_with_index(tracks, 0) + } + } + + fn reinitialise_with_index(&mut self, tracks: &[Track], index: usize) { + if tracks.is_empty() { + self.state.list.select(None); + } else if index >= tracks.len() { + self.state.list.select(Some(tracks.len() - 1)); + } else { + self.state.list.select(Some(index)); + } + } + + pub fn selected(&self) -> Option { + self.state.list.selected() + } + + pub fn select(&mut self, tracks: &[Track], to: Option) { + match to { + Some(to) => self.select_to(tracks, to), + None => self.state.list.select(None), + } + } + + fn select_to(&mut self, tracks: &[Track], mut to: usize) { + to = cmp::min(to, tracks.len() - 1); + self.state.list.select(Some(to)); + } + + pub fn selection_state<'a>(&self, list: &'a [Track]) -> Option> { + let selected = self.state.list.selected(); + selected.map(|index| SelectionState { list, index }) + } + + pub fn reset(&mut self, tracks: &[Track]) { + if self.state.list.selected() != Some(0) { + self.reinitialise(tracks, None); + } + } + + pub fn increment(&mut self, tracks: &[Track], delta: Delta) { + self.increment_by(tracks, delta.as_usize(&self.state)); + } + + fn increment_by(&mut self, tracks: &[Track], by: usize) { + if let Some(index) = self.state.list.selected() { + let mut result = index.saturating_add(by); + if result >= tracks.len() { + result = tracks.len() - 1; + } + if self.state.list.selected() != Some(result) { + self.state.list.select(Some(result)); + } + } + } + + pub fn decrement(&mut self, tracks: &[Track], delta: Delta) { + self.decrement_by(tracks, delta.as_usize(&self.state)); + } + + fn decrement_by(&mut self, _tracks: &[Track], by: usize) { + if let Some(index) = self.state.list.selected() { + let result = index.saturating_sub(by); + if self.state.list.selected() != Some(result) { + self.state.list.select(Some(result)); + } + } + } +} + +pub struct IdSelectTrack { + track_id: TrackId, +} + +impl IdSelectTrack { + pub fn get(tracks: &[Track], selection: &TrackSelection) -> Option { + selection.state.list.selected().map(|index| { + let track = &tracks[index]; + IdSelectTrack { + track_id: track.get_sort_key().clone(), + } + }) + } +} + +#[cfg(test)] +mod tests { + use crate::tui::testmod::COLLECTION; + + use super::*; + + #[test] + fn track_selection() { + let tracks = &COLLECTION[0].albums[0].tracks; + assert!(tracks.len() > 1); + + let mut empty = TrackSelection::initialise(&[]); + assert_eq!(empty.state.list.selected(), None); + + empty.increment(tracks, Delta::Line); + assert_eq!(empty.state.list.selected(), None); + + empty.decrement(tracks, Delta::Line); + assert_eq!(empty.state.list.selected(), None); + + let mut sel = TrackSelection::initialise(tracks); + assert_eq!(sel.state.list.selected(), Some(0)); + + sel.decrement(tracks, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(0)); + + sel.increment(tracks, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(1)); + + sel.decrement(tracks, Delta::Line); + assert_eq!(sel.state.list.selected(), Some(0)); + + for _ in 0..(tracks.len() + 5) { + sel.increment(tracks, Delta::Line); + } + assert_eq!(sel.state.list.selected(), Some(tracks.len() - 1)); + } + + #[test] + fn track_delta_page() { + let tracks = &COLLECTION[0].albums[0].tracks; + assert!(tracks.len() > 1); + + let empty = TrackSelection::initialise(&[]); + assert_eq!(empty.state.list.selected(), None); + + let mut sel = TrackSelection::initialise(tracks); + assert_eq!(sel.state.list.selected(), Some(0)); + + assert!(tracks.len() >= 4); + sel.state.height = 3; + + sel.decrement(tracks, Delta::Page); + assert_eq!(sel.state.list.selected(), Some(0)); + + sel.increment(tracks, Delta::Page); + assert_eq!(sel.state.list.selected(), Some(2)); + + sel.decrement(tracks, Delta::Page); + assert_eq!(sel.state.list.selected(), Some(0)); + + for _ in 0..(tracks.len() + 5) { + sel.increment(tracks, Delta::Page); + } + assert_eq!(sel.state.list.selected(), Some(tracks.len() - 1)); + } + + #[test] + fn track_reinitialise() { + let tracks = &COLLECTION[0].albums[0].tracks; + assert!(tracks.len() > 1); + + let mut sel = TrackSelection::initialise(tracks); + sel.state.list.select(Some(tracks.len() - 1)); + + // Re-initialise. + let expected = sel.clone(); + let active_track = IdSelectTrack::get(tracks, &sel); + sel.reinitialise(tracks, active_track); + assert_eq!(sel, expected); + + // Re-initialise out-of-bounds. + let mut expected = sel.clone(); + expected.decrement(tracks, Delta::Line); + let active_track = IdSelectTrack::get(tracks, &sel); + sel.reinitialise(&tracks[..(tracks.len() - 1)], active_track); + assert_eq!(sel, expected); + + // Re-initialise empty. + let expected = TrackSelection::initialise(&[]); + let active_track = IdSelectTrack::get(tracks, &sel); + sel.reinitialise(&[], active_track); + assert_eq!(sel, expected); + } +}