diff --git a/src/tui/app/machine/browse.rs b/src/tui/app/machine/browse.rs index 481ca32..686ef23 100644 --- a/src/tui/app/machine/browse.rs +++ b/src/tui/app/machine/browse.rs @@ -77,7 +77,7 @@ impl IAppInteractBrowse for AppMachine { let orig = ListSelection::get(&self.inner.selection); self.inner .selection - .reset_artist(self.inner.music_hoard.get_collection()); + .reset(self.inner.music_hoard.get_collection()); AppMachine::search(self.inner, orig).into() } diff --git a/src/tui/app/machine/search.rs b/src/tui/app/machine/search.rs index b009389..4c857bb 100644 --- a/src/tui/app/machine/search.rs +++ b/src/tui/app/machine/search.rs @@ -1,13 +1,13 @@ use aho_corasick::AhoCorasick; use once_cell::sync::Lazy; -use musichoard::collection::artist::Artist; +use musichoard::collection::{album::Album, artist::Artist, track::Track}; use crate::tui::{ app::{ machine::{App, AppInner, AppMachine}, - selection::ListSelection, - AppPublic, AppState, IAppInteractSearch, + selection::{ListSelection, SelectionState}, + AppPublic, AppState, Category, IAppInteractSearch, }, lib::IMusicHoard, }; @@ -67,7 +67,7 @@ impl IAppInteractSearch for AppMachine { fn append_character(mut self, ch: char) -> Self::APP { self.state.string.push(ch); - let index = self.inner.selection.artist.state.list.selected(); + let index = self.inner.selection.selected(); self.state.memo.push(AppSearchMemo { index, char: true }); self.incremental_search(false); self.into() @@ -75,7 +75,7 @@ impl IAppInteractSearch for AppMachine { fn search_next(mut self) -> Self::APP { if !self.state.string.is_empty() { - let index = self.inner.selection.artist.state.list.selected(); + let index = self.inner.selection.selected(); self.state.memo.push(AppSearchMemo { index, char: false }); self.incremental_search(true); } @@ -88,7 +88,7 @@ impl IAppInteractSearch for AppMachine { if memo.char { self.state.string.pop(); } - self.inner.selection.select_artist(collection, memo.index); + self.inner.selection.select(collection, memo.index); } self.into() } @@ -109,12 +109,18 @@ impl IAppInteractSearch for AppMachine { trait IAppInteractSearchPrivate { fn incremental_search(&mut self, next: bool); - fn incremental_search_predicate( - case_sensitive: bool, - char_sensitive: bool, - search_name: &str, - probe: &Artist, - ) -> bool; + fn next(pred: P, name: &str, next: bool, st: SelectionState<'_, T>) -> Option + where + P: FnMut(bool, bool, &str, &T) -> bool; + + fn search_artists(name: &str, next: bool, st: SelectionState<'_, Artist>) -> Option; + fn search_albums(name: &str, next: bool, st: SelectionState<'_, Album>) -> Option; + fn search_tracks(name: &str, next: bool, st: SelectionState<'_, Track>) -> Option; + + fn predicate_artists(case_sens: bool, char_sens: bool, search: &str, probe: &Artist) -> bool; + fn predicate_albums(case_sens: bool, char_sens: bool, search: &str, probe: &Album) -> bool; + fn predicate_tracks(case_sens: bool, char_sens: bool, search: &str, probe: &Track) -> bool; + fn predicate_title(case_sens: bool, char_sens: bool, search: &str, title: &str) -> bool; fn is_case_sensitive(artist_name: &str) -> bool; fn is_char_sensitive(artist_name: &str) -> bool; @@ -123,50 +129,86 @@ trait IAppInteractSearchPrivate { impl IAppInteractSearchPrivate for AppMachine { fn incremental_search(&mut self, next: bool) { - let artists = self.inner.music_hoard.get_collection(); - let artist_name = &self.state.string; + let collection = self.inner.music_hoard.get_collection(); + let search = &self.state.string; - let sel = &mut self.inner.selection; - if let Some(mut index) = sel.selected_artist() { - let case_sensitive = Self::is_case_sensitive(artist_name); - let char_sensitive = Self::is_char_sensitive(artist_name); - let search = Self::normalize_search(artist_name, !case_sensitive, !char_sensitive); + let sel = &self.inner.selection; + let result = match sel.active { + Category::Artist => sel + .state_artist(collection) + .and_then(|state| Self::search_artists(search, next, state)), + Category::Album => sel + .state_album(collection) + .and_then(|state| Self::search_albums(search, next, state)), + Category::Track => sel + .state_track(collection) + .and_then(|state| Self::search_tracks(search, next, state)), + }; - if next && ((index + 1) < artists.len()) { - index += 1; - } - let slice = &artists[index..]; - - let result = slice.iter().position(|probe| { - Self::incremental_search_predicate(case_sensitive, char_sensitive, &search, probe) - }); - - if let Some(slice_index) = result { - sel.select_artist(artists, Some(index + slice_index)); - } + if result.is_some() { + let collection = self.inner.music_hoard.get_collection(); + self.inner.selection.select(collection, result); } } - fn incremental_search_predicate( - case_sensitive: bool, - char_sensitive: bool, - search_name: &str, - probe: &Artist, - ) -> bool { - let name = Self::normalize_search(&probe.id.name, !case_sensitive, !char_sensitive); - let mut result = name.starts_with(search_name); + fn search_artists(name: &str, next: bool, st: SelectionState<'_, Artist>) -> Option { + Self::next(Self::predicate_artists, name, next, st) + } + + fn search_albums(name: &str, next: bool, st: SelectionState<'_, Album>) -> Option { + Self::next(Self::predicate_albums, name, next, st) + } + + fn search_tracks(name: &str, next: bool, st: SelectionState<'_, Track>) -> Option { + Self::next(Self::predicate_tracks, name, next, st) + } + + fn next(mut pred: P, name: &str, next: bool, st: SelectionState<'_, T>) -> Option + where + P: FnMut(bool, bool, &str, &T) -> bool, + { + let case_sens = Self::is_case_sensitive(name); + let char_sens = Self::is_char_sensitive(name); + let search = Self::normalize_search(name, !case_sens, !char_sens); + + let mut index = st.index; + if next && ((index + 1) < st.list.len()) { + index += 1; + } + + let slice = &st.list[index..]; + slice + .iter() + .position(|probe| pred(case_sens, char_sens, &search, probe)) + .map(|slice_index| index + slice_index) + } + + fn predicate_artists(case_sens: bool, char_sens: bool, search: &str, probe: &Artist) -> bool { + let name = Self::normalize_search(&probe.id.name, !case_sens, !char_sens); + let mut result = name.starts_with(search); if let Some(ref probe_sort) = probe.sort { if !result { - let name = - Self::normalize_search(&probe_sort.name, !case_sensitive, !char_sensitive); - result = name.starts_with(search_name); + let name = Self::normalize_search(&probe_sort.name, !case_sens, !char_sens); + result = name.starts_with(search); } } result } + fn predicate_albums(case_sens: bool, char_sens: bool, search: &str, probe: &Album) -> bool { + Self::predicate_title(case_sens, char_sens, search, &probe.id.title) + } + + fn predicate_tracks(case_sens: bool, char_sens: bool, search: &str, probe: &Track) -> bool { + Self::predicate_title(case_sens, char_sens, search, &probe.id.title) + } + + fn predicate_title(case_sens: bool, char_sens: bool, search: &str, title: &str) -> bool { + Self::normalize_search(title, !case_sens, !char_sens).starts_with(search) + } + fn is_case_sensitive(artist_name: &str) -> bool { artist_name .chars() @@ -330,6 +372,54 @@ mod tests { assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); } + #[test] + fn album_incremental_search() { + let mut search = + AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); + search.inner.selection.active = Category::Album; + + let sel = &search.inner.selection; + assert_eq!(sel.artist.album.state.list.selected(), Some(0)); + + search.state.string = String::from("album_title "); + search.incremental_search(false); + + let sel = &search.inner.selection; + assert_eq!(sel.artist.album.state.list.selected(), Some(0)); + + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('.').unwrap_search(); + let search = search.append_character('b').unwrap_search(); + + let sel = &search.inner.selection; + assert_eq!(sel.artist.album.state.list.selected(), Some(1)); + } + + #[test] + fn track_incremental_search() { + let mut search = + AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); + search.inner.selection.active = Category::Track; + + let sel = &search.inner.selection; + assert_eq!(sel.artist.album.track.state.list.selected(), Some(0)); + + search.state.string = String::from("track "); + search.incremental_search(false); + + let sel = &search.inner.selection; + assert_eq!(sel.artist.album.track.state.list.selected(), Some(0)); + + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('.').unwrap_search(); + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('.').unwrap_search(); + let search = search.append_character('2').unwrap_search(); + + let sel = &search.inner.selection; + assert_eq!(sel.artist.album.track.state.list.selected(), Some(1)); + } + #[test] fn search() { let search = AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2))); diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs deleted file mode 100644 index 6c4db7d..0000000 --- a/src/tui/app/selection.rs +++ /dev/null @@ -1,931 +0,0 @@ -use musichoard::collection::{ - album::{Album, AlbumId}, - artist::{Artist, ArtistId}, - track::{Track, TrackId}, - Collection, -}; -use ratatui::widgets::ListState; -use std::cmp; - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum Category { - Artist, - Album, - Track, -} - -#[derive(Clone, Debug, Default)] -pub struct WidgetState { - pub list: ListState, - pub height: usize, -} - -impl PartialEq for WidgetState { - fn eq(&self, other: &Self) -> bool { - self.list.selected().eq(&other.list.selected()) && self.height.eq(&other.height) - } -} - -pub struct Selection { - pub active: Category, - 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 enum Delta { - Line, - Page, -} - -impl Delta { - fn as_usize(&self, state: &WidgetState) -> usize { - match self { - Delta::Line => 1, - Delta::Page => state.height.saturating_sub(1), - } - } -} - -impl Selection { - pub fn new(artists: &[Artist]) -> Self { - Selection { - active: Category::Artist, - artist: ArtistSelection::initialise(artists), - } - } - - pub fn select_by_list(&mut self, selected: ListSelection) { - self.artist.state.list = selected.artist; - self.artist.album.state.list = selected.album; - self.artist.album.track.state.list = selected.track; - } - - pub fn select_by_id(&mut self, artists: &[Artist], selected: IdSelection) { - self.artist.reinitialise(artists, selected.artist); - } - - pub fn select_artist(&mut self, artists: &[Artist], index: Option) { - self.artist.select(artists, index); - } - - pub fn selected_artist(&self) -> Option { - self.artist.selected() - } - - pub fn reset_artist(&mut self, artists: &[Artist]) { - if self.artist.state.list.selected() != Some(0) { - self.select_by_id(artists, IdSelection { artist: None }); - } - } - - pub fn increment_category(&mut self) { - self.active = match self.active { - Category::Artist => Category::Album, - Category::Album => Category::Track, - Category::Track => Category::Track, - }; - } - - pub fn decrement_category(&mut self) { - self.active = match self.active { - Category::Artist => Category::Artist, - Category::Album => Category::Artist, - Category::Track => Category::Album, - }; - } - - pub fn increment_selection(&mut self, collection: &Collection, delta: Delta) { - match self.active { - Category::Artist => self.increment_artist(collection, delta), - Category::Album => self.increment_album(collection, delta), - Category::Track => self.increment_track(collection, delta), - } - } - - pub fn decrement_selection(&mut self, collection: &Collection, delta: Delta) { - match self.active { - Category::Artist => self.decrement_artist(collection, delta), - Category::Album => self.decrement_album(collection, delta), - Category::Track => self.decrement_track(collection, delta), - } - } - - fn increment_artist(&mut self, artists: &[Artist], delta: Delta) { - self.artist.increment(artists, delta); - } - - fn decrement_artist(&mut self, artists: &[Artist], delta: Delta) { - self.artist.decrement(artists, delta); - } - - fn increment_album(&mut self, artists: &[Artist], delta: Delta) { - self.artist.increment_album(artists, delta); - } - - fn decrement_album(&mut self, artists: &[Artist], delta: Delta) { - self.artist.decrement_album(artists, delta); - } - - fn increment_track(&mut self, artists: &[Artist], delta: Delta) { - self.artist.increment_track(artists, delta); - } - - fn decrement_track(&mut self, artists: &[Artist], delta: Delta) { - self.artist.decrement_track(artists, delta); - } -} - -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 select(&mut self, artists: &[Artist], to: Option) { - match to { - Some(to) => self.select_to(artists, to), - None => self.state.list.select(None), - } - } - - 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 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 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 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, - pub track: ListState, -} - -impl ListSelection { - pub fn get(selection: &Selection) -> Self { - ListSelection { - artist: selection.artist.state.list.clone(), - album: selection.artist.album.state.list.clone(), - track: selection.artist.album.track.state.list.clone(), - } - } -} - -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 { - artist: IdSelectArtist::get(collection, &selection.artist), - } - } -} - -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); - - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - selection.increment_selection(&COLLECTION, Delta::Line); - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - selection.increment_category(); - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - selection.increment_selection(&COLLECTION, Delta::Line); - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - selection.increment_category(); - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - selection.increment_selection(&COLLECTION, Delta::Line); - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); - - selection.increment_category(); - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); - - selection.decrement_selection(&COLLECTION, Delta::Line); - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - selection.increment_selection(&COLLECTION, Delta::Line); - selection.decrement_category(); - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); - - selection.decrement_selection(&COLLECTION, Delta::Line); - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - selection.increment_selection(&COLLECTION, Delta::Line); - selection.decrement_category(); - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - selection.decrement_selection(&COLLECTION, Delta::Line); - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - selection.increment_category(); - selection.increment_selection(&COLLECTION, Delta::Line); - selection.decrement_category(); - selection.decrement_selection(&COLLECTION, Delta::Line); - selection.decrement_category(); - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - } -} diff --git a/src/tui/app/selection/album.rs b/src/tui/app/selection/album.rs new file mode 100644 index 0000000..3780948 --- /dev/null +++ b/src/tui/app/selection/album.rs @@ -0,0 +1,342 @@ +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_select() { + let albums = &COLLECTION[0].albums; + assert!(albums.len() > 1); + + let mut sel = AlbumSelection::initialise(albums); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.selected_track(), Some(0)); + + sel.select(albums, None); + assert_eq!(sel.selected(), None); + assert_eq!(sel.selected_track(), Some(0)); + + sel.select(albums, Some(albums.len())); + assert_eq!(sel.selected(), Some(albums.len() - 1)); + assert_eq!(sel.selected_track(), Some(0)); + + sel.select_track(albums, None); + assert_eq!(sel.selected(), Some(albums.len() - 1)); + assert_eq!(sel.selected_track(), None); + + sel.reset_track(albums); + assert_eq!(sel.selected(), Some(albums.len() - 1)); + assert_eq!(sel.selected_track(), Some(0)); + + sel.select_track(albums, Some(1)); + assert_eq!(sel.selected(), Some(albums.len() - 1)); + assert_eq!(sel.selected_track(), Some(1)); + + sel.reset(albums); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.selected_track(), Some(0)); + } + + #[test] + fn album_delta_line() { + let albums = &COLLECTION[0].albums; + assert!(albums.len() > 1); + + let mut empty = AlbumSelection::initialise(&[]); + assert_eq!(empty.selected(), None); + assert_eq!(empty.selected_track(), None); + + empty.increment(albums, Delta::Line); + assert_eq!(empty.selected(), None); + assert_eq!(empty.selected_track(), None); + + empty.decrement(albums, Delta::Line); + assert_eq!(empty.selected(), None); + assert_eq!(empty.selected_track(), None); + + let mut sel = AlbumSelection::initialise(albums); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.selected_track(), Some(0)); + + sel.increment_track(albums, Delta::Line); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.selected_track(), Some(1)); + + // Verify that decrement that doesn't change index does not reset track. + sel.decrement(albums, Delta::Line); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.selected_track(), Some(1)); + + sel.increment(albums, Delta::Line); + assert_eq!(sel.selected(), Some(1)); + assert_eq!(sel.selected_track(), Some(0)); + + sel.decrement(albums, Delta::Line); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.selected_track(), Some(0)); + + for _ in 0..(albums.len() + 5) { + sel.increment(albums, Delta::Line); + } + assert_eq!(sel.selected(), Some(albums.len() - 1)); + assert_eq!(sel.selected_track(), Some(0)); + + sel.increment_track(albums, Delta::Line); + assert_eq!(sel.selected(), Some(albums.len() - 1)); + assert_eq!(sel.selected_track(), Some(1)); + + // Verify that increment that doesn't change index does not reset track. + sel.increment(albums, Delta::Line); + assert_eq!(sel.selected(), Some(albums.len() - 1)); + assert_eq!(sel.selected_track(), Some(1)); + } + + #[test] + fn album_delta_page() { + let albums = &COLLECTION[1].albums; + assert!(albums.len() > 1); + + let empty = AlbumSelection::initialise(&[]); + assert_eq!(empty.selected(), None); + + let mut sel = AlbumSelection::initialise(albums); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.selected_track(), Some(0)); + + assert!(albums.len() >= 4); + sel.state.height = 3; + + sel.increment_track(albums, Delta::Line); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.selected_track(), Some(1)); + + // Verify that decrement that doesn't change index does not reset track. + sel.decrement(albums, Delta::Page); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.selected_track(), Some(1)); + + sel.increment(albums, Delta::Page); + assert_eq!(sel.selected(), Some(2)); + assert_eq!(sel.selected_track(), Some(0)); + + sel.decrement(albums, Delta::Page); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.selected_track(), Some(0)); + + for _ in 0..(albums.len() + 5) { + sel.increment(albums, Delta::Page); + } + assert_eq!(sel.selected(), Some(albums.len() - 1)); + assert_eq!(sel.selected_track(), Some(0)); + + sel.increment_track(albums, Delta::Line); + assert_eq!(sel.selected(), Some(albums.len() - 1)); + assert_eq!(sel.selected_track(), Some(1)); + + // Verify that increment that doesn't change index does not reset track. + sel.increment(albums, Delta::Page); + assert_eq!(sel.selected(), Some(albums.len() - 1)); + assert_eq!(sel.selected_track(), 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..4b11287 --- /dev/null +++ b/src/tui/app/selection/artist.rs @@ -0,0 +1,393 @@ +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_select() { + let artists = &COLLECTION; + assert!(artists.len() > 1); + + let mut sel = ArtistSelection::initialise(artists); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.selected_album(), Some(0)); + assert_eq!(sel.selected_track(), Some(0)); + + sel.select(artists, None); + assert_eq!(sel.selected(), None); + assert_eq!(sel.selected_album(), Some(0)); + assert_eq!(sel.selected_track(), Some(0)); + + sel.select(artists, Some(artists.len())); + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.selected_album(), Some(0)); + assert_eq!(sel.selected_track(), Some(0)); + + sel.select_track(artists, None); + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.selected_album(), Some(0)); + assert_eq!(sel.selected_track(), None); + + sel.select_album(artists, None); + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.selected_album(), None); + assert_eq!(sel.selected_track(), None); + + sel.reset_album(artists); + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.selected_album(), Some(0)); + assert_eq!(sel.selected_track(), Some(0)); + + sel.select_track(artists, None); + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.selected_album(), Some(0)); + assert_eq!(sel.selected_track(), None); + + sel.reset_track(artists); + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.selected_album(), Some(0)); + assert_eq!(sel.selected_track(), Some(0)); + + sel.select_album(artists, Some(1)); + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.selected_album(), Some(1)); + assert_eq!(sel.selected_track(), Some(0)); + + sel.reset(artists); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.selected_album(), Some(0)); + assert_eq!(sel.selected_track(), Some(0)); + } + + #[test] + fn artist_delta_line() { + let artists = &COLLECTION; + assert!(artists.len() > 1); + + let mut empty = ArtistSelection::initialise(&[]); + assert_eq!(empty.selected(), None); + assert_eq!(empty.album.selected(), None); + + empty.increment(artists, Delta::Line); + assert_eq!(empty.selected(), None); + assert_eq!(empty.album.selected(), None); + + empty.decrement(artists, Delta::Line); + assert_eq!(empty.selected(), None); + assert_eq!(empty.album.selected(), None); + + let mut sel = ArtistSelection::initialise(artists); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.album.selected(), Some(0)); + + sel.increment_album(artists, Delta::Line); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.album.selected(), Some(1)); + + // Verify that decrement that doesn't change index does not reset album. + sel.decrement(artists, Delta::Line); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.album.selected(), Some(1)); + + sel.increment(artists, Delta::Line); + assert_eq!(sel.selected(), Some(1)); + assert_eq!(sel.album.selected(), Some(0)); + + sel.decrement(artists, Delta::Line); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.album.selected(), Some(0)); + + for _ in 0..(artists.len() + 5) { + sel.increment(artists, Delta::Line); + } + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.selected(), Some(0)); + + sel.increment_album(artists, Delta::Line); + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.selected(), Some(1)); + + // Verify that increment that doesn't change index does not reset album. + sel.increment(artists, Delta::Line); + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.selected(), Some(1)); + } + + #[test] + fn artist_delta_page() { + let artists = &COLLECTION; + assert!(artists.len() > 1); + + let empty = ArtistSelection::initialise(&[]); + assert_eq!(empty.selected(), None); + + let mut sel = ArtistSelection::initialise(artists); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.album.selected(), Some(0)); + + assert!(artists.len() >= 4); + sel.state.height = 3; + + sel.increment_album(artists, Delta::Line); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.album.selected(), Some(1)); + + // Verify that decrement that doesn't change index does not reset album. + sel.decrement(artists, Delta::Page); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.album.selected(), Some(1)); + + sel.increment(artists, Delta::Page); + assert_eq!(sel.selected(), Some(2)); + assert_eq!(sel.album.selected(), Some(0)); + + sel.decrement(artists, Delta::Page); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.album.selected(), Some(0)); + + for _ in 0..(artists.len() + 5) { + sel.increment(artists, Delta::Page); + } + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.selected(), Some(0)); + + sel.increment_album(artists, Delta::Line); + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.selected(), Some(1)); + + // Verify that increment that doesn't change index does not reset album. + sel.increment(artists, Delta::Page); + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.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 new file mode 100644 index 0000000..494a2e0 --- /dev/null +++ b/src/tui/app/selection/mod.rs @@ -0,0 +1,389 @@ +mod album; +mod artist; +mod track; + +use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection}; +use ratatui::widgets::ListState; + +use artist::{ArtistSelection, IdSelectArtist}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Category { + Artist, + Album, + Track, +} + +#[derive(Clone, Debug, Default)] +pub struct WidgetState { + pub list: ListState, + pub height: usize, +} + +impl PartialEq for WidgetState { + fn eq(&self, other: &Self) -> bool { + self.list.selected().eq(&other.list.selected()) && self.height.eq(&other.height) + } +} + +pub struct Selection { + pub active: Category, + pub artist: ArtistSelection, +} + +pub struct SelectionState<'a, T> { + pub list: &'a [T], + pub index: usize, +} + +pub enum Delta { + Line, + Page, +} + +impl Delta { + fn as_usize(&self, state: &WidgetState) -> usize { + match self { + Delta::Line => 1, + Delta::Page => state.height.saturating_sub(1), + } + } +} + +impl Selection { + pub fn new(artists: &[Artist]) -> Self { + Selection { + active: Category::Artist, + artist: ArtistSelection::initialise(artists), + } + } + + pub fn select_by_list(&mut self, selected: ListSelection) { + self.artist.state.list = selected.artist; + self.artist.album.state.list = selected.album; + self.artist.album.track.state.list = selected.track; + } + + pub fn select_by_id(&mut self, artists: &[Artist], selected: IdSelection) { + self.artist.reinitialise(artists, selected.artist); + } + + pub fn increment_category(&mut self) { + self.active = match self.active { + Category::Artist => Category::Album, + Category::Album => Category::Track, + Category::Track => Category::Track, + }; + } + + pub fn decrement_category(&mut self) { + self.active = match self.active { + Category::Artist => Category::Artist, + Category::Album => Category::Artist, + Category::Track => Category::Album, + }; + } + + pub fn select(&mut self, collection: &Collection, index: Option) { + match self.active { + Category::Artist => self.select_artist(collection, index), + Category::Album => self.select_album(collection, index), + Category::Track => self.select_track(collection, index), + } + } + + fn select_artist(&mut self, artists: &[Artist], index: Option) { + self.artist.select(artists, index); + } + + fn select_album(&mut self, artists: &[Artist], index: Option) { + self.artist.select_album(artists, index); + } + + fn select_track(&mut self, artists: &[Artist], index: Option) { + self.artist.select_track(artists, index); + } + + pub fn selected(&self) -> Option { + match self.active { + Category::Artist => self.selected_artist(), + Category::Album => self.selected_album(), + Category::Track => self.selected_track(), + } + } + + fn selected_artist(&self) -> Option { + self.artist.selected() + } + + fn selected_album(&self) -> Option { + self.artist.selected_album() + } + + fn selected_track(&self) -> Option { + self.artist.selected_track() + } + + pub fn state_artist<'a>(&self, coll: &'a Collection) -> Option> { + self.artist.selection_state(coll) + } + + pub fn state_album<'a>(&self, coll: &'a Collection) -> Option> { + self.artist.state_album(coll) + } + + pub fn state_track<'a>(&self, coll: &'a Collection) -> Option> { + self.artist.state_track(coll) + } + + pub fn reset(&mut self, collection: &Collection) { + match self.active { + Category::Artist => self.reset_artist(collection), + Category::Album => self.reset_album(collection), + Category::Track => self.reset_track(collection), + } + } + + fn reset_artist(&mut self, artists: &[Artist]) { + self.artist.reset(artists); + } + + fn reset_album(&mut self, artists: &[Artist]) { + self.artist.reset_album(artists); + } + + fn reset_track(&mut self, artists: &[Artist]) { + self.artist.reset_track(artists); + } + + pub fn increment_selection(&mut self, collection: &Collection, delta: Delta) { + match self.active { + Category::Artist => self.increment_artist(collection, delta), + Category::Album => self.increment_album(collection, delta), + Category::Track => self.increment_track(collection, delta), + } + } + + fn increment_artist(&mut self, artists: &[Artist], delta: Delta) { + self.artist.increment(artists, delta); + } + + fn increment_album(&mut self, artists: &[Artist], delta: Delta) { + self.artist.increment_album(artists, delta); + } + + fn increment_track(&mut self, artists: &[Artist], delta: Delta) { + self.artist.increment_track(artists, delta); + } + + pub fn decrement_selection(&mut self, collection: &Collection, delta: Delta) { + match self.active { + Category::Artist => self.decrement_artist(collection, delta), + Category::Album => self.decrement_album(collection, delta), + Category::Track => self.decrement_track(collection, delta), + } + } + + fn decrement_artist(&mut self, artists: &[Artist], delta: Delta) { + self.artist.decrement(artists, delta); + } + + fn decrement_album(&mut self, artists: &[Artist], delta: Delta) { + self.artist.decrement_album(artists, delta); + } + + fn decrement_track(&mut self, artists: &[Artist], delta: Delta) { + self.artist.decrement_track(artists, delta); + } +} + +pub struct ListSelection { + pub artist: ListState, + pub album: ListState, + pub track: ListState, +} + +impl ListSelection { + pub fn get(selection: &Selection) -> Self { + ListSelection { + artist: selection.artist.state.list.clone(), + album: selection.artist.album.state.list.clone(), + track: selection.artist.album.track.state.list.clone(), + } + } +} + +pub struct IdSelection { + artist: Option, +} + +impl IdSelection { + pub fn get(collection: &Collection, selection: &Selection) -> Self { + IdSelection { + artist: IdSelectArtist::get(collection, &selection.artist), + } + } +} + +#[cfg(test)] +mod tests { + use crate::tui::testmod::COLLECTION; + + use super::*; + + #[test] + fn selection_select() { + let mut selection = Selection::new(&COLLECTION); + + selection.select(&COLLECTION, Some(1)); + selection.increment_category(); + selection.select(&COLLECTION, Some(1)); + selection.increment_category(); + selection.select(&COLLECTION, Some(1)); + + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.selected(), Some(1)); + assert_eq!(selection.artist.album.selected(), Some(1)); + assert_eq!(selection.artist.album.track.selected(), Some(1)); + + selection.reset(&COLLECTION); + + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.selected(), Some(1)); + assert_eq!(selection.artist.album.selected(), Some(1)); + assert_eq!(selection.artist.album.track.selected(), Some(0)); + + selection.select(&COLLECTION, Some(1)); + + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.selected(), Some(1)); + assert_eq!(selection.artist.album.selected(), Some(1)); + assert_eq!(selection.artist.album.track.selected(), Some(1)); + + selection.decrement_category(); + + selection.reset(&COLLECTION); + + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.selected(), Some(1)); + assert_eq!(selection.artist.album.selected(), Some(0)); + assert_eq!(selection.artist.album.track.selected(), Some(0)); + + selection.select(&COLLECTION, Some(1)); + + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.selected(), Some(1)); + assert_eq!(selection.artist.album.selected(), Some(1)); + assert_eq!(selection.artist.album.track.selected(), Some(0)); + + selection.decrement_category(); + + selection.reset(&COLLECTION); + + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.selected(), Some(0)); + assert_eq!(selection.artist.album.selected(), Some(0)); + assert_eq!(selection.artist.album.track.selected(), Some(0)); + } + + #[test] + fn selection_delta() { + let mut selection = Selection::new(&COLLECTION); + + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.selected(), Some(0)); + assert_eq!(selection.artist.selected(), Some(0)); + assert_eq!(selection.artist.album.selected(), Some(0)); + assert_eq!(selection.artist.album.track.selected(), Some(0)); + + selection.increment_selection(&COLLECTION, Delta::Line); + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.selected(), Some(1)); + assert_eq!(selection.artist.selected(), Some(1)); + assert_eq!(selection.artist.album.selected(), Some(0)); + assert_eq!(selection.artist.album.track.selected(), Some(0)); + + selection.increment_category(); + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.selected(), Some(0)); + assert_eq!(selection.artist.selected(), Some(1)); + assert_eq!(selection.artist.album.selected(), Some(0)); + assert_eq!(selection.artist.album.track.selected(), Some(0)); + + selection.increment_selection(&COLLECTION, Delta::Line); + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.selected(), Some(1)); + assert_eq!(selection.artist.selected(), Some(1)); + assert_eq!(selection.artist.album.selected(), Some(1)); + assert_eq!(selection.artist.album.track.selected(), Some(0)); + + selection.increment_category(); + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.selected(), Some(0)); + assert_eq!(selection.artist.selected(), Some(1)); + assert_eq!(selection.artist.album.selected(), Some(1)); + assert_eq!(selection.artist.album.track.selected(), Some(0)); + + selection.increment_selection(&COLLECTION, Delta::Line); + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.selected(), Some(1)); + assert_eq!(selection.artist.selected(), Some(1)); + assert_eq!(selection.artist.album.selected(), Some(1)); + assert_eq!(selection.artist.album.track.selected(), Some(1)); + + selection.increment_category(); + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.selected(), Some(1)); + assert_eq!(selection.artist.selected(), Some(1)); + assert_eq!(selection.artist.album.selected(), Some(1)); + assert_eq!(selection.artist.album.track.selected(), Some(1)); + + selection.decrement_selection(&COLLECTION, Delta::Line); + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.selected(), Some(0)); + assert_eq!(selection.artist.selected(), Some(1)); + assert_eq!(selection.artist.album.selected(), Some(1)); + assert_eq!(selection.artist.album.track.selected(), Some(0)); + + selection.increment_selection(&COLLECTION, Delta::Line); + selection.decrement_category(); + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.selected(), Some(1)); + assert_eq!(selection.artist.selected(), Some(1)); + assert_eq!(selection.artist.album.selected(), Some(1)); + assert_eq!(selection.artist.album.track.selected(), Some(1)); + + selection.decrement_selection(&COLLECTION, Delta::Line); + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.selected(), Some(0)); + assert_eq!(selection.artist.selected(), Some(1)); + assert_eq!(selection.artist.album.selected(), Some(0)); + assert_eq!(selection.artist.album.track.selected(), Some(0)); + + selection.increment_selection(&COLLECTION, Delta::Line); + selection.decrement_category(); + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.selected(), Some(1)); + assert_eq!(selection.artist.selected(), Some(1)); + assert_eq!(selection.artist.album.selected(), Some(1)); + assert_eq!(selection.artist.album.track.selected(), Some(0)); + + selection.decrement_selection(&COLLECTION, Delta::Line); + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.selected(), Some(0)); + assert_eq!(selection.artist.selected(), Some(0)); + assert_eq!(selection.artist.album.selected(), Some(0)); + assert_eq!(selection.artist.album.track.selected(), Some(0)); + + selection.increment_category(); + selection.increment_selection(&COLLECTION, Delta::Line); + selection.decrement_category(); + selection.decrement_selection(&COLLECTION, Delta::Line); + selection.decrement_category(); + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.selected(), Some(0)); + assert_eq!(selection.artist.selected(), Some(0)); + assert_eq!(selection.artist.album.selected(), Some(1)); + assert_eq!(selection.artist.album.track.selected(), Some(0)); + } +} diff --git a/src/tui/app/selection/track.rs b/src/tui/app/selection/track.rs new file mode 100644 index 0000000..5620d1f --- /dev/null +++ b/src/tui/app/selection/track.rs @@ -0,0 +1,226 @@ +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_select() { + let tracks = &COLLECTION[0].albums[0].tracks; + assert!(tracks.len() > 1); + + let mut sel = TrackSelection::initialise(tracks); + assert_eq!(sel.selected(), Some(0)); + + sel.select(tracks, None); + assert_eq!(sel.selected(), None); + + sel.select(tracks, Some(tracks.len())); + assert_eq!(sel.selected(), Some(tracks.len() - 1)); + + sel.reset(tracks); + assert_eq!(sel.selected(), Some(0)); + } + + #[test] + fn track_delta_line() { + let tracks = &COLLECTION[0].albums[0].tracks; + assert!(tracks.len() > 1); + + let mut empty = TrackSelection::initialise(&[]); + assert_eq!(empty.selected(), None); + + empty.increment(tracks, Delta::Line); + assert_eq!(empty.selected(), None); + + empty.decrement(tracks, Delta::Line); + assert_eq!(empty.selected(), None); + + let mut sel = TrackSelection::initialise(tracks); + assert_eq!(sel.selected(), Some(0)); + + sel.decrement(tracks, Delta::Line); + assert_eq!(sel.selected(), Some(0)); + + sel.increment(tracks, Delta::Line); + assert_eq!(sel.selected(), Some(1)); + + sel.decrement(tracks, Delta::Line); + assert_eq!(sel.selected(), Some(0)); + + for _ in 0..(tracks.len() + 5) { + sel.increment(tracks, Delta::Line); + } + assert_eq!(sel.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.selected(), None); + + let mut sel = TrackSelection::initialise(tracks); + assert_eq!(sel.selected(), Some(0)); + + assert!(tracks.len() >= 4); + sel.state.height = 3; + + sel.decrement(tracks, Delta::Page); + assert_eq!(sel.selected(), Some(0)); + + sel.increment(tracks, Delta::Page); + assert_eq!(sel.selected(), Some(2)); + + sel.decrement(tracks, Delta::Page); + assert_eq!(sel.selected(), Some(0)); + + for _ in 0..(tracks.len() + 5) { + sel.increment(tracks, Delta::Page); + } + assert_eq!(sel.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); + } +}