Extend incremental search to albums and tracks (#152)
All checks were successful
Cargo CI / Build and Test (push) Successful in 1m39s
Cargo CI / Lint (push) Successful in 1m14s

Closes #145

Reviewed-on: #152
This commit is contained in:
Wojciech Kozlowski 2024-03-01 22:04:26 +01:00
parent fd19ea3eb3
commit 42d1edb69c
7 changed files with 1483 additions and 974 deletions

View File

@ -77,7 +77,7 @@ impl<MH: IMusicHoard> IAppInteractBrowse for AppMachine<MH, AppBrowse> {
let orig = ListSelection::get(&self.inner.selection); let orig = ListSelection::get(&self.inner.selection);
self.inner self.inner
.selection .selection
.reset_artist(self.inner.music_hoard.get_collection()); .reset(self.inner.music_hoard.get_collection());
AppMachine::search(self.inner, orig).into() AppMachine::search(self.inner, orig).into()
} }

View File

@ -1,13 +1,13 @@
use aho_corasick::AhoCorasick; use aho_corasick::AhoCorasick;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use musichoard::collection::artist::Artist; use musichoard::collection::{album::Album, artist::Artist, track::Track};
use crate::tui::{ use crate::tui::{
app::{ app::{
machine::{App, AppInner, AppMachine}, machine::{App, AppInner, AppMachine},
selection::ListSelection, selection::{ListSelection, SelectionState},
AppPublic, AppState, IAppInteractSearch, AppPublic, AppState, Category, IAppInteractSearch,
}, },
lib::IMusicHoard, lib::IMusicHoard,
}; };
@ -67,7 +67,7 @@ impl<MH: IMusicHoard> IAppInteractSearch for AppMachine<MH, AppSearch> {
fn append_character(mut self, ch: char) -> Self::APP { fn append_character(mut self, ch: char) -> Self::APP {
self.state.string.push(ch); 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.state.memo.push(AppSearchMemo { index, char: true });
self.incremental_search(false); self.incremental_search(false);
self.into() self.into()
@ -75,7 +75,7 @@ impl<MH: IMusicHoard> IAppInteractSearch for AppMachine<MH, AppSearch> {
fn search_next(mut self) -> Self::APP { fn search_next(mut self) -> Self::APP {
if !self.state.string.is_empty() { 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.state.memo.push(AppSearchMemo { index, char: false });
self.incremental_search(true); self.incremental_search(true);
} }
@ -88,7 +88,7 @@ impl<MH: IMusicHoard> IAppInteractSearch for AppMachine<MH, AppSearch> {
if memo.char { if memo.char {
self.state.string.pop(); self.state.string.pop();
} }
self.inner.selection.select_artist(collection, memo.index); self.inner.selection.select(collection, memo.index);
} }
self.into() self.into()
} }
@ -109,12 +109,18 @@ impl<MH: IMusicHoard> IAppInteractSearch for AppMachine<MH, AppSearch> {
trait IAppInteractSearchPrivate { trait IAppInteractSearchPrivate {
fn incremental_search(&mut self, next: bool); fn incremental_search(&mut self, next: bool);
fn incremental_search_predicate( fn next<P, T>(pred: P, name: &str, next: bool, st: SelectionState<'_, T>) -> Option<usize>
case_sensitive: bool, where
char_sensitive: bool, P: FnMut(bool, bool, &str, &T) -> bool;
search_name: &str,
probe: &Artist, fn search_artists(name: &str, next: bool, st: SelectionState<'_, Artist>) -> Option<usize>;
) -> bool; fn search_albums(name: &str, next: bool, st: SelectionState<'_, Album>) -> Option<usize>;
fn search_tracks(name: &str, next: bool, st: SelectionState<'_, Track>) -> Option<usize>;
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_case_sensitive(artist_name: &str) -> bool;
fn is_char_sensitive(artist_name: &str) -> bool; fn is_char_sensitive(artist_name: &str) -> bool;
@ -123,50 +129,86 @@ trait IAppInteractSearchPrivate {
impl<MH: IMusicHoard> IAppInteractSearchPrivate for AppMachine<MH, AppSearch> { impl<MH: IMusicHoard> IAppInteractSearchPrivate for AppMachine<MH, AppSearch> {
fn incremental_search(&mut self, next: bool) { fn incremental_search(&mut self, next: bool) {
let artists = self.inner.music_hoard.get_collection(); let collection = self.inner.music_hoard.get_collection();
let artist_name = &self.state.string; let search = &self.state.string;
let sel = &mut self.inner.selection; let sel = &self.inner.selection;
if let Some(mut index) = sel.selected_artist() { let result = match sel.active {
let case_sensitive = Self::is_case_sensitive(artist_name); Category::Artist => sel
let char_sensitive = Self::is_char_sensitive(artist_name); .state_artist(collection)
let search = Self::normalize_search(artist_name, !case_sensitive, !char_sensitive); .and_then(|state| Self::search_artists(search, next, state)),
Category::Album => sel
.state_album(collection)
.and_then(|state| Self::search_albums(search, next, state)),
Category::Track => sel
.state_track(collection)
.and_then(|state| Self::search_tracks(search, next, state)),
};
if next && ((index + 1) < artists.len()) { if result.is_some() {
index += 1; let collection = self.inner.music_hoard.get_collection();
} self.inner.selection.select(collection, result);
let slice = &artists[index..];
let result = slice.iter().position(|probe| {
Self::incremental_search_predicate(case_sensitive, char_sensitive, &search, probe)
});
if let Some(slice_index) = result {
sel.select_artist(artists, Some(index + slice_index));
}
} }
} }
fn incremental_search_predicate( fn search_artists(name: &str, next: bool, st: SelectionState<'_, Artist>) -> Option<usize> {
case_sensitive: bool, Self::next(Self::predicate_artists, name, next, st)
char_sensitive: bool, }
search_name: &str,
probe: &Artist, fn search_albums(name: &str, next: bool, st: SelectionState<'_, Album>) -> Option<usize> {
) -> bool { Self::next(Self::predicate_albums, name, next, st)
let name = Self::normalize_search(&probe.id.name, !case_sensitive, !char_sensitive); }
let mut result = name.starts_with(search_name);
fn search_tracks(name: &str, next: bool, st: SelectionState<'_, Track>) -> Option<usize> {
Self::next(Self::predicate_tracks, name, next, st)
}
fn next<P, T>(mut pred: P, name: &str, next: bool, st: SelectionState<'_, T>) -> Option<usize>
where
P: FnMut(bool, bool, &str, &T) -> bool,
{
let case_sens = Self::is_case_sensitive(name);
let char_sens = Self::is_char_sensitive(name);
let search = Self::normalize_search(name, !case_sens, !char_sens);
let mut index = st.index;
if next && ((index + 1) < st.list.len()) {
index += 1;
}
let slice = &st.list[index..];
slice
.iter()
.position(|probe| pred(case_sens, char_sens, &search, probe))
.map(|slice_index| index + slice_index)
}
fn predicate_artists(case_sens: bool, char_sens: bool, search: &str, probe: &Artist) -> bool {
let name = Self::normalize_search(&probe.id.name, !case_sens, !char_sens);
let mut result = name.starts_with(search);
if let Some(ref probe_sort) = probe.sort { if let Some(ref probe_sort) = probe.sort {
if !result { if !result {
let name = let name = Self::normalize_search(&probe_sort.name, !case_sens, !char_sens);
Self::normalize_search(&probe_sort.name, !case_sensitive, !char_sensitive); result = name.starts_with(search);
result = name.starts_with(search_name);
} }
} }
result 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 { fn is_case_sensitive(artist_name: &str) -> bool {
artist_name artist_name
.chars() .chars()
@ -330,6 +372,54 @@ mod tests {
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3));
} }
#[test]
fn album_incremental_search() {
let mut search =
AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
search.inner.selection.active = Category::Album;
let sel = &search.inner.selection;
assert_eq!(sel.artist.album.state.list.selected(), Some(0));
search.state.string = String::from("album_title ");
search.incremental_search(false);
let sel = &search.inner.selection;
assert_eq!(sel.artist.album.state.list.selected(), Some(0));
let search = search.append_character('a').unwrap_search();
let search = search.append_character('.').unwrap_search();
let search = search.append_character('b').unwrap_search();
let sel = &search.inner.selection;
assert_eq!(sel.artist.album.state.list.selected(), Some(1));
}
#[test]
fn track_incremental_search() {
let mut search =
AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
search.inner.selection.active = Category::Track;
let sel = &search.inner.selection;
assert_eq!(sel.artist.album.track.state.list.selected(), Some(0));
search.state.string = String::from("track ");
search.incremental_search(false);
let sel = &search.inner.selection;
assert_eq!(sel.artist.album.track.state.list.selected(), Some(0));
let search = search.append_character('a').unwrap_search();
let search = search.append_character('.').unwrap_search();
let search = search.append_character('a').unwrap_search();
let search = search.append_character('.').unwrap_search();
let search = search.append_character('2').unwrap_search();
let sel = &search.inner.selection;
assert_eq!(sel.artist.album.track.state.list.selected(), Some(1));
}
#[test] #[test]
fn search() { fn search() {
let search = AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2))); let search = AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2)));

View File

@ -1,931 +0,0 @@
use musichoard::collection::{
album::{Album, AlbumId},
artist::{Artist, ArtistId},
track::{Track, TrackId},
Collection,
};
use ratatui::widgets::ListState;
use std::cmp;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Category {
Artist,
Album,
Track,
}
#[derive(Clone, Debug, Default)]
pub struct WidgetState {
pub list: ListState,
pub height: usize,
}
impl PartialEq for WidgetState {
fn eq(&self, other: &Self) -> bool {
self.list.selected().eq(&other.list.selected()) && self.height.eq(&other.height)
}
}
pub struct Selection {
pub active: Category,
pub artist: ArtistSelection,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ArtistSelection {
pub state: WidgetState,
pub album: AlbumSelection,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AlbumSelection {
pub state: WidgetState,
pub track: TrackSelection,
}
#[derive(Clone, Debug, PartialEq)]
pub struct TrackSelection {
pub state: WidgetState,
}
pub enum Delta {
Line,
Page,
}
impl Delta {
fn as_usize(&self, state: &WidgetState) -> usize {
match self {
Delta::Line => 1,
Delta::Page => state.height.saturating_sub(1),
}
}
}
impl Selection {
pub fn new(artists: &[Artist]) -> Self {
Selection {
active: Category::Artist,
artist: ArtistSelection::initialise(artists),
}
}
pub fn select_by_list(&mut self, selected: ListSelection) {
self.artist.state.list = selected.artist;
self.artist.album.state.list = selected.album;
self.artist.album.track.state.list = selected.track;
}
pub fn select_by_id(&mut self, artists: &[Artist], selected: IdSelection) {
self.artist.reinitialise(artists, selected.artist);
}
pub fn select_artist(&mut self, artists: &[Artist], index: Option<usize>) {
self.artist.select(artists, index);
}
pub fn selected_artist(&self) -> Option<usize> {
self.artist.selected()
}
pub fn reset_artist(&mut self, artists: &[Artist]) {
if self.artist.state.list.selected() != Some(0) {
self.select_by_id(artists, IdSelection { artist: None });
}
}
pub fn increment_category(&mut self) {
self.active = match self.active {
Category::Artist => Category::Album,
Category::Album => Category::Track,
Category::Track => Category::Track,
};
}
pub fn decrement_category(&mut self) {
self.active = match self.active {
Category::Artist => Category::Artist,
Category::Album => Category::Artist,
Category::Track => Category::Album,
};
}
pub fn increment_selection(&mut self, collection: &Collection, delta: Delta) {
match self.active {
Category::Artist => self.increment_artist(collection, delta),
Category::Album => self.increment_album(collection, delta),
Category::Track => self.increment_track(collection, delta),
}
}
pub fn decrement_selection(&mut self, collection: &Collection, delta: Delta) {
match self.active {
Category::Artist => self.decrement_artist(collection, delta),
Category::Album => self.decrement_album(collection, delta),
Category::Track => self.decrement_track(collection, delta),
}
}
fn increment_artist(&mut self, artists: &[Artist], delta: Delta) {
self.artist.increment(artists, delta);
}
fn decrement_artist(&mut self, artists: &[Artist], delta: Delta) {
self.artist.decrement(artists, delta);
}
fn increment_album(&mut self, artists: &[Artist], delta: Delta) {
self.artist.increment_album(artists, delta);
}
fn decrement_album(&mut self, artists: &[Artist], delta: Delta) {
self.artist.decrement_album(artists, delta);
}
fn increment_track(&mut self, artists: &[Artist], delta: Delta) {
self.artist.increment_track(artists, delta);
}
fn decrement_track(&mut self, artists: &[Artist], delta: Delta) {
self.artist.decrement_track(artists, delta);
}
}
impl ArtistSelection {
fn initialise(artists: &[Artist]) -> Self {
let mut selection = ArtistSelection {
state: WidgetState::default(),
album: AlbumSelection::initialise(&[]),
};
selection.reinitialise(artists, None);
selection
}
fn reinitialise(&mut self, artists: &[Artist], active: Option<IdSelectArtist>) {
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<IdSelectAlbum>,
) {
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<usize> {
self.state.list.selected()
}
fn select(&mut self, artists: &[Artist], to: Option<usize>) {
match to {
Some(to) => self.select_to(artists, to),
None => self.state.list.select(None),
}
}
fn select_to(&mut self, artists: &[Artist], mut to: usize) {
to = cmp::min(to, artists.len() - 1);
if self.state.list.selected() != Some(to) {
self.state.list.select(Some(to));
self.album = AlbumSelection::initialise(&artists[to].albums);
}
}
fn increment_by(&mut self, artists: &[Artist], by: usize) {
if let Some(index) = self.state.list.selected() {
let result = index.saturating_add(by);
self.select_to(artists, result);
}
}
fn increment(&mut self, artists: &[Artist], delta: Delta) {
self.increment_by(artists, delta.as_usize(&self.state));
}
fn increment_album(&mut self, artists: &[Artist], delta: Delta) {
if let Some(index) = self.state.list.selected() {
self.album.increment(&artists[index].albums, delta);
}
}
fn increment_track(&mut self, artists: &[Artist], delta: Delta) {
if let Some(index) = self.state.list.selected() {
self.album.increment_track(&artists[index].albums, delta);
}
}
fn decrement_by(&mut self, artists: &[Artist], by: usize) {
if let Some(index) = self.state.list.selected() {
let result = index.saturating_sub(by);
if self.state.list.selected() != Some(result) {
self.state.list.select(Some(result));
self.album = AlbumSelection::initialise(&artists[result].albums);
}
}
}
fn decrement(&mut self, artists: &[Artist], delta: Delta) {
self.decrement_by(artists, delta.as_usize(&self.state));
}
fn decrement_album(&mut self, artists: &[Artist], delta: Delta) {
if let Some(index) = self.state.list.selected() {
self.album.decrement(&artists[index].albums, delta);
}
}
fn decrement_track(&mut self, artists: &[Artist], delta: Delta) {
if let Some(index) = self.state.list.selected() {
self.album.decrement_track(&artists[index].albums, delta);
}
}
}
impl AlbumSelection {
fn initialise(albums: &[Album]) -> Self {
let mut selection = AlbumSelection {
state: WidgetState::default(),
track: TrackSelection::initialise(&[]),
};
selection.reinitialise(albums, None);
selection
}
fn reinitialise(&mut self, albums: &[Album], album: Option<IdSelectAlbum>) {
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<IdSelectTrack>,
) {
if albums.is_empty() {
self.state.list.select(None);
self.track = TrackSelection::initialise(&[]);
} else if index >= albums.len() {
let end = albums.len() - 1;
self.state.list.select(Some(end));
self.track = TrackSelection::initialise(&albums[end].tracks);
} else {
self.state.list.select(Some(index));
self.track.reinitialise(&albums[index].tracks, active_track);
}
}
fn increment_by(&mut self, albums: &[Album], by: usize) {
if let Some(index) = self.state.list.selected() {
let mut result = index.saturating_add(by);
if result >= albums.len() {
result = albums.len() - 1;
}
if self.state.list.selected() != Some(result) {
self.state.list.select(Some(result));
self.track = TrackSelection::initialise(&albums[result].tracks);
}
}
}
fn increment(&mut self, albums: &[Album], delta: Delta) {
self.increment_by(albums, delta.as_usize(&self.state));
}
fn increment_track(&mut self, albums: &[Album], delta: Delta) {
if let Some(index) = self.state.list.selected() {
self.track.increment(&albums[index].tracks, delta);
}
}
fn decrement_by(&mut self, albums: &[Album], by: usize) {
if let Some(index) = self.state.list.selected() {
let result = index.saturating_sub(by);
if self.state.list.selected() != Some(result) {
self.state.list.select(Some(result));
self.track = TrackSelection::initialise(&albums[result].tracks);
}
}
}
fn decrement(&mut self, albums: &[Album], delta: Delta) {
self.decrement_by(albums, delta.as_usize(&self.state));
}
fn decrement_track(&mut self, albums: &[Album], delta: Delta) {
if let Some(index) = self.state.list.selected() {
self.track.decrement(&albums[index].tracks, delta);
}
}
}
impl TrackSelection {
fn initialise(tracks: &[Track]) -> Self {
let mut selection = TrackSelection {
state: WidgetState::default(),
};
selection.reinitialise(tracks, None);
selection
}
fn reinitialise(&mut self, tracks: &[Track], track: Option<IdSelectTrack>) {
if let Some(track) = track {
let result = tracks.binary_search_by(|t| t.get_sort_key().cmp(&track.track_id));
match result {
Ok(index) | Err(index) => self.reinitialise_with_index(tracks, index),
}
} else {
self.reinitialise_with_index(tracks, 0)
}
}
fn reinitialise_with_index(&mut self, tracks: &[Track], index: usize) {
if tracks.is_empty() {
self.state.list.select(None);
} else if index >= tracks.len() {
self.state.list.select(Some(tracks.len() - 1));
} else {
self.state.list.select(Some(index));
}
}
fn increment_by(&mut self, tracks: &[Track], by: usize) {
if let Some(index) = self.state.list.selected() {
let mut result = index.saturating_add(by);
if result >= tracks.len() {
result = tracks.len() - 1;
}
if self.state.list.selected() != Some(result) {
self.state.list.select(Some(result));
}
}
}
fn increment(&mut self, tracks: &[Track], delta: Delta) {
self.increment_by(tracks, delta.as_usize(&self.state));
}
fn decrement_by(&mut self, _tracks: &[Track], by: usize) {
if let Some(index) = self.state.list.selected() {
let result = index.saturating_sub(by);
if self.state.list.selected() != Some(result) {
self.state.list.select(Some(result));
}
}
}
fn decrement(&mut self, tracks: &[Track], delta: Delta) {
self.decrement_by(tracks, delta.as_usize(&self.state));
}
}
pub struct ListSelection {
pub artist: ListState,
pub album: ListState,
pub track: ListState,
}
impl ListSelection {
pub fn get(selection: &Selection) -> Self {
ListSelection {
artist: selection.artist.state.list.clone(),
album: selection.artist.album.state.list.clone(),
track: selection.artist.album.track.state.list.clone(),
}
}
}
pub struct IdSelection {
artist: Option<IdSelectArtist>,
}
struct IdSelectArtist {
artist_id: ArtistId,
album: Option<IdSelectAlbum>,
}
struct IdSelectAlbum {
album_id: AlbumId,
track: Option<IdSelectTrack>,
}
struct IdSelectTrack {
track_id: TrackId,
}
impl IdSelection {
pub fn get(collection: &Collection, selection: &Selection) -> Self {
IdSelection {
artist: IdSelectArtist::get(collection, &selection.artist),
}
}
}
impl IdSelectArtist {
fn get(artists: &[Artist], selection: &ArtistSelection) -> Option<Self> {
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<Self> {
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<Self> {
selection.state.list.selected().map(|index| {
let track = &tracks[index];
IdSelectTrack {
track_id: track.get_sort_key().clone(),
}
})
}
}
#[cfg(test)]
mod tests {
use crate::tui::testmod::COLLECTION;
use super::*;
#[test]
fn track_selection() {
let tracks = &COLLECTION[0].albums[0].tracks;
assert!(tracks.len() > 1);
let mut empty = TrackSelection::initialise(&[]);
assert_eq!(empty.state.list.selected(), None);
empty.increment(tracks, Delta::Line);
assert_eq!(empty.state.list.selected(), None);
empty.decrement(tracks, Delta::Line);
assert_eq!(empty.state.list.selected(), None);
let mut sel = TrackSelection::initialise(tracks);
assert_eq!(sel.state.list.selected(), Some(0));
sel.decrement(tracks, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(0));
sel.increment(tracks, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(1));
sel.decrement(tracks, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(0));
for _ in 0..(tracks.len() + 5) {
sel.increment(tracks, Delta::Line);
}
assert_eq!(sel.state.list.selected(), Some(tracks.len() - 1));
}
#[test]
fn track_delta_page() {
let tracks = &COLLECTION[0].albums[0].tracks;
assert!(tracks.len() > 1);
let empty = TrackSelection::initialise(&[]);
assert_eq!(empty.state.list.selected(), None);
let mut sel = TrackSelection::initialise(tracks);
assert_eq!(sel.state.list.selected(), Some(0));
assert!(tracks.len() >= 4);
sel.state.height = 3;
sel.decrement(tracks, Delta::Page);
assert_eq!(sel.state.list.selected(), Some(0));
sel.increment(tracks, Delta::Page);
assert_eq!(sel.state.list.selected(), Some(2));
sel.decrement(tracks, Delta::Page);
assert_eq!(sel.state.list.selected(), Some(0));
for _ in 0..(tracks.len() + 5) {
sel.increment(tracks, Delta::Page);
}
assert_eq!(sel.state.list.selected(), Some(tracks.len() - 1));
}
#[test]
fn track_reinitialise() {
let tracks = &COLLECTION[0].albums[0].tracks;
assert!(tracks.len() > 1);
let mut sel = TrackSelection::initialise(tracks);
sel.state.list.select(Some(tracks.len() - 1));
// Re-initialise.
let expected = sel.clone();
let active_track = IdSelectTrack::get(tracks, &sel);
sel.reinitialise(tracks, active_track);
assert_eq!(sel, expected);
// Re-initialise out-of-bounds.
let mut expected = sel.clone();
expected.decrement(tracks, Delta::Line);
let active_track = IdSelectTrack::get(tracks, &sel);
sel.reinitialise(&tracks[..(tracks.len() - 1)], active_track);
assert_eq!(sel, expected);
// Re-initialise empty.
let expected = TrackSelection::initialise(&[]);
let active_track = IdSelectTrack::get(tracks, &sel);
sel.reinitialise(&[], active_track);
assert_eq!(sel, expected);
}
#[test]
fn album_selection() {
let albums = &COLLECTION[0].albums;
assert!(albums.len() > 1);
let mut empty = AlbumSelection::initialise(&[]);
assert_eq!(empty.state.list.selected(), None);
assert_eq!(empty.track.state.list.selected(), None);
empty.increment(albums, Delta::Line);
assert_eq!(empty.state.list.selected(), None);
assert_eq!(empty.track.state.list.selected(), None);
empty.decrement(albums, Delta::Line);
assert_eq!(empty.state.list.selected(), None);
assert_eq!(empty.track.state.list.selected(), None);
let mut sel = AlbumSelection::initialise(albums);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.track.state.list.selected(), Some(0));
sel.increment_track(albums, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.track.state.list.selected(), Some(1));
// Verify that decrement that doesn't change index does not reset track.
sel.decrement(albums, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.track.state.list.selected(), Some(1));
sel.increment(albums, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(1));
assert_eq!(sel.track.state.list.selected(), Some(0));
sel.decrement(albums, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.track.state.list.selected(), Some(0));
for _ in 0..(albums.len() + 5) {
sel.increment(albums, Delta::Line);
}
assert_eq!(sel.state.list.selected(), Some(albums.len() - 1));
assert_eq!(sel.track.state.list.selected(), Some(0));
sel.increment_track(albums, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(albums.len() - 1));
assert_eq!(sel.track.state.list.selected(), Some(1));
// Verify that increment that doesn't change index does not reset track.
sel.increment(albums, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(albums.len() - 1));
assert_eq!(sel.track.state.list.selected(), Some(1));
}
#[test]
fn album_delta_page() {
let albums = &COLLECTION[1].albums;
assert!(albums.len() > 1);
let empty = AlbumSelection::initialise(&[]);
assert_eq!(empty.state.list.selected(), None);
let mut sel = AlbumSelection::initialise(albums);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.track.state.list.selected(), Some(0));
assert!(albums.len() >= 4);
sel.state.height = 3;
sel.increment_track(albums, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.track.state.list.selected(), Some(1));
// Verify that decrement that doesn't change index does not reset track.
sel.decrement(albums, Delta::Page);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.track.state.list.selected(), Some(1));
sel.increment(albums, Delta::Page);
assert_eq!(sel.state.list.selected(), Some(2));
assert_eq!(sel.track.state.list.selected(), Some(0));
sel.decrement(albums, Delta::Page);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.track.state.list.selected(), Some(0));
for _ in 0..(albums.len() + 5) {
sel.increment(albums, Delta::Page);
}
assert_eq!(sel.state.list.selected(), Some(albums.len() - 1));
assert_eq!(sel.track.state.list.selected(), Some(0));
sel.increment_track(albums, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(albums.len() - 1));
assert_eq!(sel.track.state.list.selected(), Some(1));
// Verify that increment that doesn't change index does not reset track.
sel.increment(albums, Delta::Page);
assert_eq!(sel.state.list.selected(), Some(albums.len() - 1));
assert_eq!(sel.track.state.list.selected(), Some(1));
}
#[test]
fn album_reinitialise() {
let albums = &COLLECTION[0].albums;
assert!(albums.len() > 1);
let mut sel = AlbumSelection::initialise(albums);
sel.state.list.select(Some(albums.len() - 1));
sel.track.state.list.select(Some(1));
// Re-initialise.
let expected = sel.clone();
let active_album = IdSelectAlbum::get(albums, &sel);
sel.reinitialise(albums, active_album);
assert_eq!(sel, expected);
// Re-initialise out-of-bounds.
let mut expected = sel.clone();
expected.decrement(albums, Delta::Line);
let active_album = IdSelectAlbum::get(albums, &sel);
sel.reinitialise(&albums[..(albums.len() - 1)], active_album);
assert_eq!(sel, expected);
// Re-initialise empty.
let expected = AlbumSelection::initialise(&[]);
let active_album = IdSelectAlbum::get(albums, &sel);
sel.reinitialise(&[], active_album);
assert_eq!(sel, expected);
}
#[test]
fn artist_selection() {
let artists = &COLLECTION;
assert!(artists.len() > 1);
let mut empty = ArtistSelection::initialise(&[]);
assert_eq!(empty.state.list.selected(), None);
assert_eq!(empty.album.state.list.selected(), None);
empty.increment(artists, Delta::Line);
assert_eq!(empty.state.list.selected(), None);
assert_eq!(empty.album.state.list.selected(), None);
empty.decrement(artists, Delta::Line);
assert_eq!(empty.state.list.selected(), None);
assert_eq!(empty.album.state.list.selected(), None);
let mut sel = ArtistSelection::initialise(artists);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.album.state.list.selected(), Some(0));
sel.increment_album(artists, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.album.state.list.selected(), Some(1));
// Verify that decrement that doesn't change index does not reset album.
sel.decrement(artists, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.album.state.list.selected(), Some(1));
sel.increment(artists, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(1));
assert_eq!(sel.album.state.list.selected(), Some(0));
sel.decrement(artists, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.album.state.list.selected(), Some(0));
for _ in 0..(artists.len() + 5) {
sel.increment(artists, Delta::Line);
}
assert_eq!(sel.state.list.selected(), Some(artists.len() - 1));
assert_eq!(sel.album.state.list.selected(), Some(0));
sel.increment_album(artists, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(artists.len() - 1));
assert_eq!(sel.album.state.list.selected(), Some(1));
// Verify that increment that doesn't change index does not reset album.
sel.increment(artists, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(artists.len() - 1));
assert_eq!(sel.album.state.list.selected(), Some(1));
}
#[test]
fn artist_delta_page() {
let artists = &COLLECTION;
assert!(artists.len() > 1);
let empty = ArtistSelection::initialise(&[]);
assert_eq!(empty.state.list.selected(), None);
let mut sel = ArtistSelection::initialise(artists);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.album.state.list.selected(), Some(0));
assert!(artists.len() >= 4);
sel.state.height = 3;
sel.increment_album(artists, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.album.state.list.selected(), Some(1));
// Verify that decrement that doesn't change index does not reset album.
sel.decrement(artists, Delta::Page);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.album.state.list.selected(), Some(1));
sel.increment(artists, Delta::Page);
assert_eq!(sel.state.list.selected(), Some(2));
assert_eq!(sel.album.state.list.selected(), Some(0));
sel.decrement(artists, Delta::Page);
assert_eq!(sel.state.list.selected(), Some(0));
assert_eq!(sel.album.state.list.selected(), Some(0));
for _ in 0..(artists.len() + 5) {
sel.increment(artists, Delta::Page);
}
assert_eq!(sel.state.list.selected(), Some(artists.len() - 1));
assert_eq!(sel.album.state.list.selected(), Some(0));
sel.increment_album(artists, Delta::Line);
assert_eq!(sel.state.list.selected(), Some(artists.len() - 1));
assert_eq!(sel.album.state.list.selected(), Some(1));
// Verify that increment that doesn't change index does not reset album.
sel.increment(artists, Delta::Page);
assert_eq!(sel.state.list.selected(), Some(artists.len() - 1));
assert_eq!(sel.album.state.list.selected(), Some(1));
}
#[test]
fn artist_reinitialise() {
let artists = &COLLECTION;
assert!(artists.len() > 1);
let mut sel = ArtistSelection::initialise(artists);
sel.state.list.select(Some(artists.len() - 1));
sel.album.state.list.select(Some(1));
// Re-initialise.
let expected = sel.clone();
let active_artist = IdSelectArtist::get(artists, &sel);
sel.reinitialise(artists, active_artist);
assert_eq!(sel, expected);
// Re-initialise out-of-bounds.
let mut expected = sel.clone();
expected.decrement(artists, Delta::Line);
let active_artist = IdSelectArtist::get(artists, &sel);
sel.reinitialise(&artists[..(artists.len() - 1)], active_artist);
assert_eq!(sel, expected);
// Re-initialise empty.
let expected = ArtistSelection::initialise(&[]);
let active_artist = IdSelectArtist::get(artists, &sel);
sel.reinitialise(&[], active_artist);
assert_eq!(sel, expected);
}
#[test]
fn selection() {
let mut selection = Selection::new(&COLLECTION);
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
selection.increment_selection(&COLLECTION, Delta::Line);
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
selection.increment_category();
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
selection.increment_selection(&COLLECTION, Delta::Line);
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
selection.increment_category();
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
selection.increment_selection(&COLLECTION, Delta::Line);
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(1));
selection.increment_category();
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(1));
selection.decrement_selection(&COLLECTION, Delta::Line);
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
selection.increment_selection(&COLLECTION, Delta::Line);
selection.decrement_category();
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(1));
selection.decrement_selection(&COLLECTION, Delta::Line);
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
selection.increment_selection(&COLLECTION, Delta::Line);
selection.decrement_category();
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
selection.decrement_selection(&COLLECTION, Delta::Line);
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
selection.increment_category();
selection.increment_selection(&COLLECTION, Delta::Line);
selection.decrement_category();
selection.decrement_selection(&COLLECTION, Delta::Line);
selection.decrement_category();
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
}
}

View File

@ -0,0 +1,342 @@
use std::cmp;
use musichoard::collection::{
album::{Album, AlbumId},
track::Track,
};
use crate::tui::app::selection::{
track::{IdSelectTrack, TrackSelection},
Delta, SelectionState, WidgetState,
};
#[derive(Clone, Debug, PartialEq)]
pub struct AlbumSelection {
pub state: WidgetState,
pub track: TrackSelection,
}
impl AlbumSelection {
pub fn initialise(albums: &[Album]) -> Self {
let mut selection = AlbumSelection {
state: WidgetState::default(),
track: TrackSelection::initialise(&[]),
};
selection.reinitialise(albums, None);
selection
}
pub fn reinitialise(&mut self, albums: &[Album], album: Option<IdSelectAlbum>) {
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<IdSelectTrack>,
) {
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<usize> {
self.state.list.selected()
}
pub fn selected_track(&self) -> Option<usize> {
self.track.selected()
}
pub fn select(&mut self, albums: &[Album], to: Option<usize>) {
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<usize>) {
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<SelectionState<'a, Album>> {
let selected = self.state.list.selected();
selected.map(|index| SelectionState { list, index })
}
pub fn state_tracks<'a>(&self, albums: &'a [Album]) -> Option<SelectionState<'a, Track>> {
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<IdSelectTrack>,
}
impl IdSelectAlbum {
pub fn get(albums: &[Album], selection: &AlbumSelection) -> Option<Self> {
selection.state.list.selected().map(|index| {
let album = &albums[index];
IdSelectAlbum {
album_id: album.get_sort_key().clone(),
track: IdSelectTrack::get(&album.tracks, &selection.track),
}
})
}
}
#[cfg(test)]
mod tests {
use crate::tui::testmod::COLLECTION;
use super::*;
#[test]
fn album_select() {
let albums = &COLLECTION[0].albums;
assert!(albums.len() > 1);
let mut sel = AlbumSelection::initialise(albums);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.selected_track(), Some(0));
sel.select(albums, None);
assert_eq!(sel.selected(), None);
assert_eq!(sel.selected_track(), Some(0));
sel.select(albums, Some(albums.len()));
assert_eq!(sel.selected(), Some(albums.len() - 1));
assert_eq!(sel.selected_track(), Some(0));
sel.select_track(albums, None);
assert_eq!(sel.selected(), Some(albums.len() - 1));
assert_eq!(sel.selected_track(), None);
sel.reset_track(albums);
assert_eq!(sel.selected(), Some(albums.len() - 1));
assert_eq!(sel.selected_track(), Some(0));
sel.select_track(albums, Some(1));
assert_eq!(sel.selected(), Some(albums.len() - 1));
assert_eq!(sel.selected_track(), Some(1));
sel.reset(albums);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.selected_track(), Some(0));
}
#[test]
fn album_delta_line() {
let albums = &COLLECTION[0].albums;
assert!(albums.len() > 1);
let mut empty = AlbumSelection::initialise(&[]);
assert_eq!(empty.selected(), None);
assert_eq!(empty.selected_track(), None);
empty.increment(albums, Delta::Line);
assert_eq!(empty.selected(), None);
assert_eq!(empty.selected_track(), None);
empty.decrement(albums, Delta::Line);
assert_eq!(empty.selected(), None);
assert_eq!(empty.selected_track(), None);
let mut sel = AlbumSelection::initialise(albums);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.selected_track(), Some(0));
sel.increment_track(albums, Delta::Line);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.selected_track(), Some(1));
// Verify that decrement that doesn't change index does not reset track.
sel.decrement(albums, Delta::Line);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.selected_track(), Some(1));
sel.increment(albums, Delta::Line);
assert_eq!(sel.selected(), Some(1));
assert_eq!(sel.selected_track(), Some(0));
sel.decrement(albums, Delta::Line);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.selected_track(), Some(0));
for _ in 0..(albums.len() + 5) {
sel.increment(albums, Delta::Line);
}
assert_eq!(sel.selected(), Some(albums.len() - 1));
assert_eq!(sel.selected_track(), Some(0));
sel.increment_track(albums, Delta::Line);
assert_eq!(sel.selected(), Some(albums.len() - 1));
assert_eq!(sel.selected_track(), Some(1));
// Verify that increment that doesn't change index does not reset track.
sel.increment(albums, Delta::Line);
assert_eq!(sel.selected(), Some(albums.len() - 1));
assert_eq!(sel.selected_track(), Some(1));
}
#[test]
fn album_delta_page() {
let albums = &COLLECTION[1].albums;
assert!(albums.len() > 1);
let empty = AlbumSelection::initialise(&[]);
assert_eq!(empty.selected(), None);
let mut sel = AlbumSelection::initialise(albums);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.selected_track(), Some(0));
assert!(albums.len() >= 4);
sel.state.height = 3;
sel.increment_track(albums, Delta::Line);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.selected_track(), Some(1));
// Verify that decrement that doesn't change index does not reset track.
sel.decrement(albums, Delta::Page);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.selected_track(), Some(1));
sel.increment(albums, Delta::Page);
assert_eq!(sel.selected(), Some(2));
assert_eq!(sel.selected_track(), Some(0));
sel.decrement(albums, Delta::Page);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.selected_track(), Some(0));
for _ in 0..(albums.len() + 5) {
sel.increment(albums, Delta::Page);
}
assert_eq!(sel.selected(), Some(albums.len() - 1));
assert_eq!(sel.selected_track(), Some(0));
sel.increment_track(albums, Delta::Line);
assert_eq!(sel.selected(), Some(albums.len() - 1));
assert_eq!(sel.selected_track(), Some(1));
// Verify that increment that doesn't change index does not reset track.
sel.increment(albums, Delta::Page);
assert_eq!(sel.selected(), Some(albums.len() - 1));
assert_eq!(sel.selected_track(), Some(1));
}
#[test]
fn album_reinitialise() {
let albums = &COLLECTION[0].albums;
assert!(albums.len() > 1);
let mut sel = AlbumSelection::initialise(albums);
sel.state.list.select(Some(albums.len() - 1));
sel.track.state.list.select(Some(1));
// Re-initialise.
let expected = sel.clone();
let active_album = IdSelectAlbum::get(albums, &sel);
sel.reinitialise(albums, active_album);
assert_eq!(sel, expected);
// Re-initialise out-of-bounds.
let mut expected = sel.clone();
expected.decrement(albums, Delta::Line);
let active_album = IdSelectAlbum::get(albums, &sel);
sel.reinitialise(&albums[..(albums.len() - 1)], active_album);
assert_eq!(sel, expected);
// Re-initialise empty.
let expected = AlbumSelection::initialise(&[]);
let active_album = IdSelectAlbum::get(albums, &sel);
sel.reinitialise(&[], active_album);
assert_eq!(sel, expected);
}
}

View File

@ -0,0 +1,393 @@
use std::cmp;
use musichoard::collection::{
album::Album,
artist::{Artist, ArtistId},
track::Track,
};
use crate::tui::app::selection::{
album::{AlbumSelection, IdSelectAlbum},
Delta, SelectionState, WidgetState,
};
#[derive(Clone, Debug, PartialEq)]
pub struct ArtistSelection {
pub state: WidgetState,
pub album: AlbumSelection,
}
impl ArtistSelection {
pub fn initialise(artists: &[Artist]) -> Self {
let mut selection = ArtistSelection {
state: WidgetState::default(),
album: AlbumSelection::initialise(&[]),
};
selection.reinitialise(artists, None);
selection
}
pub fn reinitialise(&mut self, artists: &[Artist], active: Option<IdSelectArtist>) {
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<IdSelectAlbum>,
) {
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<usize> {
self.state.list.selected()
}
pub fn selected_album(&self) -> Option<usize> {
self.album.selected()
}
pub fn selected_track(&self) -> Option<usize> {
self.album.selected_track()
}
pub fn select(&mut self, artists: &[Artist], to: Option<usize>) {
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<usize>) {
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<usize>) {
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<SelectionState<'a, Artist>> {
let selected = self.state.list.selected();
selected.map(|index| SelectionState { list, index })
}
pub fn state_album<'a>(&self, artists: &'a [Artist]) -> Option<SelectionState<'a, Album>> {
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<SelectionState<'a, Track>> {
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<IdSelectAlbum>,
}
impl IdSelectArtist {
pub fn get(artists: &[Artist], selection: &ArtistSelection) -> Option<Self> {
selection.state.list.selected().map(|index| {
let artist = &artists[index];
IdSelectArtist {
artist_id: artist.get_sort_key().clone(),
album: IdSelectAlbum::get(&artist.albums, &selection.album),
}
})
}
}
#[cfg(test)]
mod tests {
use crate::tui::testmod::COLLECTION;
use super::*;
#[test]
fn artist_select() {
let artists = &COLLECTION;
assert!(artists.len() > 1);
let mut sel = ArtistSelection::initialise(artists);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.selected_album(), Some(0));
assert_eq!(sel.selected_track(), Some(0));
sel.select(artists, None);
assert_eq!(sel.selected(), None);
assert_eq!(sel.selected_album(), Some(0));
assert_eq!(sel.selected_track(), Some(0));
sel.select(artists, Some(artists.len()));
assert_eq!(sel.selected(), Some(artists.len() - 1));
assert_eq!(sel.selected_album(), Some(0));
assert_eq!(sel.selected_track(), Some(0));
sel.select_track(artists, None);
assert_eq!(sel.selected(), Some(artists.len() - 1));
assert_eq!(sel.selected_album(), Some(0));
assert_eq!(sel.selected_track(), None);
sel.select_album(artists, None);
assert_eq!(sel.selected(), Some(artists.len() - 1));
assert_eq!(sel.selected_album(), None);
assert_eq!(sel.selected_track(), None);
sel.reset_album(artists);
assert_eq!(sel.selected(), Some(artists.len() - 1));
assert_eq!(sel.selected_album(), Some(0));
assert_eq!(sel.selected_track(), Some(0));
sel.select_track(artists, None);
assert_eq!(sel.selected(), Some(artists.len() - 1));
assert_eq!(sel.selected_album(), Some(0));
assert_eq!(sel.selected_track(), None);
sel.reset_track(artists);
assert_eq!(sel.selected(), Some(artists.len() - 1));
assert_eq!(sel.selected_album(), Some(0));
assert_eq!(sel.selected_track(), Some(0));
sel.select_album(artists, Some(1));
assert_eq!(sel.selected(), Some(artists.len() - 1));
assert_eq!(sel.selected_album(), Some(1));
assert_eq!(sel.selected_track(), Some(0));
sel.reset(artists);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.selected_album(), Some(0));
assert_eq!(sel.selected_track(), Some(0));
}
#[test]
fn artist_delta_line() {
let artists = &COLLECTION;
assert!(artists.len() > 1);
let mut empty = ArtistSelection::initialise(&[]);
assert_eq!(empty.selected(), None);
assert_eq!(empty.album.selected(), None);
empty.increment(artists, Delta::Line);
assert_eq!(empty.selected(), None);
assert_eq!(empty.album.selected(), None);
empty.decrement(artists, Delta::Line);
assert_eq!(empty.selected(), None);
assert_eq!(empty.album.selected(), None);
let mut sel = ArtistSelection::initialise(artists);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.album.selected(), Some(0));
sel.increment_album(artists, Delta::Line);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.album.selected(), Some(1));
// Verify that decrement that doesn't change index does not reset album.
sel.decrement(artists, Delta::Line);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.album.selected(), Some(1));
sel.increment(artists, Delta::Line);
assert_eq!(sel.selected(), Some(1));
assert_eq!(sel.album.selected(), Some(0));
sel.decrement(artists, Delta::Line);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.album.selected(), Some(0));
for _ in 0..(artists.len() + 5) {
sel.increment(artists, Delta::Line);
}
assert_eq!(sel.selected(), Some(artists.len() - 1));
assert_eq!(sel.album.selected(), Some(0));
sel.increment_album(artists, Delta::Line);
assert_eq!(sel.selected(), Some(artists.len() - 1));
assert_eq!(sel.album.selected(), Some(1));
// Verify that increment that doesn't change index does not reset album.
sel.increment(artists, Delta::Line);
assert_eq!(sel.selected(), Some(artists.len() - 1));
assert_eq!(sel.album.selected(), Some(1));
}
#[test]
fn artist_delta_page() {
let artists = &COLLECTION;
assert!(artists.len() > 1);
let empty = ArtistSelection::initialise(&[]);
assert_eq!(empty.selected(), None);
let mut sel = ArtistSelection::initialise(artists);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.album.selected(), Some(0));
assert!(artists.len() >= 4);
sel.state.height = 3;
sel.increment_album(artists, Delta::Line);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.album.selected(), Some(1));
// Verify that decrement that doesn't change index does not reset album.
sel.decrement(artists, Delta::Page);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.album.selected(), Some(1));
sel.increment(artists, Delta::Page);
assert_eq!(sel.selected(), Some(2));
assert_eq!(sel.album.selected(), Some(0));
sel.decrement(artists, Delta::Page);
assert_eq!(sel.selected(), Some(0));
assert_eq!(sel.album.selected(), Some(0));
for _ in 0..(artists.len() + 5) {
sel.increment(artists, Delta::Page);
}
assert_eq!(sel.selected(), Some(artists.len() - 1));
assert_eq!(sel.album.selected(), Some(0));
sel.increment_album(artists, Delta::Line);
assert_eq!(sel.selected(), Some(artists.len() - 1));
assert_eq!(sel.album.selected(), Some(1));
// Verify that increment that doesn't change index does not reset album.
sel.increment(artists, Delta::Page);
assert_eq!(sel.selected(), Some(artists.len() - 1));
assert_eq!(sel.album.selected(), Some(1));
}
#[test]
fn artist_reinitialise() {
let artists = &COLLECTION;
assert!(artists.len() > 1);
let mut sel = ArtistSelection::initialise(artists);
sel.state.list.select(Some(artists.len() - 1));
sel.album.state.list.select(Some(1));
// Re-initialise.
let expected = sel.clone();
let active_artist = IdSelectArtist::get(artists, &sel);
sel.reinitialise(artists, active_artist);
assert_eq!(sel, expected);
// Re-initialise out-of-bounds.
let mut expected = sel.clone();
expected.decrement(artists, Delta::Line);
let active_artist = IdSelectArtist::get(artists, &sel);
sel.reinitialise(&artists[..(artists.len() - 1)], active_artist);
assert_eq!(sel, expected);
// Re-initialise empty.
let expected = ArtistSelection::initialise(&[]);
let active_artist = IdSelectArtist::get(artists, &sel);
sel.reinitialise(&[], active_artist);
assert_eq!(sel, expected);
}
}

View File

@ -0,0 +1,389 @@
mod album;
mod artist;
mod track;
use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection};
use ratatui::widgets::ListState;
use artist::{ArtistSelection, IdSelectArtist};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Category {
Artist,
Album,
Track,
}
#[derive(Clone, Debug, Default)]
pub struct WidgetState {
pub list: ListState,
pub height: usize,
}
impl PartialEq for WidgetState {
fn eq(&self, other: &Self) -> bool {
self.list.selected().eq(&other.list.selected()) && self.height.eq(&other.height)
}
}
pub struct Selection {
pub active: Category,
pub artist: ArtistSelection,
}
pub struct SelectionState<'a, T> {
pub list: &'a [T],
pub index: usize,
}
pub enum Delta {
Line,
Page,
}
impl Delta {
fn as_usize(&self, state: &WidgetState) -> usize {
match self {
Delta::Line => 1,
Delta::Page => state.height.saturating_sub(1),
}
}
}
impl Selection {
pub fn new(artists: &[Artist]) -> Self {
Selection {
active: Category::Artist,
artist: ArtistSelection::initialise(artists),
}
}
pub fn select_by_list(&mut self, selected: ListSelection) {
self.artist.state.list = selected.artist;
self.artist.album.state.list = selected.album;
self.artist.album.track.state.list = selected.track;
}
pub fn select_by_id(&mut self, artists: &[Artist], selected: IdSelection) {
self.artist.reinitialise(artists, selected.artist);
}
pub fn increment_category(&mut self) {
self.active = match self.active {
Category::Artist => Category::Album,
Category::Album => Category::Track,
Category::Track => Category::Track,
};
}
pub fn decrement_category(&mut self) {
self.active = match self.active {
Category::Artist => Category::Artist,
Category::Album => Category::Artist,
Category::Track => Category::Album,
};
}
pub fn select(&mut self, collection: &Collection, index: Option<usize>) {
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<usize>) {
self.artist.select(artists, index);
}
fn select_album(&mut self, artists: &[Artist], index: Option<usize>) {
self.artist.select_album(artists, index);
}
fn select_track(&mut self, artists: &[Artist], index: Option<usize>) {
self.artist.select_track(artists, index);
}
pub fn selected(&self) -> Option<usize> {
match self.active {
Category::Artist => self.selected_artist(),
Category::Album => self.selected_album(),
Category::Track => self.selected_track(),
}
}
fn selected_artist(&self) -> Option<usize> {
self.artist.selected()
}
fn selected_album(&self) -> Option<usize> {
self.artist.selected_album()
}
fn selected_track(&self) -> Option<usize> {
self.artist.selected_track()
}
pub fn state_artist<'a>(&self, coll: &'a Collection) -> Option<SelectionState<'a, Artist>> {
self.artist.selection_state(coll)
}
pub fn state_album<'a>(&self, coll: &'a Collection) -> Option<SelectionState<'a, Album>> {
self.artist.state_album(coll)
}
pub fn state_track<'a>(&self, coll: &'a Collection) -> Option<SelectionState<'a, Track>> {
self.artist.state_track(coll)
}
pub fn reset(&mut self, collection: &Collection) {
match self.active {
Category::Artist => self.reset_artist(collection),
Category::Album => self.reset_album(collection),
Category::Track => self.reset_track(collection),
}
}
fn reset_artist(&mut self, artists: &[Artist]) {
self.artist.reset(artists);
}
fn reset_album(&mut self, artists: &[Artist]) {
self.artist.reset_album(artists);
}
fn reset_track(&mut self, artists: &[Artist]) {
self.artist.reset_track(artists);
}
pub fn increment_selection(&mut self, collection: &Collection, delta: Delta) {
match self.active {
Category::Artist => self.increment_artist(collection, delta),
Category::Album => self.increment_album(collection, delta),
Category::Track => self.increment_track(collection, delta),
}
}
fn increment_artist(&mut self, artists: &[Artist], delta: Delta) {
self.artist.increment(artists, delta);
}
fn increment_album(&mut self, artists: &[Artist], delta: Delta) {
self.artist.increment_album(artists, delta);
}
fn increment_track(&mut self, artists: &[Artist], delta: Delta) {
self.artist.increment_track(artists, delta);
}
pub fn decrement_selection(&mut self, collection: &Collection, delta: Delta) {
match self.active {
Category::Artist => self.decrement_artist(collection, delta),
Category::Album => self.decrement_album(collection, delta),
Category::Track => self.decrement_track(collection, delta),
}
}
fn decrement_artist(&mut self, artists: &[Artist], delta: Delta) {
self.artist.decrement(artists, delta);
}
fn decrement_album(&mut self, artists: &[Artist], delta: Delta) {
self.artist.decrement_album(artists, delta);
}
fn decrement_track(&mut self, artists: &[Artist], delta: Delta) {
self.artist.decrement_track(artists, delta);
}
}
pub struct ListSelection {
pub artist: ListState,
pub album: ListState,
pub track: ListState,
}
impl ListSelection {
pub fn get(selection: &Selection) -> Self {
ListSelection {
artist: selection.artist.state.list.clone(),
album: selection.artist.album.state.list.clone(),
track: selection.artist.album.track.state.list.clone(),
}
}
}
pub struct IdSelection {
artist: Option<IdSelectArtist>,
}
impl IdSelection {
pub fn get(collection: &Collection, selection: &Selection) -> Self {
IdSelection {
artist: IdSelectArtist::get(collection, &selection.artist),
}
}
}
#[cfg(test)]
mod tests {
use crate::tui::testmod::COLLECTION;
use super::*;
#[test]
fn selection_select() {
let mut selection = Selection::new(&COLLECTION);
selection.select(&COLLECTION, Some(1));
selection.increment_category();
selection.select(&COLLECTION, Some(1));
selection.increment_category();
selection.select(&COLLECTION, Some(1));
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.selected(), Some(1));
assert_eq!(selection.artist.album.selected(), Some(1));
assert_eq!(selection.artist.album.track.selected(), Some(1));
selection.reset(&COLLECTION);
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.selected(), Some(1));
assert_eq!(selection.artist.album.selected(), Some(1));
assert_eq!(selection.artist.album.track.selected(), Some(0));
selection.select(&COLLECTION, Some(1));
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.selected(), Some(1));
assert_eq!(selection.artist.album.selected(), Some(1));
assert_eq!(selection.artist.album.track.selected(), Some(1));
selection.decrement_category();
selection.reset(&COLLECTION);
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.artist.selected(), Some(1));
assert_eq!(selection.artist.album.selected(), Some(0));
assert_eq!(selection.artist.album.track.selected(), Some(0));
selection.select(&COLLECTION, Some(1));
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.artist.selected(), Some(1));
assert_eq!(selection.artist.album.selected(), Some(1));
assert_eq!(selection.artist.album.track.selected(), Some(0));
selection.decrement_category();
selection.reset(&COLLECTION);
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.selected(), Some(0));
assert_eq!(selection.artist.album.selected(), Some(0));
assert_eq!(selection.artist.album.track.selected(), Some(0));
}
#[test]
fn selection_delta() {
let mut selection = Selection::new(&COLLECTION);
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.selected(), Some(0));
assert_eq!(selection.artist.selected(), Some(0));
assert_eq!(selection.artist.album.selected(), Some(0));
assert_eq!(selection.artist.album.track.selected(), Some(0));
selection.increment_selection(&COLLECTION, Delta::Line);
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.selected(), Some(1));
assert_eq!(selection.artist.selected(), Some(1));
assert_eq!(selection.artist.album.selected(), Some(0));
assert_eq!(selection.artist.album.track.selected(), Some(0));
selection.increment_category();
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.selected(), Some(0));
assert_eq!(selection.artist.selected(), Some(1));
assert_eq!(selection.artist.album.selected(), Some(0));
assert_eq!(selection.artist.album.track.selected(), Some(0));
selection.increment_selection(&COLLECTION, Delta::Line);
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.selected(), Some(1));
assert_eq!(selection.artist.selected(), Some(1));
assert_eq!(selection.artist.album.selected(), Some(1));
assert_eq!(selection.artist.album.track.selected(), Some(0));
selection.increment_category();
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.selected(), Some(0));
assert_eq!(selection.artist.selected(), Some(1));
assert_eq!(selection.artist.album.selected(), Some(1));
assert_eq!(selection.artist.album.track.selected(), Some(0));
selection.increment_selection(&COLLECTION, Delta::Line);
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.selected(), Some(1));
assert_eq!(selection.artist.selected(), Some(1));
assert_eq!(selection.artist.album.selected(), Some(1));
assert_eq!(selection.artist.album.track.selected(), Some(1));
selection.increment_category();
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.selected(), Some(1));
assert_eq!(selection.artist.selected(), Some(1));
assert_eq!(selection.artist.album.selected(), Some(1));
assert_eq!(selection.artist.album.track.selected(), Some(1));
selection.decrement_selection(&COLLECTION, Delta::Line);
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.selected(), Some(0));
assert_eq!(selection.artist.selected(), Some(1));
assert_eq!(selection.artist.album.selected(), Some(1));
assert_eq!(selection.artist.album.track.selected(), Some(0));
selection.increment_selection(&COLLECTION, Delta::Line);
selection.decrement_category();
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.selected(), Some(1));
assert_eq!(selection.artist.selected(), Some(1));
assert_eq!(selection.artist.album.selected(), Some(1));
assert_eq!(selection.artist.album.track.selected(), Some(1));
selection.decrement_selection(&COLLECTION, Delta::Line);
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.selected(), Some(0));
assert_eq!(selection.artist.selected(), Some(1));
assert_eq!(selection.artist.album.selected(), Some(0));
assert_eq!(selection.artist.album.track.selected(), Some(0));
selection.increment_selection(&COLLECTION, Delta::Line);
selection.decrement_category();
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.selected(), Some(1));
assert_eq!(selection.artist.selected(), Some(1));
assert_eq!(selection.artist.album.selected(), Some(1));
assert_eq!(selection.artist.album.track.selected(), Some(0));
selection.decrement_selection(&COLLECTION, Delta::Line);
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.selected(), Some(0));
assert_eq!(selection.artist.selected(), Some(0));
assert_eq!(selection.artist.album.selected(), Some(0));
assert_eq!(selection.artist.album.track.selected(), Some(0));
selection.increment_category();
selection.increment_selection(&COLLECTION, Delta::Line);
selection.decrement_category();
selection.decrement_selection(&COLLECTION, Delta::Line);
selection.decrement_category();
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.selected(), Some(0));
assert_eq!(selection.artist.selected(), Some(0));
assert_eq!(selection.artist.album.selected(), Some(1));
assert_eq!(selection.artist.album.track.selected(), Some(0));
}
}

View File

@ -0,0 +1,226 @@
use std::cmp;
use musichoard::collection::track::{Track, TrackId};
use crate::tui::app::selection::{Delta, SelectionState, WidgetState};
#[derive(Clone, Debug, PartialEq)]
pub struct TrackSelection {
pub state: WidgetState,
}
impl TrackSelection {
pub fn initialise(tracks: &[Track]) -> Self {
let mut selection = TrackSelection {
state: WidgetState::default(),
};
selection.reinitialise(tracks, None);
selection
}
pub fn reinitialise(&mut self, tracks: &[Track], track: Option<IdSelectTrack>) {
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<usize> {
self.state.list.selected()
}
pub fn select(&mut self, tracks: &[Track], to: Option<usize>) {
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<SelectionState<'a, Track>> {
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<Self> {
selection.state.list.selected().map(|index| {
let track = &tracks[index];
IdSelectTrack {
track_id: track.get_sort_key().clone(),
}
})
}
}
#[cfg(test)]
mod tests {
use crate::tui::testmod::COLLECTION;
use super::*;
#[test]
fn track_select() {
let tracks = &COLLECTION[0].albums[0].tracks;
assert!(tracks.len() > 1);
let mut sel = TrackSelection::initialise(tracks);
assert_eq!(sel.selected(), Some(0));
sel.select(tracks, None);
assert_eq!(sel.selected(), None);
sel.select(tracks, Some(tracks.len()));
assert_eq!(sel.selected(), Some(tracks.len() - 1));
sel.reset(tracks);
assert_eq!(sel.selected(), Some(0));
}
#[test]
fn track_delta_line() {
let tracks = &COLLECTION[0].albums[0].tracks;
assert!(tracks.len() > 1);
let mut empty = TrackSelection::initialise(&[]);
assert_eq!(empty.selected(), None);
empty.increment(tracks, Delta::Line);
assert_eq!(empty.selected(), None);
empty.decrement(tracks, Delta::Line);
assert_eq!(empty.selected(), None);
let mut sel = TrackSelection::initialise(tracks);
assert_eq!(sel.selected(), Some(0));
sel.decrement(tracks, Delta::Line);
assert_eq!(sel.selected(), Some(0));
sel.increment(tracks, Delta::Line);
assert_eq!(sel.selected(), Some(1));
sel.decrement(tracks, Delta::Line);
assert_eq!(sel.selected(), Some(0));
for _ in 0..(tracks.len() + 5) {
sel.increment(tracks, Delta::Line);
}
assert_eq!(sel.selected(), Some(tracks.len() - 1));
}
#[test]
fn track_delta_page() {
let tracks = &COLLECTION[0].albums[0].tracks;
assert!(tracks.len() > 1);
let empty = TrackSelection::initialise(&[]);
assert_eq!(empty.selected(), None);
let mut sel = TrackSelection::initialise(tracks);
assert_eq!(sel.selected(), Some(0));
assert!(tracks.len() >= 4);
sel.state.height = 3;
sel.decrement(tracks, Delta::Page);
assert_eq!(sel.selected(), Some(0));
sel.increment(tracks, Delta::Page);
assert_eq!(sel.selected(), Some(2));
sel.decrement(tracks, Delta::Page);
assert_eq!(sel.selected(), Some(0));
for _ in 0..(tracks.len() + 5) {
sel.increment(tracks, Delta::Page);
}
assert_eq!(sel.selected(), Some(tracks.len() - 1));
}
#[test]
fn track_reinitialise() {
let tracks = &COLLECTION[0].albums[0].tracks;
assert!(tracks.len() > 1);
let mut sel = TrackSelection::initialise(tracks);
sel.state.list.select(Some(tracks.len() - 1));
// Re-initialise.
let expected = sel.clone();
let active_track = IdSelectTrack::get(tracks, &sel);
sel.reinitialise(tracks, active_track);
assert_eq!(sel, expected);
// Re-initialise out-of-bounds.
let mut expected = sel.clone();
expected.decrement(tracks, Delta::Line);
let active_track = IdSelectTrack::get(tracks, &sel);
sel.reinitialise(&tracks[..(tracks.len() - 1)], active_track);
assert_eq!(sel, expected);
// Re-initialise empty.
let expected = TrackSelection::initialise(&[]);
let active_track = IdSelectTrack::get(tracks, &sel);
sel.reinitialise(&[], active_track);
assert_eq!(sel, expected);
}
}