use std::cmp::{max, min}; use std::sync::{Arc, RwLock}; use cursive::align::HAlign; use cursive::event::{Event, EventResult, MouseButton, MouseEvent}; use cursive::theme::{ColorStyle, ColorType, PaletteColor}; use cursive::traits::View; use cursive::view::ScrollBase; use cursive::{Cursive, Printer, Rect, Vec2}; use unicode_width::UnicodeWidthStr; use commands::CommandResult; use library::Library; use queue::Queue; use track::Track; use traits::{IntoBoxedViewExt, ListItem, ViewExt}; use ui::album::AlbumView; use ui::artist::ArtistView; pub type Paginator = Box>>) + Send + Sync>; pub struct Pagination { max_content: Arc>>, callback: Arc>>>, busy: Arc>, } impl Default for Pagination { fn default() -> Self { Pagination { max_content: Arc::new(RwLock::new(None)), callback: Arc::new(RwLock::new(None)), busy: Arc::new(RwLock::new(false)), } } } // TODO: figure out why deriving Clone doesn't work impl Clone for Pagination { fn clone(&self) -> Self { Pagination { max_content: self.max_content.clone(), callback: self.callback.clone(), busy: self.busy.clone(), } } } impl Pagination { pub fn clear(&mut self) { *self.max_content.write().unwrap() = None; *self.callback.write().unwrap() = None; } pub fn set(&mut self, max_content: usize, callback: Paginator) { *self.max_content.write().unwrap() = Some(max_content); *self.callback.write().unwrap() = Some(callback); } fn max_content(&self) -> Option { *self.max_content.read().unwrap() } fn is_busy(&self) -> bool { *self.busy.read().unwrap() } fn call(&self, content: &Arc>>) { let pagination = self.clone(); let content = content.clone(); if !self.is_busy() { *self.busy.write().unwrap() = true; std::thread::spawn(move || { let cb = pagination.callback.read().unwrap(); if let Some(ref cb) = *cb { debug!("calling paginator!"); cb(content); *pagination.busy.write().unwrap() = false; } }); } } } pub struct ListView { content: Arc>>, last_content_len: usize, selected: usize, last_size: Vec2, scrollbar: ScrollBase, queue: Arc, library: Arc, pagination: Pagination, } impl ListView { pub fn new(content: Arc>>, queue: Arc, library: Arc) -> Self { Self { content, last_content_len: 0, selected: 0, last_size: Vec2::new(0, 0), scrollbar: ScrollBase::new(), queue, library, pagination: Pagination::default(), } } pub fn get_pagination(&self) -> &Pagination { &self.pagination } fn can_paginate(&self) -> bool { if let Some(max) = self.get_pagination().max_content() { trace!( "pagination: total: {}, current: {}", max, self.last_content_len ); if max > self.last_content_len { return true; } } false } pub fn get_selected_index(&self) -> usize { self.selected } pub fn move_focus_to(&mut self, target: usize) { let len = self.content.read().unwrap().len().saturating_sub(1); self.selected = min(target, len); self.scrollbar.scroll_to(self.selected); } pub fn move_focus(&mut self, delta: i32) { let new = self.selected as i32 + delta; self.move_focus_to(max(new, 0) as usize); } fn attempt_play_all_tracks(&self) -> bool { let content = self.content.read().unwrap(); let any = &(*content) as &dyn std::any::Any; if let Some(tracks) = any.downcast_ref::>() { let tracks: Vec<&Track> = tracks.iter().collect(); let index = self.queue.append_next(tracks); self.queue.play(index + self.selected, true); true } else { false } } } impl View for ListView { fn draw(&self, printer: &Printer<'_, '_>) { let content = self.content.read().unwrap(); self.scrollbar.draw(printer, |printer, i| { let item = &content[i]; let style = if self.selected == i { let fg = if item.is_playing(self.queue.clone()) { *printer.theme.palette.custom("playing").unwrap() } else { PaletteColor::Tertiary.resolve(&printer.theme.palette) }; ColorStyle::new( ColorType::Color(fg), ColorType::Palette(PaletteColor::Highlight), ) } else if item.is_playing(self.queue.clone()) { ColorStyle::new( ColorType::Color(*printer.theme.palette.custom("playing").unwrap()), ColorType::Color(*printer.theme.palette.custom("playing_bg").unwrap()), ) } else { ColorStyle::primary() }; let left = item.display_left(); let right = item.display_right(self.library.clone()); // draw left string printer.with_color(style, |printer| { printer.print_hline((0, 0), printer.size.x, " "); printer.print((0, 0), &left); }); // draw ".." to indicate a cut off string let max_length = printer.size.x.saturating_sub(right.width() + 1); if max_length < left.width() { let offset = max_length.saturating_sub(1); printer.with_color(style, |printer| { printer.print((offset, 0), ".."); }); } // draw right string let offset = HAlign::Right.get_offset(right.width(), printer.size.x); printer.with_color(style, |printer| { printer.print((offset, 0), &right); }); }); } fn layout(&mut self, size: Vec2) { self.last_content_len = self.content.read().unwrap().len(); self.last_size = size; self.scrollbar.set_heights(size.y, self.last_content_len); } fn needs_relayout(&self) -> bool { self.content.read().unwrap().len() != self.last_content_len } fn on_event(&mut self, e: Event) -> EventResult { match e { Event::Mouse { event: MouseEvent::WheelUp, .. } => self.move_focus(-3), Event::Mouse { event: MouseEvent::WheelDown, .. } => self.move_focus(3), Event::Mouse { event: MouseEvent::Press(MouseButton::Left), position, offset, } => { if self.scrollbar.scrollable() && position.y > 0 && position.y <= self.last_size.y && position .checked_sub(offset) .map(|p| self.scrollbar.start_drag(p, self.last_size.x)) .unwrap_or(false) {} } Event::Mouse { event: MouseEvent::Hold(MouseButton::Left), position, offset, } => { if self.scrollbar.scrollable() { self.scrollbar.drag(position.saturating_sub(offset)); } } Event::Mouse { event: MouseEvent::Release(MouseButton::Left), .. } => { self.scrollbar.release_grab(); } _ => { return EventResult::Ignored; } } EventResult::Consumed(None) } fn required_size(&mut self, constraint: Vec2) -> Vec2 { Vec2::new(constraint.x, self.content.read().unwrap().len()) } fn important_area(&self, view_size: Vec2) -> Rect { if self.content.read().unwrap().len() > 0 { Rect::from((view_size.x, self.selected)) } else { Rect::from((0, 0)) } } } impl ViewExt for ListView { fn on_command( &mut self, _s: &mut Cursive, cmd: &str, args: &[String], ) -> Result { if cmd == "play" { self.queue.clear(); if !self.attempt_play_all_tracks() { let mut content = self.content.write().unwrap(); if let Some(item) = content.get_mut(self.selected) { item.play(self.queue.clone()); } } return Ok(CommandResult::Consumed(None)); } if cmd == "queue" { let mut content = self.content.write().unwrap(); if let Some(item) = content.get_mut(self.selected) { item.queue(self.queue.clone()); } return Ok(CommandResult::Consumed(None)); } if cmd == "save" { let mut item = { let content = self.content.read().unwrap(); content.get(self.selected).cloned() }; if let Some(item) = item.as_mut() { item.toggle_saved(self.library.clone()); } } if cmd == "move" { if let Some(dir) = args.get(0) { let amount: usize = args .get(1) .unwrap_or(&"1".to_string()) .parse() .map_err(|e| format!("{:?}", e))?; let len = self.content.read().unwrap().len(); if dir == "up" && self.selected > 0 { self.move_focus(-(amount as i32)); return Ok(CommandResult::Consumed(None)); } if dir == "down" { if self.selected < len.saturating_sub(1) { self.move_focus(amount as i32); return Ok(CommandResult::Consumed(None)); } else if self.selected == len.saturating_sub(1) && self.can_paginate() { self.pagination.call(&self.content); } } } } if cmd == "open" { let mut content = self.content.write().unwrap(); if let Some(item) = content.get_mut(self.selected) { let queue = self.queue.clone(); let library = self.library.clone(); if let Some(view) = item.open(queue, library) { return Ok(CommandResult::View(view)); } } } if cmd == "goto" { let mut content = self.content.write().unwrap(); if let Some(item) = content.get_mut(self.selected) { let queue = self.queue.clone(); let library = self.library.clone(); let arg = args.get(0).cloned().unwrap_or_default(); if arg == "album" { if let Some(album) = item.album(queue.clone()) { let view = AlbumView::new(queue, library, &album).as_boxed_view_ext(); return Ok(CommandResult::View(view)); } } if arg == "artist" { if let Some(artist) = item.artist() { let view = ArtistView::new(queue, library, &artist).as_boxed_view_ext(); return Ok(CommandResult::View(view)); } } } } Ok(CommandResult::Ignored) } }