pub mod layout;
pub mod pane;
pub mod panes;
pub mod text;
pub mod theme;
pub mod widgets;
use ratatui::Frame;
use widgets::{about_modal, help_modal, playlist_modal, search_modal};
use widgets::playlist_modal::PlaylistModalMode;
use crate::app::App;
use crate::app::state::{FocusedPane, InfoView, Tab};
use layout::LayoutAreas;
use pane::Pane;
use panes::dir_browser_pane::DirBrowserPane;
use panes::library_pane::LibraryPane;
use panes::list_pane::{self, ListPane};
use panes::track_info_pane::TrackInfoPane;
use panes::queue_pane::QueuePane;
pub const SPLASH_FADE_IN: f32 = 0.5;
pub const SPLASH_FADE_OUT_AT: f32 = 1.5;
pub const SPLASH_END: f32 = 2.0;
pub fn list_slot(tab: Tab) -> usize {
match tab {
Tab::Artists => 0,
Tab::Albums => 1,
Tab::Genre => 2,
Tab::Format => 3,
_ => 4, }
}
use theme::Theme;
use widgets::info_pane;
use widgets::progress_bar;
use widgets::status_bar;
use widgets::tab_bar;
pub struct Ui {
pub theme: Theme,
pub library_pane: LibraryPane,
pub dir_browser_pane: DirBrowserPane,
pub queue_pane: QueuePane,
pub list_panes: [ListPane; 5],
pub track_info_pane: TrackInfoPane,
pub last_click: Option<(std::time::Instant, u16, u16)>,
pub mouse_pos: Option<(u16, u16)>,
pub hovered_tab: Option<usize>,
pub pane_widths: [u16; 3],
pub dragging_border: Option<u8>,
pub right_split: u16,
pub show_help_modal: bool,
pub show_search_modal: bool,
pub search_modal_input: String,
pub search_modal_results: Vec<usize>,
pub search_modal_selected: usize,
pub search_modal_scroll: usize,
pub search_modal_result_height: usize,
pub search_modal_result_area: ratatui::layout::Rect,
pub search_modal_hover_row: Option<usize>,
pub show_playlist_modal: bool,
pub playlist_modal_selected: usize,
pub playlist_modal_mode: PlaylistModalMode,
pub playlist_modal_input: String,
pub show_about_modal: bool,
pub show_splash: bool,
pub splash_start: Option<std::time::Instant>,
pub info_view: InfoView,
pub album_art_cache: info_pane::AlbumArtCache,
}
impl Ui {
pub fn new(music_dir: std::path::PathBuf, picker: ratatui_image::picker::Picker) -> Self {
Self {
theme: Theme::default(),
library_pane: LibraryPane::new(),
dir_browser_pane: DirBrowserPane::new(music_dir),
queue_pane: QueuePane::new(),
list_panes: Default::default(),
track_info_pane: TrackInfoPane::new(),
last_click: None,
mouse_pos: None,
hovered_tab: None,
pane_widths: [20, 60, 20],
dragging_border: None,
right_split: 50,
show_help_modal: false,
show_search_modal: false,
search_modal_input: String::new(),
search_modal_results: Vec::new(),
search_modal_selected: 0,
search_modal_scroll: 0,
search_modal_result_height: 10,
search_modal_result_area: ratatui::layout::Rect::default(),
search_modal_hover_row: None,
show_playlist_modal: false,
playlist_modal_selected: 0,
playlist_modal_mode: PlaylistModalMode::List,
playlist_modal_input: String::new(),
show_about_modal: false,
show_splash: true,
splash_start: Some(std::time::Instant::now()),
info_view: InfoView::Clock,
album_art_cache: info_pane::AlbumArtCache::new(picker),
}
}
pub fn dismiss_splash(&mut self) {
self.show_splash = false;
self.splash_start = None;
}
pub fn render(&mut self, frame: &mut Frame, app: &App) {
if self.show_splash {
let elapsed = self.splash_start
.map(|s| s.elapsed().as_secs_f32())
.unwrap_or(SPLASH_END);
let opacity = if elapsed < SPLASH_FADE_IN {
elapsed / SPLASH_FADE_IN
} else if elapsed < SPLASH_FADE_OUT_AT {
1.0
} else {
(1.0 - (elapsed - SPLASH_FADE_OUT_AT) / (SPLASH_END - SPLASH_FADE_OUT_AT)).max(0.0)
};
about_modal::render_splash_screen(frame, frame.area(), &self.theme, opacity);
return;
}
let areas = LayoutAreas::compute(frame.area(), self.pane_widths, self.right_split);
status_bar::render_status_bar(frame, areas.status_bar, app, &self.theme);
tab_bar::render_tab_bar(frame, areas.tab_bar, app.tab, self.hovered_tab, &self.theme);
let lib_focused = app.focus == FocusedPane::Library;
match app.tab {
Tab::Queue => self.library_pane.render(frame, areas.library, lib_focused, app, &self.theme),
Tab::Directories => self.dir_browser_pane.render(frame, areas.library, lib_focused, app, &self.theme),
tab => {
let rows = list_pane::rows_for(app, tab);
let theme = &self.theme;
self.list_panes[list_slot(tab)]
.render(frame, areas.library, lib_focused, theme, &rows);
}
}
let playlist_focused = app.focus == FocusedPane::Playlist;
self.queue_pane.render(frame, areas.playlist, playlist_focused, app, &self.theme);
info_pane::render_info_pane(frame, areas.info_pane, app, &self.theme, self.info_view, &mut self.album_art_cache);
let lyrics_focused = app.focus == FocusedPane::Lyrics;
self.track_info_pane.render(frame, areas.lyrics, lyrics_focused, app, &self.theme);
progress_bar::render_progress_bar(frame, areas.progress_bar, app, &self.theme);
if self.show_search_modal {
let (rh, ra) = search_modal::render_search_modal(
frame,
frame.area(),
&self.search_modal_input,
&self.search_modal_results,
self.search_modal_selected,
self.search_modal_scroll,
self.search_modal_hover_row,
app,
&self.theme,
);
self.search_modal_result_height = rh;
self.search_modal_result_area = ra;
}
if self.show_help_modal {
help_modal::render_help_modal(frame, frame.area(), &self.theme);
}
if self.show_about_modal {
about_modal::render_about_modal(frame, frame.area(), &self.theme);
}
if self.show_playlist_modal {
playlist_modal::render_playlist_modal(
frame,
frame.area(),
self.playlist_modal_selected,
&self.playlist_modal_mode,
&self.playlist_modal_input,
app,
&self.theme,
);
}
}
pub fn list_pane_mut(&mut self, tab: Tab) -> &mut ListPane {
&mut self.list_panes[list_slot(tab)]
}
pub fn library_pane_scroll(&self, app: &App) -> usize {
match app.tab {
Tab::Queue => self.library_pane.scroll_offset,
Tab::Directories => self.dir_browser_pane.scroll_offset,
tab => self.list_panes[list_slot(tab)].scroll_offset,
}
}
pub fn refresh_dir_browser(&mut self, app: &App) {
self.dir_browser_pane.refresh(app);
}
pub fn refresh_search_results(&mut self, app: &App) {
if !self.show_search_modal {
self.search_modal_results.clear();
return;
}
self.search_modal_results = app.library.search(&self.search_modal_input);
self.search_modal_selected = 0;
self.search_modal_scroll = 0;
self.search_modal_hover_row = None;
}
pub fn clamp_selections(&mut self, app: &App) {
for tab in [Tab::Artists, Tab::Albums, Tab::Genre, Tab::Format, Tab::Playlists] {
let len = list_pane::row_count(app, tab);
let pane = &mut self.list_panes[list_slot(tab)];
pane.selected = pane.selected.min(len.saturating_sub(1));
pane.scroll_offset = pane.scroll_offset.min(len.saturating_sub(1));
}
self.library_pane.selected = 0;
self.library_pane.scroll_offset = 0;
self.dir_browser_pane.selected = 0;
self.dir_browser_pane.scroll_offset = 0;
let queue_len = app.queue.tracks.len();
if queue_len == 0 {
self.queue_pane.scroll_offset = 0;
} else {
self.queue_pane.scroll_offset = self.queue_pane.scroll_offset.min(queue_len - 1);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ui() -> Ui {
Ui::new(
std::path::PathBuf::from("."),
ratatui_image::picker::Picker::from_fontsize((8, 16)),
)
}
#[test]
fn a_keypress_drops_the_splash_immediately() {
let mut ui = ui();
assert!(ui.show_splash);
ui.dismiss_splash();
assert!(!ui.show_splash);
assert!(ui.splash_start.is_none());
}
#[test]
fn dismissing_twice_is_harmless() {
let mut ui = ui();
ui.dismiss_splash();
ui.dismiss_splash();
assert!(!ui.show_splash);
}
}