Extend incremental search to albums and tracks (#152)
Closes #145 Reviewed-on: #152
This commit is contained in:
parent
fd19ea3eb3
commit
42d1edb69c
@ -77,7 +77,7 @@ impl<MH: IMusicHoard> IAppInteractBrowse for AppMachine<MH, AppBrowse> {
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
use aho_corasick::AhoCorasick;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use musichoard::collection::artist::Artist;
|
||||
use musichoard::collection::{album::Album, artist::Artist, track::Track};
|
||||
|
||||
use crate::tui::{
|
||||
app::{
|
||||
machine::{App, AppInner, AppMachine},
|
||||
selection::ListSelection,
|
||||
AppPublic, AppState, IAppInteractSearch,
|
||||
selection::{ListSelection, SelectionState},
|
||||
AppPublic, AppState, Category, IAppInteractSearch,
|
||||
},
|
||||
lib::IMusicHoard,
|
||||
};
|
||||
@ -67,7 +67,7 @@ impl<MH: IMusicHoard> IAppInteractSearch for AppMachine<MH, AppSearch> {
|
||||
|
||||
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<MH: IMusicHoard> IAppInteractSearch for AppMachine<MH, AppSearch> {
|
||||
|
||||
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<MH: IMusicHoard> IAppInteractSearch for AppMachine<MH, AppSearch> {
|
||||
if memo.char {
|
||||
self.state.string.pop();
|
||||
}
|
||||
self.inner.selection.select_artist(collection, memo.index);
|
||||
self.inner.selection.select(collection, memo.index);
|
||||
}
|
||||
self.into()
|
||||
}
|
||||
@ -109,12 +109,18 @@ impl<MH: IMusicHoard> IAppInteractSearch for AppMachine<MH, AppSearch> {
|
||||
|
||||
trait IAppInteractSearchPrivate {
|
||||
fn incremental_search(&mut self, next: bool);
|
||||
fn incremental_search_predicate(
|
||||
case_sensitive: bool,
|
||||
char_sensitive: bool,
|
||||
search_name: &str,
|
||||
probe: &Artist,
|
||||
) -> bool;
|
||||
fn next<P, T>(pred: P, name: &str, next: bool, st: SelectionState<'_, T>) -> Option<usize>
|
||||
where
|
||||
P: FnMut(bool, bool, &str, &T) -> bool;
|
||||
|
||||
fn search_artists(name: &str, next: bool, st: SelectionState<'_, Artist>) -> Option<usize>;
|
||||
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_char_sensitive(artist_name: &str) -> bool;
|
||||
@ -123,50 +129,86 @@ trait IAppInteractSearchPrivate {
|
||||
|
||||
impl<MH: IMusicHoard> IAppInteractSearchPrivate for AppMachine<MH, AppSearch> {
|
||||
fn incremental_search(&mut self, next: bool) {
|
||||
let artists = self.inner.music_hoard.get_collection();
|
||||
let artist_name = &self.state.string;
|
||||
let collection = self.inner.music_hoard.get_collection();
|
||||
let search = &self.state.string;
|
||||
|
||||
let sel = &mut self.inner.selection;
|
||||
if let Some(mut index) = sel.selected_artist() {
|
||||
let case_sensitive = Self::is_case_sensitive(artist_name);
|
||||
let char_sensitive = Self::is_char_sensitive(artist_name);
|
||||
let search = Self::normalize_search(artist_name, !case_sensitive, !char_sensitive);
|
||||
let sel = &self.inner.selection;
|
||||
let result = match sel.active {
|
||||
Category::Artist => sel
|
||||
.state_artist(collection)
|
||||
.and_then(|state| Self::search_artists(search, next, state)),
|
||||
Category::Album => sel
|
||||
.state_album(collection)
|
||||
.and_then(|state| Self::search_albums(search, next, state)),
|
||||
Category::Track => sel
|
||||
.state_track(collection)
|
||||
.and_then(|state| Self::search_tracks(search, next, state)),
|
||||
};
|
||||
|
||||
if next && ((index + 1) < artists.len()) {
|
||||
index += 1;
|
||||
}
|
||||
let slice = &artists[index..];
|
||||
|
||||
let result = slice.iter().position(|probe| {
|
||||
Self::incremental_search_predicate(case_sensitive, char_sensitive, &search, probe)
|
||||
});
|
||||
|
||||
if let Some(slice_index) = result {
|
||||
sel.select_artist(artists, Some(index + slice_index));
|
||||
}
|
||||
if result.is_some() {
|
||||
let collection = self.inner.music_hoard.get_collection();
|
||||
self.inner.selection.select(collection, result);
|
||||
}
|
||||
}
|
||||
|
||||
fn incremental_search_predicate(
|
||||
case_sensitive: bool,
|
||||
char_sensitive: bool,
|
||||
search_name: &str,
|
||||
probe: &Artist,
|
||||
) -> bool {
|
||||
let name = Self::normalize_search(&probe.id.name, !case_sensitive, !char_sensitive);
|
||||
let mut result = name.starts_with(search_name);
|
||||
fn search_artists(name: &str, next: bool, st: SelectionState<'_, Artist>) -> Option<usize> {
|
||||
Self::next(Self::predicate_artists, name, next, st)
|
||||
}
|
||||
|
||||
fn search_albums(name: &str, next: bool, st: SelectionState<'_, Album>) -> Option<usize> {
|
||||
Self::next(Self::predicate_albums, name, next, st)
|
||||
}
|
||||
|
||||
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 !result {
|
||||
let name =
|
||||
Self::normalize_search(&probe_sort.name, !case_sensitive, !char_sensitive);
|
||||
result = name.starts_with(search_name);
|
||||
let name = Self::normalize_search(&probe_sort.name, !case_sens, !char_sens);
|
||||
result = name.starts_with(search);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn predicate_albums(case_sens: bool, char_sens: bool, search: &str, probe: &Album) -> bool {
|
||||
Self::predicate_title(case_sens, char_sens, search, &probe.id.title)
|
||||
}
|
||||
|
||||
fn predicate_tracks(case_sens: bool, char_sens: bool, search: &str, probe: &Track) -> bool {
|
||||
Self::predicate_title(case_sens, char_sens, search, &probe.id.title)
|
||||
}
|
||||
|
||||
fn predicate_title(case_sens: bool, char_sens: bool, search: &str, title: &str) -> bool {
|
||||
Self::normalize_search(title, !case_sens, !char_sens).starts_with(search)
|
||||
}
|
||||
|
||||
fn is_case_sensitive(artist_name: &str) -> bool {
|
||||
artist_name
|
||||
.chars()
|
||||
@ -330,6 +372,54 @@ mod tests {
|
||||
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn album_incremental_search() {
|
||||
let mut search =
|
||||
AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
|
||||
search.inner.selection.active = Category::Album;
|
||||
|
||||
let sel = &search.inner.selection;
|
||||
assert_eq!(sel.artist.album.state.list.selected(), Some(0));
|
||||
|
||||
search.state.string = String::from("album_title ");
|
||||
search.incremental_search(false);
|
||||
|
||||
let sel = &search.inner.selection;
|
||||
assert_eq!(sel.artist.album.state.list.selected(), Some(0));
|
||||
|
||||
let search = search.append_character('a').unwrap_search();
|
||||
let search = search.append_character('.').unwrap_search();
|
||||
let search = search.append_character('b').unwrap_search();
|
||||
|
||||
let sel = &search.inner.selection;
|
||||
assert_eq!(sel.artist.album.state.list.selected(), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn track_incremental_search() {
|
||||
let mut search =
|
||||
AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
|
||||
search.inner.selection.active = Category::Track;
|
||||
|
||||
let sel = &search.inner.selection;
|
||||
assert_eq!(sel.artist.album.track.state.list.selected(), Some(0));
|
||||
|
||||
search.state.string = String::from("track ");
|
||||
search.incremental_search(false);
|
||||
|
||||
let sel = &search.inner.selection;
|
||||
assert_eq!(sel.artist.album.track.state.list.selected(), Some(0));
|
||||
|
||||
let search = search.append_character('a').unwrap_search();
|
||||
let search = search.append_character('.').unwrap_search();
|
||||
let search = search.append_character('a').unwrap_search();
|
||||
let search = search.append_character('.').unwrap_search();
|
||||
let search = search.append_character('2').unwrap_search();
|
||||
|
||||
let sel = &search.inner.selection;
|
||||
assert_eq!(sel.artist.album.track.state.list.selected(), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search() {
|
||||
let search = AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2)));
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
342
src/tui/app/selection/album.rs
Normal file
342
src/tui/app/selection/album.rs
Normal 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);
|
||||
}
|
||||
}
|
393
src/tui/app/selection/artist.rs
Normal file
393
src/tui/app/selection/artist.rs
Normal 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);
|
||||
}
|
||||
}
|
389
src/tui/app/selection/mod.rs
Normal file
389
src/tui/app/selection/mod.rs
Normal 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));
|
||||
}
|
||||
}
|
226
src/tui/app/selection/track.rs
Normal file
226
src/tui/app/selection/track.rs
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user