From a381705a1cfd00e53b96e5e27d474d2ddc54e25e Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 1 Mar 2024 18:48:23 +0100 Subject: [PATCH 1/7] Working search --- src/tui/app/machine/browse.rs | 2 +- src/tui/app/machine/search.rs | 129 +++++++++++++++------- src/tui/app/selection.rs | 199 +++++++++++++++++++++++++++++----- 3 files changed, 265 insertions(+), 65 deletions(-) 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..3a1e886 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, + 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,13 @@ 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 search_category(list: &[T], index: usize, next: bool, pred: P) -> Option + where + P: FnMut(&T) -> bool; + 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 +124,102 @@ 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_name = &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); + if let Some(index) = sel.selected() { + let case_sensitive = Self::is_case_sensitive(search_name); + let char_sensitive = Self::is_char_sensitive(search_name); + let search = Self::normalize_search(search_name, !case_sensitive, !char_sensitive); - if next && ((index + 1) < artists.len()) { - index += 1; - } - let slice = &artists[index..]; + let result = match sel.active { + Category::Artist => { + let artists = collection; + Self::search_category(artists, index, next, |probe| { + Self::predicate_artists(case_sensitive, char_sensitive, &search, probe) + }) + } + Category::Album => { + let artists = collection; + let albums = sel + .artist + .state + .list + .selected() + .map(|i| &artists[i].albums) + .unwrap(); + Self::search_category(albums, index, next, |probe| { + Self::predicate_albums(case_sensitive, char_sensitive, &search, probe) + }) + } + Category::Track => { + let artists = collection; + let albums = sel + .artist + .state + .list + .selected() + .map(|i| &artists[i].albums) + .unwrap(); + let tracks = sel + .artist + .album + .state + .list + .selected() + .map(|i| &albums[i].tracks) + .unwrap(); + Self::search_category(tracks, index, next, |probe| { + Self::predicate_tracks(case_sensitive, char_sensitive, &search, probe) + }) + } + }; - 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() { + sel.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_category(list: &[T], mut index: usize, next: bool, pred: P) -> Option + where + P: FnMut(&T) -> bool, + { + if next && ((index + 1) < list.len()) { + index += 1; + } + let slice = &list[index..]; + let result = slice.iter().position(pred); + result.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() diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index 6c4db7d..34a2845 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -80,20 +80,6 @@ impl Selection { 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, @@ -110,6 +96,66 @@ impl Selection { }; } + 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 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), @@ -118,6 +164,18 @@ impl Selection { } } + 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), @@ -126,26 +184,14 @@ impl Selection { } } - 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); } @@ -197,6 +243,14 @@ impl ArtistSelection { 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), @@ -204,6 +258,18 @@ impl ArtistSelection { } } + 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) { @@ -212,6 +278,24 @@ impl ArtistSelection { } } + 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); @@ -303,6 +387,47 @@ impl AlbumSelection { } } + 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 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); @@ -377,6 +502,28 @@ impl TrackSelection { } } + 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 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); -- 2.45.2 From 32f6fbd39a63549965905a13b2f7c43ec2970435 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 1 Mar 2024 20:08:23 +0100 Subject: [PATCH 2/7] Streamline code --- src/tui/app/machine/search.rs | 101 ++++++++++++++-------------------- src/tui/app/selection.rs | 47 ++++++++++++++++ 2 files changed, 88 insertions(+), 60 deletions(-) diff --git a/src/tui/app/machine/search.rs b/src/tui/app/machine/search.rs index 3a1e886..c34048f 100644 --- a/src/tui/app/machine/search.rs +++ b/src/tui/app/machine/search.rs @@ -6,7 +6,7 @@ use musichoard::collection::{album::Album, artist::Artist, track::Track}; use crate::tui::{ app::{ machine::{App, AppInner, AppMachine}, - selection::ListSelection, + selection::{ListSelection, SelectionState}, AppPublic, AppState, Category, IAppInteractSearch, }, lib::IMusicHoard, @@ -109,9 +109,14 @@ impl IAppInteractSearch for AppMachine { trait IAppInteractSearchPrivate { fn incremental_search(&mut self, next: bool); - fn search_category(list: &[T], index: usize, next: bool, pred: P) -> Option + fn search_category( + state: SelectionState<'_, T>, + search_name: &str, + next: bool, + predicate: P, + ) -> Option where - P: FnMut(&T) -> bool; + P: FnMut(bool, bool, &str, &T) -> bool; 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; @@ -125,73 +130,49 @@ trait IAppInteractSearchPrivate { impl IAppInteractSearchPrivate for AppMachine { fn incremental_search(&mut self, next: bool) { let collection = self.inner.music_hoard.get_collection(); - let search_name = &self.state.string; + let search = &self.state.string; let sel = &mut self.inner.selection; - if let Some(index) = sel.selected() { - let case_sensitive = Self::is_case_sensitive(search_name); - let char_sensitive = Self::is_char_sensitive(search_name); - let search = Self::normalize_search(search_name, !case_sensitive, !char_sensitive); + let result = match sel.active { + Category::Artist => sel.state_artist(collection).and_then(|state| { + Self::search_category(state, search, next, Self::predicate_artists) + }), + Category::Album => sel.state_album(collection).and_then(|state| { + Self::search_category(state, search, next, Self::predicate_albums) + }), + Category::Track => sel.state_track(collection).and_then(|state| { + Self::search_category(state, search, next, Self::predicate_tracks) + }), + }; - let result = match sel.active { - Category::Artist => { - let artists = collection; - Self::search_category(artists, index, next, |probe| { - Self::predicate_artists(case_sensitive, char_sensitive, &search, probe) - }) - } - Category::Album => { - let artists = collection; - let albums = sel - .artist - .state - .list - .selected() - .map(|i| &artists[i].albums) - .unwrap(); - Self::search_category(albums, index, next, |probe| { - Self::predicate_albums(case_sensitive, char_sensitive, &search, probe) - }) - } - Category::Track => { - let artists = collection; - let albums = sel - .artist - .state - .list - .selected() - .map(|i| &artists[i].albums) - .unwrap(); - let tracks = sel - .artist - .album - .state - .list - .selected() - .map(|i| &albums[i].tracks) - .unwrap(); - Self::search_category(tracks, index, next, |probe| { - Self::predicate_tracks(case_sensitive, char_sensitive, &search, probe) - }) - } - }; - - if result.is_some() { - sel.select(collection, result); - } + if result.is_some() { + sel.select(collection, result); } } - fn search_category(list: &[T], mut index: usize, next: bool, pred: P) -> Option + fn search_category( + state: SelectionState<'_, T>, + search_name: &str, + next: bool, + mut predicate: P, + ) -> Option where - P: FnMut(&T) -> bool, + P: FnMut(bool, bool, &str, &T) -> bool, { - if next && ((index + 1) < list.len()) { + let case_sens = Self::is_case_sensitive(search_name); + let char_sens = Self::is_char_sensitive(search_name); + let search = Self::normalize_search(search_name, !case_sens, !char_sens); + + let mut index = state.index; + if next && ((index + 1) < state.list.len()) { index += 1; } - let slice = &list[index..]; - let result = slice.iter().position(pred); - result.map(|slice_index| index + slice_index) + + let slice = &state.list[index..]; + slice + .iter() + .position(|probe| predicate(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 { diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index 34a2845..6ffd615 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -62,6 +62,11 @@ impl Delta { } } +pub struct SelectionState<'a, T> { + pub list: &'a [T], + pub index: usize, +} + impl Selection { pub fn new(artists: &[Artist]) -> Self { Selection { @@ -136,6 +141,18 @@ impl Selection { 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), @@ -278,6 +295,21 @@ impl ArtistSelection { } } + 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); @@ -416,6 +448,16 @@ impl AlbumSelection { } } + 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); @@ -518,6 +560,11 @@ impl TrackSelection { 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); -- 2.45.2 From da52299ba727578f433375c6bf6ec394f0ce2060 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 1 Mar 2024 20:41:04 +0100 Subject: [PATCH 3/7] More streamlining --- src/tui/app/machine/search.rs | 68 +++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/src/tui/app/machine/search.rs b/src/tui/app/machine/search.rs index c34048f..08ebe70 100644 --- a/src/tui/app/machine/search.rs +++ b/src/tui/app/machine/search.rs @@ -109,14 +109,14 @@ impl IAppInteractSearch for AppMachine { trait IAppInteractSearchPrivate { fn incremental_search(&mut self, next: bool); - fn search_category( - state: SelectionState<'_, T>, - search_name: &str, - next: bool, - predicate: P, - ) -> Option + 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; @@ -132,46 +132,54 @@ impl IAppInteractSearchPrivate for AppMachine { let collection = self.inner.music_hoard.get_collection(); let search = &self.state.string; - let sel = &mut self.inner.selection; + let sel = &self.inner.selection; let result = match sel.active { - Category::Artist => sel.state_artist(collection).and_then(|state| { - Self::search_category(state, search, next, Self::predicate_artists) - }), - Category::Album => sel.state_album(collection).and_then(|state| { - Self::search_category(state, search, next, Self::predicate_albums) - }), - Category::Track => sel.state_track(collection).and_then(|state| { - Self::search_category(state, search, next, Self::predicate_tracks) - }), + 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 result.is_some() { - sel.select(collection, result); + let collection = self.inner.music_hoard.get_collection(); + self.inner.selection.select(collection, result); } } - fn search_category( - state: SelectionState<'_, T>, - search_name: &str, - next: bool, - mut predicate: P, - ) -> Option + 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(search_name); - let char_sens = Self::is_char_sensitive(search_name); - let search = Self::normalize_search(search_name, !case_sens, !char_sens); + 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 = state.index; - if next && ((index + 1) < state.list.len()) { + let mut index = st.index; + if next && ((index + 1) < st.list.len()) { index += 1; } - let slice = &state.list[index..]; + let slice = &st.list[index..]; slice .iter() - .position(|probe| predicate(case_sens, char_sens, &search, probe)) + .position(|probe| pred(case_sens, char_sens, &search, probe)) .map(|slice_index| index + slice_index) } -- 2.45.2 From 90237fb77e0cc041528e4cc61f6e1d10e7978c51 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 1 Mar 2024 20:52:21 +0100 Subject: [PATCH 4/7] Unit test search --- src/tui/app/machine/search.rs | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/tui/app/machine/search.rs b/src/tui/app/machine/search.rs index 08ebe70..6545247 100644 --- a/src/tui/app/machine/search.rs +++ b/src/tui/app/machine/search.rs @@ -372,6 +372,50 @@ 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)); + + search.state.string = String::from("album_title a.b"); + search.incremental_search(false); + + 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)); + + search.state.string = String::from("track a.a.2"); + search.incremental_search(false); + + 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))); -- 2.45.2 From 6beb8cd856a80a4e94c49bbb0156a70d1fb02467 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 1 Mar 2024 20:53:56 +0100 Subject: [PATCH 5/7] Move selection module to subdirectory --- src/tui/app/{selection.rs => selection/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/tui/app/{selection.rs => selection/mod.rs} (100%) diff --git a/src/tui/app/selection.rs b/src/tui/app/selection/mod.rs similarity index 100% rename from src/tui/app/selection.rs rename to src/tui/app/selection/mod.rs -- 2.45.2 From 588abb12b86e75686c00d96b72242b824a496231 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 1 Mar 2024 21:16:08 +0100 Subject: [PATCH 6/7] Split selection module --- src/tui/app/selection/album.rs | 306 ++++++++++++ src/tui/app/selection/artist.rs | 338 +++++++++++++ src/tui/app/selection/mod.rs | 824 +------------------------------- src/tui/app/selection/track.rs | 208 ++++++++ 4 files changed, 862 insertions(+), 814 deletions(-) create mode 100644 src/tui/app/selection/album.rs create mode 100644 src/tui/app/selection/artist.rs create mode 100644 src/tui/app/selection/track.rs 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); + } +} -- 2.45.2 From abc51ffb0278944d2fafddd797cadef2c551342e Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 1 Mar 2024 22:00:59 +0100 Subject: [PATCH 7/7] Complete unit tests --- src/tui/app/machine/search.rs | 12 ++- src/tui/app/selection/album.rs | 118 ++++++++++++++++--------- src/tui/app/selection/artist.rs | 135 ++++++++++++++++++++--------- src/tui/app/selection/mod.rs | 148 +++++++++++++++++++++++--------- src/tui/app/selection/track.rs | 48 +++++++---- 5 files changed, 321 insertions(+), 140 deletions(-) diff --git a/src/tui/app/machine/search.rs b/src/tui/app/machine/search.rs index 6545247..4c857bb 100644 --- a/src/tui/app/machine/search.rs +++ b/src/tui/app/machine/search.rs @@ -387,8 +387,9 @@ mod tests { let sel = &search.inner.selection; assert_eq!(sel.artist.album.state.list.selected(), Some(0)); - search.state.string = String::from("album_title a.b"); - search.incremental_search(false); + 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)); @@ -409,8 +410,11 @@ mod tests { let sel = &search.inner.selection; assert_eq!(sel.artist.album.track.state.list.selected(), Some(0)); - search.state.string = String::from("track a.a.2"); - search.incremental_search(false); + 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)); diff --git a/src/tui/app/selection/album.rs b/src/tui/app/selection/album.rs index 06cdb16..3780948 100644 --- a/src/tui/app/selection/album.rs +++ b/src/tui/app/selection/album.rs @@ -1,6 +1,9 @@ use std::cmp; -use musichoard::collection::{album::{Album, AlbumId}, track::Track}; +use musichoard::collection::{ + album::{Album, AlbumId}, + track::Track, +}; use crate::tui::app::selection::{ track::{IdSelectTrack, TrackSelection}, @@ -172,59 +175,92 @@ mod tests { 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_selection() { + fn album_delta_line() { 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); + assert_eq!(empty.selected(), None); + assert_eq!(empty.selected_track(), None); empty.increment(albums, Delta::Line); - assert_eq!(empty.state.list.selected(), None); - assert_eq!(empty.track.state.list.selected(), None); + assert_eq!(empty.selected(), None); + assert_eq!(empty.selected_track(), None); empty.decrement(albums, Delta::Line); - assert_eq!(empty.state.list.selected(), None); - assert_eq!(empty.track.state.list.selected(), None); + assert_eq!(empty.selected(), None); + assert_eq!(empty.selected_track(), 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_eq!(sel.selected(), Some(0)); + assert_eq!(sel.selected_track(), 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)); + 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.state.list.selected(), Some(0)); - assert_eq!(sel.track.state.list.selected(), Some(1)); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.selected_track(), Some(1)); sel.increment(albums, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(1)); - assert_eq!(sel.track.state.list.selected(), Some(0)); + assert_eq!(sel.selected(), Some(1)); + assert_eq!(sel.selected_track(), Some(0)); sel.decrement(albums, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.track.state.list.selected(), Some(0)); + 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.state.list.selected(), Some(albums.len() - 1)); - assert_eq!(sel.track.state.list.selected(), Some(0)); + assert_eq!(sel.selected(), Some(albums.len() - 1)); + assert_eq!(sel.selected_track(), 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)); + 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.state.list.selected(), Some(albums.len() - 1)); - assert_eq!(sel.track.state.list.selected(), Some(1)); + assert_eq!(sel.selected(), Some(albums.len() - 1)); + assert_eq!(sel.selected_track(), Some(1)); } #[test] @@ -233,46 +269,46 @@ mod tests { assert!(albums.len() > 1); let empty = AlbumSelection::initialise(&[]); - assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.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_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.state.list.selected(), Some(0)); - assert_eq!(sel.track.state.list.selected(), Some(1)); + 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.state.list.selected(), Some(0)); - assert_eq!(sel.track.state.list.selected(), Some(1)); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.selected_track(), Some(1)); sel.increment(albums, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(2)); - assert_eq!(sel.track.state.list.selected(), Some(0)); + assert_eq!(sel.selected(), Some(2)); + assert_eq!(sel.selected_track(), Some(0)); sel.decrement(albums, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(0)); - assert_eq!(sel.track.state.list.selected(), Some(0)); + 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.state.list.selected(), Some(albums.len() - 1)); - assert_eq!(sel.track.state.list.selected(), Some(0)); + assert_eq!(sel.selected(), Some(albums.len() - 1)); + assert_eq!(sel.selected_track(), 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)); + 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.state.list.selected(), Some(albums.len() - 1)); - assert_eq!(sel.track.state.list.selected(), Some(1)); + assert_eq!(sel.selected(), Some(albums.len() - 1)); + assert_eq!(sel.selected_track(), Some(1)); } #[test] diff --git a/src/tui/app/selection/artist.rs b/src/tui/app/selection/artist.rs index 02547b9..4b11287 100644 --- a/src/tui/app/selection/artist.rs +++ b/src/tui/app/selection/artist.rs @@ -204,59 +204,114 @@ mod tests { 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_selection() { + fn artist_delta_line() { 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); + assert_eq!(empty.selected(), None); + assert_eq!(empty.album.selected(), None); empty.increment(artists, Delta::Line); - assert_eq!(empty.state.list.selected(), None); - assert_eq!(empty.album.state.list.selected(), None); + assert_eq!(empty.selected(), None); + assert_eq!(empty.album.selected(), None); empty.decrement(artists, Delta::Line); - assert_eq!(empty.state.list.selected(), None); - assert_eq!(empty.album.state.list.selected(), None); + assert_eq!(empty.selected(), None); + assert_eq!(empty.album.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_eq!(sel.selected(), Some(0)); + assert_eq!(sel.album.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)); + 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.state.list.selected(), Some(0)); - assert_eq!(sel.album.state.list.selected(), Some(1)); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.album.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)); + assert_eq!(sel.selected(), Some(1)); + assert_eq!(sel.album.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)); + 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.state.list.selected(), Some(artists.len() - 1)); - assert_eq!(sel.album.state.list.selected(), Some(0)); + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.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)); + 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.state.list.selected(), Some(artists.len() - 1)); - assert_eq!(sel.album.state.list.selected(), Some(1)); + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.selected(), Some(1)); } #[test] @@ -265,46 +320,46 @@ mod tests { assert!(artists.len() > 1); let empty = ArtistSelection::initialise(&[]); - assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.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_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.state.list.selected(), Some(0)); - assert_eq!(sel.album.state.list.selected(), Some(1)); + 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.state.list.selected(), Some(0)); - assert_eq!(sel.album.state.list.selected(), Some(1)); + assert_eq!(sel.selected(), Some(0)); + assert_eq!(sel.album.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)); + assert_eq!(sel.selected(), Some(2)); + assert_eq!(sel.album.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)); + 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.state.list.selected(), Some(artists.len() - 1)); - assert_eq!(sel.album.state.list.selected(), Some(0)); + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.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)); + 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.state.list.selected(), Some(artists.len() - 1)); - assert_eq!(sel.album.state.list.selected(), Some(1)); + assert_eq!(sel.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.selected(), Some(1)); } #[test] diff --git a/src/tui/app/selection/mod.rs b/src/tui/app/selection/mod.rs index 27c8c2c..494a2e0 100644 --- a/src/tui/app/selection/mod.rs +++ b/src/tui/app/selection/mod.rs @@ -232,81 +232,148 @@ mod tests { use super::*; #[test] - fn selection() { + 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.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)); + 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.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)); + 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.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)); + 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.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)); + 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.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)); + 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.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)); + 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.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)); + 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.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)); + 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.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)); + 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.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)); + 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.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)); + 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.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)); + 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); @@ -314,8 +381,9 @@ mod tests { 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)); + 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 index 7a5b5a8..5620d1f 100644 --- a/src/tui/app/selection/track.rs +++ b/src/tui/app/selection/track.rs @@ -118,35 +118,53 @@ mod tests { use super::*; #[test] - fn track_selection() { + 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.state.list.selected(), None); + assert_eq!(empty.selected(), None); empty.increment(tracks, Delta::Line); - assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.selected(), None); empty.decrement(tracks, Delta::Line); - assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.selected(), None); let mut sel = TrackSelection::initialise(tracks); - assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.selected(), Some(0)); sel.decrement(tracks, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.selected(), Some(0)); sel.increment(tracks, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(1)); + assert_eq!(sel.selected(), Some(1)); sel.decrement(tracks, Delta::Line); - assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.selected(), Some(0)); for _ in 0..(tracks.len() + 5) { sel.increment(tracks, Delta::Line); } - assert_eq!(sel.state.list.selected(), Some(tracks.len() - 1)); + assert_eq!(sel.selected(), Some(tracks.len() - 1)); } #[test] @@ -155,27 +173,27 @@ mod tests { assert!(tracks.len() > 1); let empty = TrackSelection::initialise(&[]); - assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.selected(), None); let mut sel = TrackSelection::initialise(tracks); - assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.selected(), Some(0)); assert!(tracks.len() >= 4); sel.state.height = 3; sel.decrement(tracks, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.selected(), Some(0)); sel.increment(tracks, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(2)); + assert_eq!(sel.selected(), Some(2)); sel.decrement(tracks, Delta::Page); - assert_eq!(sel.state.list.selected(), Some(0)); + assert_eq!(sel.selected(), Some(0)); for _ in 0..(tracks.len() + 5) { sel.increment(tracks, Delta::Page); } - assert_eq!(sel.state.list.selected(), Some(tracks.len() - 1)); + assert_eq!(sel.selected(), Some(tracks.len() - 1)); } #[test] -- 2.45.2