Extend incremental search to albums and tracks #152
306
src/tui/app/selection/album.rs
Normal file
306
src/tui/app/selection/album.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
338
src/tui/app/selection/artist.rs
Normal file
338
src/tui/app/selection/artist.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
208
src/tui/app/selection/track.rs
Normal file
208
src/tui/app/selection/track.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user