use iced::{widget::Id, Point};
use crate::{
data::library::LibraryItem,
types::{QueueTab, Track},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TrackListKind {
Queue,
Active,
Recent,
}
impl From<TrackListKind> for Id {
fn from(list: TrackListKind) -> Self {
match list {
TrackListKind::Queue => crate::app::ui::QUEUE_LIST_ID,
TrackListKind::Active => crate::app::ui::TRACK_LIST_ID,
TrackListKind::Recent => crate::app::ui::QUEUE_RECENT_LIST_ID,
}
}
}
impl TrackListKind {
pub const fn first_index(self) -> usize {
match self {
TrackListKind::Queue => 1,
_ => 0,
}
}
pub const fn slot(self) -> usize {
match self {
TrackListKind::Queue => 0,
TrackListKind::Active => 1,
TrackListKind::Recent => 2,
}
}
pub const fn is_main(self) -> bool {
matches!(self, TrackListKind::Active)
}
}
pub fn row_id(list: TrackListKind, index: usize) -> Id {
let tag = match list {
TrackListKind::Queue => "queue",
TrackListKind::Active => "active",
TrackListKind::Recent => "recent",
};
Id::from(format!("row:{tag}:{index}"))
}
impl From<QueueTab> for TrackListKind {
fn from(tab: QueueTab) -> Self {
match tab {
QueueTab::Queue => TrackListKind::Queue,
QueueTab::RecentlyPlayed => TrackListKind::Recent,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TrackPos {
pub index: usize,
pub list: TrackListKind,
}
impl TrackPos {
pub const fn new(index: usize, list: TrackListKind) -> Self {
Self { index, list }
}
}
#[derive(Debug, Clone)]
pub struct TrackListSearch {
pub list: TrackListKind,
pub query: String,
pub matches: Vec<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubmenuKind {
Play,
Download,
SongRadio,
ArtistRadio,
GoToArtist,
}
impl SubmenuKind {
pub fn entry_message(
self,
provider: crate::providers::ProviderId,
menu: &ContextMenuState,
) -> super::message::Message {
use super::message::Message;
match self {
SubmenuKind::Play => Message::ContextMenuPlayViaProvider(provider, menu.pos),
SubmenuKind::Download => Message::ContextMenuDownloadViaProvider(provider),
SubmenuKind::SongRadio => Message::ContextMenuSongRadioProvider(provider),
SubmenuKind::ArtistRadio => Message::ContextMenuArtistRadioProvider(provider),
SubmenuKind::GoToArtist => Message::ContextMenuGoToArtistProvider(provider),
}
}
pub fn providers(self, track: &Track) -> Vec<crate::providers::ProviderId> {
use crate::providers::ProviderId;
match self {
SubmenuKind::Play => {
let mut v: Vec<ProviderId> = ProviderId::searchable()
.iter()
.copied()
.filter(|p| p.capabilities().stream)
.collect();
if track.local_path().is_some() {
v.push(ProviderId::Local);
}
v
}
SubmenuKind::Download => ProviderId::defaultable()
.iter()
.copied()
.filter(|p| p.capabilities().download)
.collect(),
SubmenuKind::SongRadio | SubmenuKind::ArtistRadio => ProviderId::searchable()
.iter()
.copied()
.filter(|p| p.capabilities().radio)
.collect(),
SubmenuKind::GoToArtist => ProviderId::searchable()
.iter()
.copied()
.filter(|p| p.capabilities().search)
.collect(),
}
}
pub fn available(self) -> bool {
use crate::providers::ProviderId;
let any = |list: &'static [ProviderId], cap: fn(ProviderId) -> bool| {
list.iter().copied().any(cap)
};
match self {
SubmenuKind::Play => any(ProviderId::searchable(), |p| p.capabilities().stream),
SubmenuKind::Download => any(ProviderId::defaultable(), |p| p.capabilities().download),
SubmenuKind::SongRadio | SubmenuKind::ArtistRadio => {
any(ProviderId::searchable(), |p| p.capabilities().radio)
}
SubmenuKind::GoToArtist => !ProviderId::searchable().is_empty(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DefaultCtxAction {
Download,
SongRadio,
ArtistRadio,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CtxAction {
Play,
Edit,
GoToArtist,
AddToPlaylist,
Download,
SongRadio,
ArtistRadio,
RemoveFromQueue,
RemoveFromPlaylist,
}
impl CtxAction {
pub fn submenu(self) -> Option<SubmenuKind> {
match self {
CtxAction::Play => Some(SubmenuKind::Play),
CtxAction::Download => Some(SubmenuKind::Download),
CtxAction::SongRadio => Some(SubmenuKind::SongRadio),
CtxAction::ArtistRadio => Some(SubmenuKind::ArtistRadio),
CtxAction::GoToArtist => Some(SubmenuKind::GoToArtist),
CtxAction::Edit
| CtxAction::AddToPlaylist
| CtxAction::RemoveFromQueue
| CtxAction::RemoveFromPlaylist => None,
}
}
pub fn to_message(self, menu: &ContextMenuState) -> super::message::Message {
use super::message::Message;
match self {
CtxAction::Play => Message::ContextMenuPlayTrack(menu.pos),
CtxAction::Edit => Message::ContextMenuEditTrack,
CtxAction::GoToArtist => Message::ContextMenuGoToArtist,
CtxAction::AddToPlaylist => Message::TogglePicker(menu.target_indices.clone()),
CtxAction::Download => Message::ContextMenuDefault(DefaultCtxAction::Download),
CtxAction::SongRadio => Message::ContextMenuDefault(DefaultCtxAction::SongRadio),
CtxAction::ArtistRadio => Message::ContextMenuDefault(DefaultCtxAction::ArtistRadio),
CtxAction::RemoveFromQueue => {
Message::ContextMenuRemoveFromQueue(menu.target_indices.clone())
}
CtxAction::RemoveFromPlaylist => {
Message::ContextMenuRemoveFromPlaylist(menu.target_indices.clone())
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContextMenuFocus {
Item(usize),
Sub(SubmenuKind, usize),
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct ContextMenuState {
pub pos: TrackPos,
pub position: (f32, f32),
pub cursor: (f32, f32),
pub in_playlist: bool,
pub track: Track,
pub target_indices: Vec<usize>,
pub hovered: Option<ContextMenuFocus>,
}
impl ContextMenuState {
pub fn actions(&self) -> Vec<CtxAction> {
let mut v = vec![CtxAction::Play, CtxAction::Edit];
if !self.track.artist.is_empty() {
v.push(CtxAction::GoToArtist);
}
v.push(CtxAction::AddToPlaylist);
v.push(CtxAction::Download);
if SubmenuKind::SongRadio.available() {
v.push(CtxAction::SongRadio);
v.push(CtxAction::ArtistRadio);
}
if self.pos.list == TrackListKind::Queue {
v.push(CtxAction::RemoveFromQueue);
} else if self.in_playlist && self.pos.list != TrackListKind::Recent {
v.push(CtxAction::RemoveFromPlaylist);
}
v
}
pub fn open_submenu_kind(&self) -> Option<SubmenuKind> {
match self.hovered? {
ContextMenuFocus::Item(i) => self.actions().get(i).and_then(|a| a.submenu()),
ContextMenuFocus::Sub(kind, _) => Some(kind),
}
}
pub fn default_provider(
&self,
action: DefaultCtxAction,
fallback: crate::providers::ProviderId,
) -> crate::providers::ProviderId {
use crate::providers::ProviderId;
let capable = |p: ProviderId| match action {
DefaultCtxAction::Download => p.capabilities().download,
DefaultCtxAction::SongRadio | DefaultCtxAction::ArtistRadio => p.capabilities().radio,
};
if capable(self.track.source) {
return self.track.source;
}
ProviderId::searchable()
.iter()
.copied()
.find(|&p| action != DefaultCtxAction::Download && capable(p))
.unwrap_or(fallback)
}
pub fn default_go_to_artist_provider(
&self,
fallback: crate::providers::ProviderId,
) -> crate::providers::ProviderId {
if self.track.provider_artist_id(self.track.source).is_some() {
self.track.source
} else {
fallback
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DropTarget {
Track(TrackPos),
Playlist(usize),
Library(usize),
PlaylistAdd(usize),
PlaylistReorder { from: usize, to: usize },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HoverTarget {
Track(TrackPos),
Card(LibraryItem),
LibraryCard(LibraryItem),
Playlist(usize),
SearchHistory(usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Pressed {
Track(TrackPos),
Card(LibraryItem),
Playlist(usize),
}
#[derive(Debug, Clone, PartialEq)]
pub struct PressedDrag {
pub what: Pressed,
pub origin: Point,
}
#[derive(Debug, Clone, Default)]
pub struct DragState {
pub cursor_pos: Point,
pub pressed: Option<PressedDrag>,
pub drag_active: bool,
pub drop_target: Option<DropTarget>,
pub dragged: Option<(TrackListKind, Vec<usize>)>,
pub is_hover_controlled: bool,
pub hovered: Option<HoverTarget>,
pub last_focus: [usize; 3],
}
impl DragState {
pub fn stop(&mut self) {
self.drag_active = false;
self.pressed = None;
self.drop_target = None;
self.dragged = None;
}
pub(crate) fn cleanup(&mut self) {
self.stop();
self.hovered = None;
}
pub fn hovered_track(&self) -> Option<TrackPos> {
match self.hovered {
Some(HoverTarget::Track(pos)) => Some(pos),
_ => None,
}
}
pub fn set_hovered(&mut self, target: HoverTarget) {
if let HoverTarget::Track(pos) = &target {
self.last_focus[pos.list.slot()] = pos.index;
}
self.hovered = Some(target);
}
pub fn recall_focus(&self, list: TrackListKind) -> usize {
self.last_focus[list.slot()]
}
pub fn clear_hovered_track(&mut self) {
if matches!(self.hovered, Some(HoverTarget::Track(_))) {
self.hovered = None;
}
}
pub fn hovered_search_history(&self) -> Option<usize> {
match self.hovered {
Some(HoverTarget::SearchHistory(i)) => Some(i),
_ => None,
}
}
pub fn set_hovered_search_history(&mut self, index: usize) {
self.hovered = Some(HoverTarget::SearchHistory(index));
}
pub fn clear_hovered_search_history(&mut self) {
if matches!(self.hovered, Some(HoverTarget::SearchHistory(_))) {
self.hovered = None;
}
}
pub fn is_hovered_card(&self, item: &LibraryItem) -> bool {
matches!(self.hovered, Some(HoverTarget::Card(ref c)) if c == item)
}
pub fn is_hovered_library_card(&self, item: &LibraryItem) -> bool {
matches!(self.hovered, Some(HoverTarget::LibraryCard(ref c)) if c == item)
}
pub fn is_pressed_card(&self) -> bool {
matches!(
self.pressed,
Some(PressedDrag {
what: Pressed::Card(_),
..
})
)
}
pub fn hovered_playlist(&self) -> Option<usize> {
match self.hovered {
Some(HoverTarget::Playlist(i)) => Some(i),
_ => None,
}
}
pub fn cursor_interaction(&self) -> Option<iced::mouse::Interaction> {
if self.drag_active && self.pressed.is_some() {
Some(iced::mouse::Interaction::Grabbing)
} else {
None
}
}
pub fn clickable_cursor_interaction(&self) -> iced::mouse::Interaction {
self.cursor_interaction()
.unwrap_or(iced::mouse::Interaction::Pointer)
}
}
#[cfg(test)]
mod tests {
use iced::widget::Id;
use super::TrackListKind::{Active, Queue, Recent};
use crate::app::ui::{QUEUE_LIST_ID, QUEUE_RECENT_LIST_ID, TRACK_LIST_ID};
#[test]
fn each_list_targets_a_distinct_scrollable() {
assert_eq!(Id::from(Queue), QUEUE_LIST_ID);
assert_eq!(Id::from(Active), TRACK_LIST_ID);
assert_eq!(Id::from(Recent), QUEUE_RECENT_LIST_ID);
assert_ne!(Id::from(Queue), Id::from(Recent));
}
#[test]
fn only_queue_offsets_its_first_row() {
assert_eq!(Queue.first_index(), 1);
assert_eq!(Active.first_index(), 0);
assert_eq!(Recent.first_index(), 0);
}
}