Extend incremental search to albums and tracks #152

Merged
wojtek merged 7 commits from 145---extend-incremental-search-to-albums-and-tracks into main 2024-03-01 22:04:26 +01:00
4 changed files with 862 additions and 814 deletions
Showing only changes of commit 588abb12b8 - Show all commits

View File

@ -0,0 +1,306 @@
use std::cmp;
use musichoard::collection::{album::{Album, AlbumId}, track::Track};
use crate::tui::app::selection::{
track::{IdSelectTrack, TrackSelection},
Delta, SelectionState, WidgetState,
};
#[derive(Clone, Debug, PartialEq)]
pub struct AlbumSelection {
pub state: WidgetState,
pub track: TrackSelection,
}
impl AlbumSelection {
pub fn initialise(albums: &[Album]) -> Self {
let mut selection = AlbumSelection {
state: WidgetState::default(),
track: TrackSelection::initialise(&[]),
};
selection.reinitialise(albums, None);
selection
}
pub fn reinitialise(&mut self, albums: &[Album], album: Option<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_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);
}
}

View File

@ -0,0 +1,338 @@
use std::cmp;
use musichoard::collection::{
album::Album,
artist::{Artist, ArtistId},
track::Track,
};
use crate::tui::app::selection::{
album::{AlbumSelection, IdSelectAlbum},
Delta, SelectionState, WidgetState,
};
#[derive(Clone, Debug, PartialEq)]
pub struct ArtistSelection {
pub state: WidgetState,
pub album: AlbumSelection,
}
impl ArtistSelection {
pub fn initialise(artists: &[Artist]) -> Self {
let mut selection = ArtistSelection {
state: WidgetState::default(),
album: AlbumSelection::initialise(&[]),
};
selection.reinitialise(artists, None);
selection
}
pub fn reinitialise(&mut self, artists: &[Artist], active: Option<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_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);
}
}

View File

@ -1,11 +1,11 @@
use musichoard::collection::{ mod album;
album::{Album, AlbumId}, mod artist;
artist::{Artist, ArtistId}, mod track;
track::{Track, TrackId},
Collection, use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection};
};
use ratatui::widgets::ListState; use ratatui::widgets::ListState;
use std::cmp;
use artist::{ArtistSelection, IdSelectArtist};
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Category { pub enum Category {
@ -31,21 +31,9 @@ pub struct Selection {
pub artist: ArtistSelection, pub artist: ArtistSelection,
} }
#[derive(Clone, Debug, PartialEq)] pub struct SelectionState<'a, T> {
pub struct ArtistSelection { pub list: &'a [T],
pub state: WidgetState, pub index: usize,
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 { pub enum Delta {
@ -62,11 +50,6 @@ impl Delta {
} }
} }
pub struct SelectionState<'a, T> {
pub list: &'a [T],
pub index: usize,
}
impl Selection { impl Selection {
pub fn new(artists: &[Artist]) -> Self { pub fn new(artists: &[Artist]) -> Self {
Selection { Selection {
@ -214,393 +197,6 @@ impl Selection {
} }
} }
impl ArtistSelection {
fn initialise(artists: &[Artist]) -> Self {
let mut selection = ArtistSelection {
state: WidgetState::default(),
album: AlbumSelection::initialise(&[]),
};
selection.reinitialise(artists, None);
selection
}
fn reinitialise(&mut self, artists: &[Artist], active: Option<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 selected_album(&self) -> Option<usize> {
self.album.selected()
}
fn selected_track(&self) -> Option<usize> {
self.album.selected_track()
}
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_album(&mut self, artists: &[Artist], to: Option<usize>) {
if let Some(index) = self.state.list.selected() {
self.album.select(&artists[index].albums, to);
}
}
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);
}
}
fn selection_state<'a>(&self, list: &'a [Artist]) -> Option<SelectionState<'a, Artist>> {
let selected = self.state.list.selected();
selected.map(|index| SelectionState { list, index })
}
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))
}
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))
}
fn reset(&mut self, artists: &[Artist]) {
if self.state.list.selected() != Some(0) {
self.reinitialise(artists, None);
}
}
fn reset_album(&mut self, artists: &[Artist]) {
if let Some(index) = self.state.list.selected() {
self.album.reset(&artists[index].albums);
}
}
fn reset_track(&mut self, artists: &[Artist]) {
if let Some(index) = self.state.list.selected() {
self.album.reset_track(&artists[index].albums);
}
}
fn increment_by(&mut self, artists: &[Artist], by: usize) {
if let Some(index) = self.state.list.selected() {
let result = index.saturating_add(by);
self.select_to(artists, result);
}
}
fn increment(&mut self, artists: &[Artist], delta: Delta) {
self.increment_by(artists, delta.as_usize(&self.state));
}
fn increment_album(&mut self, artists: &[Artist], delta: Delta) {
if let Some(index) = self.state.list.selected() {
self.album.increment(&artists[index].albums, delta);
}
}
fn increment_track(&mut self, artists: &[Artist], delta: Delta) {
if let Some(index) = self.state.list.selected() {
self.album.increment_track(&artists[index].albums, delta);
}
}
fn decrement_by(&mut self, artists: &[Artist], by: usize) {
if let Some(index) = self.state.list.selected() {
let result = index.saturating_sub(by);
if self.state.list.selected() != Some(result) {
self.state.list.select(Some(result));
self.album = AlbumSelection::initialise(&artists[result].albums);
}
}
}
fn decrement(&mut self, artists: &[Artist], delta: Delta) {
self.decrement_by(artists, delta.as_usize(&self.state));
}
fn decrement_album(&mut self, artists: &[Artist], delta: Delta) {
if let Some(index) = self.state.list.selected() {
self.album.decrement(&artists[index].albums, delta);
}
}
fn decrement_track(&mut self, artists: &[Artist], delta: Delta) {
if let Some(index) = self.state.list.selected() {
self.album.decrement_track(&artists[index].albums, delta);
}
}
}
impl AlbumSelection {
fn initialise(albums: &[Album]) -> Self {
let mut selection = AlbumSelection {
state: WidgetState::default(),
track: TrackSelection::initialise(&[]),
};
selection.reinitialise(albums, None);
selection
}
fn reinitialise(&mut self, albums: &[Album], album: Option<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 selected(&self) -> Option<usize> {
self.state.list.selected()
}
fn selected_track(&self) -> Option<usize> {
self.track.selected()
}
fn select(&mut self, albums: &[Album], to: Option<usize>) {
match to {
Some(to) => self.select_to(albums, to),
None => self.state.list.select(None),
}
}
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);
}
}
fn selection_state<'a>(&self, list: &'a [Album]) -> Option<SelectionState<'a, Album>> {
let selected = self.state.list.selected();
selected.map(|index| SelectionState { list, index })
}
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))
}
fn reset(&mut self, albums: &[Album]) {
if self.state.list.selected() != Some(0) {
self.reinitialise(albums, None);
}
}
fn reset_track(&mut self, albums: &[Album]) {
if let Some(index) = self.state.list.selected() {
self.track.reset(&albums[index].tracks);
}
}
fn increment_by(&mut self, albums: &[Album], by: usize) {
if let Some(index) = self.state.list.selected() {
let mut result = index.saturating_add(by);
if result >= albums.len() {
result = albums.len() - 1;
}
if self.state.list.selected() != Some(result) {
self.state.list.select(Some(result));
self.track = TrackSelection::initialise(&albums[result].tracks);
}
}
}
fn increment(&mut self, albums: &[Album], delta: Delta) {
self.increment_by(albums, delta.as_usize(&self.state));
}
fn increment_track(&mut self, albums: &[Album], delta: Delta) {
if let Some(index) = self.state.list.selected() {
self.track.increment(&albums[index].tracks, delta);
}
}
fn decrement_by(&mut self, albums: &[Album], by: usize) {
if let Some(index) = self.state.list.selected() {
let result = index.saturating_sub(by);
if self.state.list.selected() != Some(result) {
self.state.list.select(Some(result));
self.track = TrackSelection::initialise(&albums[result].tracks);
}
}
}
fn decrement(&mut self, albums: &[Album], delta: Delta) {
self.decrement_by(albums, delta.as_usize(&self.state));
}
fn decrement_track(&mut self, albums: &[Album], delta: Delta) {
if let Some(index) = self.state.list.selected() {
self.track.decrement(&albums[index].tracks, delta);
}
}
}
impl TrackSelection {
fn initialise(tracks: &[Track]) -> Self {
let mut selection = TrackSelection {
state: WidgetState::default(),
};
selection.reinitialise(tracks, None);
selection
}
fn reinitialise(&mut self, tracks: &[Track], track: Option<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 selected(&self) -> Option<usize> {
self.state.list.selected()
}
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));
}
fn selection_state<'a>(&self, list: &'a [Track]) -> Option<SelectionState<'a, Track>> {
let selected = self.state.list.selected();
selected.map(|index| SelectionState { list, index })
}
fn reset(&mut self, tracks: &[Track]) {
if self.state.list.selected() != Some(0) {
self.reinitialise(tracks, None);
}
}
fn increment_by(&mut self, tracks: &[Track], by: usize) {
if let Some(index) = self.state.list.selected() {
let mut result = index.saturating_add(by);
if result >= tracks.len() {
result = tracks.len() - 1;
}
if self.state.list.selected() != Some(result) {
self.state.list.select(Some(result));
}
}
}
fn increment(&mut self, tracks: &[Track], delta: Delta) {
self.increment_by(tracks, delta.as_usize(&self.state));
}
fn decrement_by(&mut self, _tracks: &[Track], by: usize) {
if let Some(index) = self.state.list.selected() {
let result = index.saturating_sub(by);
if self.state.list.selected() != Some(result) {
self.state.list.select(Some(result));
}
}
}
fn decrement(&mut self, tracks: &[Track], delta: Delta) {
self.decrement_by(tracks, delta.as_usize(&self.state));
}
}
pub struct ListSelection { pub struct ListSelection {
pub artist: ListState, pub artist: ListState,
pub album: ListState, pub album: ListState,
@ -621,20 +217,6 @@ pub struct IdSelection {
artist: Option<IdSelectArtist>, 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 { impl IdSelection {
pub fn get(collection: &Collection, selection: &Selection) -> Self { pub fn get(collection: &Collection, selection: &Selection) -> Self {
IdSelection { IdSelection {
@ -643,398 +225,12 @@ impl IdSelection {
} }
} }
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)] #[cfg(test)]
mod tests { mod tests {
use crate::tui::testmod::COLLECTION; use crate::tui::testmod::COLLECTION;
use super::*; 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] #[test]
fn selection() { fn selection() {
let mut selection = Selection::new(&COLLECTION); let mut selection = Selection::new(&COLLECTION);

View File

@ -0,0 +1,208 @@
use std::cmp;
use musichoard::collection::track::{Track, TrackId};
use crate::tui::app::selection::{Delta, SelectionState, WidgetState};
#[derive(Clone, Debug, PartialEq)]
pub struct TrackSelection {
pub state: WidgetState,
}
impl TrackSelection {
pub fn initialise(tracks: &[Track]) -> Self {
let mut selection = TrackSelection {
state: WidgetState::default(),
};
selection.reinitialise(tracks, None);
selection
}
pub fn reinitialise(&mut self, tracks: &[Track], track: Option<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_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);
}
}