From 608c7952100331ce8fec03ee116a66df360c3e5a Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Thu, 2 Mar 2023 22:25:31 -0500 Subject: [PATCH 01/13] add `border_style` config option --- spotify_player/src/config/mod.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/spotify_player/src/config/mod.rs b/spotify_player/src/config/mod.rs index 7d446e09..344d6e61 100644 --- a/spotify_player/src/config/mod.rs +++ b/spotify_player/src/config/mod.rs @@ -49,6 +49,8 @@ pub struct AppConfig { pub liked_icon: String, // layout configs + pub border_type: BorderType, + pub playback_window_position: Position, #[cfg(feature = "image")] @@ -75,6 +77,16 @@ pub enum Position { } config_parser_impl!(Position); +#[derive(Debug, Deserialize, Clone)] +pub enum BorderType { + None, + Plain, + Rounded, + Double, + Thick, +} +config_parser_impl!(BorderType); + #[derive(Debug, Deserialize, ConfigParse, Clone)] pub struct Command { pub command: String, @@ -144,6 +156,8 @@ impl Default for AppConfig { play_icon: "▶".to_string(), liked_icon: "♥".to_string(), + border_type: BorderType::Plain, + playback_window_position: Position::Top, #[cfg(feature = "image")] From cd3bdbee5aa27fcd5c29265a68d43d910be5ce6b Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Thu, 2 Mar 2023 22:30:29 -0500 Subject: [PATCH 02/13] allow to configure border type --- spotify_player/src/ui/page.rs | 32 +++++++++++---------- spotify_player/src/ui/playback.rs | 7 ++--- spotify_player/src/ui/popup.rs | 48 +++++++++++-------------------- spotify_player/src/ui/utils.rs | 46 ++++++++++++++++++++--------- 4 files changed, 67 insertions(+), 66 deletions(-) diff --git a/spotify_player/src/ui/page.rs b/spotify_player/src/ui/page.rs index bb16a8e1..b01e3ed1 100644 --- a/spotify_player/src/ui/page.rs +++ b/spotify_player/src/ui/page.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{utils::construct_block, *}; pub fn render_search_page( is_active: bool, @@ -33,6 +33,7 @@ pub fn render_search_page( let is_active = is_active && focus_state == SearchFocusState::Tracks; utils::construct_list_widget( + state, &ui.theme, track_items, &format!("Tracks{}", if is_active { " [*]" } else { "" }), @@ -54,6 +55,7 @@ pub fn render_search_page( let is_active = is_active && focus_state == SearchFocusState::Albums; utils::construct_list_widget( + state, &ui.theme, album_items, &format!("Albums{}", if is_active { " [*]" } else { "" }), @@ -75,6 +77,7 @@ pub fn render_search_page( let is_active = is_active && focus_state == SearchFocusState::Artists; utils::construct_list_widget( + state, &ui.theme, artist_items, &format!("Artists{}", if is_active { " [*]" } else { "" }), @@ -96,6 +99,7 @@ pub fn render_search_page( let is_active = is_active && focus_state == SearchFocusState::Playlists; utils::construct_list_widget( + state, &ui.theme, playlist_items, &format!("Playlists{}", if is_active { " [*]" } else { "" }), @@ -105,10 +109,7 @@ pub fn render_search_page( }; // renders borders with title - let block = Block::default() - .title(ui.theme.block_title_with_style("Search")) - .borders(Borders::ALL) - .border_style(ui.theme.border()); + let block = construct_block("Search", &ui.theme, state, None); frame.render_widget(block, rect); // renders the query input box @@ -197,10 +198,7 @@ pub fn render_context_page( s => anyhow::bail!("expect a context page state, found {s:?}"), }; - let block = Block::default() - .title(ui.theme.block_title_with_style(context_page_type.title())) - .borders(Borders::ALL) - .border_style(ui.theme.border()); + let block = construct_block(&context_page_type.title(), &ui.theme, state, None); let id = match id { None => { @@ -322,6 +320,7 @@ pub fn render_library_page( // Construct the playlist window let (playlist_list, n_playlists) = utils::construct_list_widget( + state, &ui.theme, ui.search_filtered_items(&data.user_data.playlists) .into_iter() @@ -333,6 +332,7 @@ pub fn render_library_page( ); // Construct the saved album window let (album_list, n_albums) = utils::construct_list_widget( + state, &ui.theme, ui.search_filtered_items(&data.user_data.saved_albums) .into_iter() @@ -344,6 +344,7 @@ pub fn render_library_page( ); // Construct the followed artist window let (artist_list, n_artists) = utils::construct_list_widget( + state, &ui.theme, ui.search_filtered_items(&data.user_data.followed_artists) .into_iter() @@ -396,8 +397,9 @@ pub fn render_browse_page( let data = state.data.read(); let (list, len) = match ui.current_page() { - PageState::Browse { state } => match state { + PageState::Browse { state: ui_state } => match ui_state { BrowsePageUIState::CategoryList { .. } => utils::construct_list_widget( + state, &ui.theme, ui.search_filtered_items(&data.browse.categories) .into_iter() @@ -412,11 +414,12 @@ pub fn render_browse_page( let playlists = match data.browse.category_playlists.get(&category.id) { Some(playlists) => playlists, None => { - utils::render_loading_window(&ui.theme, frame, rect, &title); + utils::render_loading_window(state, &ui.theme, frame, rect, &title); return Ok(()); } }; utils::construct_list_widget( + state, &ui.theme, ui.search_filtered_items(playlists) .into_iter() @@ -451,10 +454,7 @@ pub fn render_lyric_page( ) -> Result<()> { let data = state.data.read(); - let block = Block::default() - .title(ui.theme.block_title_with_style("Lyric")) - .borders(Borders::ALL) - .border_style(ui.theme.border()); + let block = construct_block("Lyric", &ui.theme, state, None); let (track, artists, scroll_offset) = match ui.current_page_mut() { PageState::Lyric { @@ -575,6 +575,7 @@ fn render_artist_context_page_windows( .collect::>(); utils::construct_list_widget( + state, &ui.theme, album_items, "Albums", @@ -591,6 +592,7 @@ fn render_artist_context_page_windows( .collect::>(); utils::construct_list_widget( + state, &ui.theme, artist_items, "Related Artists", diff --git a/spotify_player/src/ui/playback.rs b/spotify_player/src/ui/playback.rs index c32b4965..e520c5c3 100644 --- a/spotify_player/src/ui/playback.rs +++ b/spotify_player/src/ui/playback.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{utils::construct_block, *}; /// Renders a playback window showing information about the current playback, which includes /// - track title, artists, album @@ -12,10 +12,7 @@ pub fn render_playback_window( rect: Rect, ) -> Result<()> { // render borders and title - let block = Block::default() - .title(ui.theme.block_title_with_style("Playback")) - .borders(Borders::ALL) - .border_style(ui.theme.border()); + let block = construct_block("Playback", &ui.theme, state, None); frame.render_widget(block, rect); let rect = { diff --git a/spotify_player/src/ui/popup.rs b/spotify_player/src/ui/popup.rs index ec69d8d9..1caf58a8 100644 --- a/spotify_player/src/ui/popup.rs +++ b/spotify_player/src/ui/popup.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{utils::construct_block, *}; use std::collections::{btree_map::Entry, BTreeMap}; const SHORTCUT_TABLE_N_COLUMNS: usize = 3; @@ -33,12 +33,8 @@ pub fn render_popup( .constraints([Constraint::Min(0), Constraint::Length(3)].as_ref()) .split(rect); - let widget = Paragraph::new(format!("/{query}")).block( - Block::default() - .borders(Borders::ALL) - .border_style(ui.theme.border()) - .title(ui.theme.block_title_with_style("Search")), - ); + let widget = Paragraph::new(format!("/{query}")) + .block(construct_block("Search", &ui.theme, state, None)); frame.render_widget(widget, chunks[1]); (chunks[0], true) } @@ -72,6 +68,7 @@ pub fn render_popup( .map(|d| (d, false)) .collect(), 7, + state, ui, ); (rect, false) @@ -89,13 +86,13 @@ pub fn render_popup( .map(|d| (format!("{} | {}", d.name, d.id), current_device_id == d.id)) .collect(); - let rect = render_list_popup(frame, rect, "Devices", items, 5, ui); + let rect = render_list_popup(frame, rect, "Devices", items, 5, state, ui); (rect, false) } PopupState::ThemeList(themes, ..) => { let items = themes.iter().map(|t| (t.name.clone(), false)).collect(); - let rect = render_list_popup(frame, rect, "Themes", items, 7, ui); + let rect = render_list_popup(frame, rect, "Themes", items, 7, state, ui); (rect, false) } PopupState::UserPlaylistList(action, _) => { @@ -109,7 +106,7 @@ pub fn render_popup( .map(|p| (p.to_string(), false)) .collect(); - let rect = render_list_popup(frame, rect, "User Playlists", items, 10, ui); + let rect = render_list_popup(frame, rect, "User Playlists", items, 10, state, ui); (rect, false) } PopupState::UserFollowedArtistList { .. } => { @@ -122,7 +119,8 @@ pub fn render_popup( .map(|a| (a.to_string(), false)) .collect(); - let rect = render_list_popup(frame, rect, "User Followed Artists", items, 7, ui); + let rect = + render_list_popup(frame, rect, "User Followed Artists", items, 7, state, ui); (rect, false) } PopupState::UserSavedAlbumList { .. } => { @@ -135,13 +133,13 @@ pub fn render_popup( .map(|a| (a.to_string(), false)) .collect(); - let rect = render_list_popup(frame, rect, "User Saved Albums", items, 7, ui); + let rect = render_list_popup(frame, rect, "User Saved Albums", items, 7, state, ui); (rect, false) } PopupState::ArtistList(_, artists, ..) => { let items = artists.iter().map(|a| (a.to_string(), false)).collect(); - let rect = render_list_popup(frame, rect, "Artists", items, 5, ui); + let rect = render_list_popup(frame, rect, "Artists", items, 5, state, ui); (rect, false) } }, @@ -155,6 +153,7 @@ fn render_list_popup( title: &str, items: Vec<(String, bool)>, length: u16, + state: &SharedState, ui: &mut UIStateGuard, ) -> Rect { let chunks = Layout::default() @@ -162,7 +161,7 @@ fn render_list_popup( .constraints([Constraint::Min(0), Constraint::Length(length)].as_ref()) .split(rect); - let (list, len) = utils::construct_list_widget(&ui.theme, items, title, true, None); + let (list, len) = utils::construct_list_widget(state, &ui.theme, items, title, true, None); utils::render_list_window( frame, @@ -222,12 +221,7 @@ pub fn render_shortcut_help_popup( .collect::>(), ) .widths(&SHORTCUT_TABLE_CONSTRAINS) - .block( - Block::default() - .title(ui.theme.block_title_with_style("Shortcuts")) - .borders(Borders::ALL) - .border_style(ui.theme.border()), - ); + .block(construct_block("Shortcuts", &ui.theme, state, None)); frame.render_widget(help_table, chunks[1]); chunks[0] } @@ -286,12 +280,7 @@ pub fn render_commands_help_popup( .style(ui.theme.table_header()), ) .widths(&COMMAND_TABLE_CONSTRAINTS) - .block( - Block::default() - .title(ui.theme.block_title_with_style("Commands")) - .borders(Borders::ALL) - .border_style(ui.theme.border()), - ); + .block(construct_block("Commands", &ui.theme, state, None)); frame.render_widget(help_table, rect); } @@ -344,12 +333,7 @@ pub fn render_queue_popup( ) .header(Row::new(vec![Cell::from("#"), Cell::from("Title")]).style(ui.theme.table_header())) .widths(&[Constraint::Percentage(10), Constraint::Percentage(90)]) - .block( - Block::default() - .title(ui.theme.block_title_with_style("Queue")) - .borders(Borders::ALL) - .border_style(ui.theme.border()), - ) + .block(construct_block("Queue", &ui.theme, state, None)) }; frame.render_widget(queue_table, rect); } diff --git a/spotify_player/src/ui/utils.rs b/spotify_player/src/ui/utils.rs index 5ec02b72..14d90a72 100644 --- a/spotify_player/src/ui/utils.rs +++ b/spotify_player/src/ui/utils.rs @@ -1,7 +1,30 @@ use super::*; +pub fn construct_block<'a>( + title: &str, + theme: &config::Theme, + state: &SharedState, + borders: Option, +) -> Block<'a> { + let borders = borders.unwrap_or(Borders::ALL); + let (borders, border_type) = match state.app_config.border_type { + config::BorderType::None => (Borders::NONE, BorderType::Plain), + config::BorderType::Plain => (borders, BorderType::Plain), + config::BorderType::Rounded => (borders, BorderType::Rounded), + config::BorderType::Double => (borders, BorderType::Double), + config::BorderType::Thick => (borders, BorderType::Thick), + }; + + Block::default() + .title(theme.block_title_with_style(title)) + .borders(borders) + .border_style(theme.border()) + .border_type(border_type) +} + /// constructs a generic list widget pub fn construct_list_widget<'a>( + state: &SharedState, theme: &config::Theme, items: Vec<(String, bool)>, title: &str, @@ -9,7 +32,6 @@ pub fn construct_list_widget<'a>( borders: Option, ) -> (List<'a>, usize) { let n_items = items.len(); - let borders = borders.unwrap_or(Borders::ALL); ( List::new( @@ -25,12 +47,7 @@ pub fn construct_list_widget<'a>( .collect::>(), ) .highlight_style(theme.selection_style(is_active)) - .block( - Block::default() - .title(theme.block_title_with_style(title)) - .borders(borders) - .border_style(theme.border()), - ), + .block(construct_block(title, theme, state, borders)), n_items, ) } @@ -75,14 +92,15 @@ pub fn render_table_window( frame.render_stateful_widget(widget, rect, state); } -pub fn render_loading_window(theme: &config::Theme, frame: &mut Frame, rect: Rect, title: &str) { +pub fn render_loading_window( + state: &SharedState, + theme: &config::Theme, + frame: &mut Frame, + rect: Rect, + title: &str, +) { frame.render_widget( - Paragraph::new("Loading...").block( - Block::default() - .title(theme.block_title_with_style(title)) - .borders(Borders::ALL) - .border_style(theme.border()), - ), + Paragraph::new("Loading...").block(construct_block(title, theme, state, None)), rect, ); } From 43302978a9d164e03e453074ef48cd2b8595e1be Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Fri, 3 Mar 2023 17:25:15 -0500 Subject: [PATCH 03/13] change `construct_block` to `construct_and_render_block` This is to force manually rendering blocks. --- spotify_player/src/ui/utils.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/spotify_player/src/ui/utils.rs b/spotify_player/src/ui/utils.rs index 14d90a72..fae294f1 100644 --- a/spotify_player/src/ui/utils.rs +++ b/spotify_player/src/ui/utils.rs @@ -1,12 +1,17 @@ use super::*; -pub fn construct_block<'a>( +/// Construct and render a block. +/// +/// This function should only be used to render a window's borders and its title. +/// It returns the rectangle to render the inner widgets inside the block. +pub fn construct_and_render_block( title: &str, theme: &config::Theme, state: &SharedState, - borders: Option, -) -> Block<'a> { - let borders = borders.unwrap_or(Borders::ALL); + borders: Borders, + frame: &mut Frame, + rect: Rect, +) -> Rect { let (borders, border_type) = match state.app_config.border_type { config::BorderType::None => (Borders::NONE, BorderType::Plain), config::BorderType::Plain => (borders, BorderType::Plain), @@ -15,11 +20,16 @@ pub fn construct_block<'a>( config::BorderType::Thick => (borders, BorderType::Thick), }; - Block::default() + let block = Block::default() .title(theme.block_title_with_style(title)) .borders(borders) .border_style(theme.border()) - .border_type(border_type) + .border_type(border_type); + + frame.render_widget(block, rect); + + // margin to separate the block with its inner widget(s) + Layout::default().margin(1).split(rect)[0] } /// constructs a generic list widget From cc3e6df07e8273a3f35657308502db6bff3262a0 Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Fri, 3 Mar 2023 17:26:50 -0500 Subject: [PATCH 04/13] don't wrap the list inside a block --- spotify_player/src/ui/utils.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/spotify_player/src/ui/utils.rs b/spotify_player/src/ui/utils.rs index fae294f1..f8b45adf 100644 --- a/spotify_player/src/ui/utils.rs +++ b/spotify_player/src/ui/utils.rs @@ -37,9 +37,7 @@ pub fn construct_list_widget<'a>( state: &SharedState, theme: &config::Theme, items: Vec<(String, bool)>, - title: &str, is_active: bool, - borders: Option, ) -> (List<'a>, usize) { let n_items = items.len(); @@ -56,8 +54,7 @@ pub fn construct_list_widget<'a>( }) .collect::>(), ) - .highlight_style(theme.selection_style(is_active)) - .block(construct_block(title, theme, state, borders)), + .highlight_style(theme.selection_style(is_active)), n_items, ) } @@ -109,8 +106,6 @@ pub fn render_loading_window( rect: Rect, title: &str, ) { - frame.render_widget( - Paragraph::new("Loading...").block(construct_block(title, theme, state, None)), - rect, - ); + let rect = construct_and_render_block(title, theme, state, None, frame, rect); + frame.render_widget(Paragraph::new("Loading..."), rect); } From 647b75469a8d07d405d9d25eb452cf55d2cb97e3 Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Fri, 3 Mar 2023 17:27:36 -0500 Subject: [PATCH 05/13] update the page rendering codes - to manually render a window's block - to make the codes more organized and follow a given structure --- spotify_player/src/ui/page.rs | 357 +++++++++++++++++++--------------- 1 file changed, 203 insertions(+), 154 deletions(-) diff --git a/spotify_player/src/ui/page.rs b/spotify_player/src/ui/page.rs index b01e3ed1..71843980 100644 --- a/spotify_player/src/ui/page.rs +++ b/spotify_player/src/ui/page.rs @@ -1,4 +1,10 @@ -use super::{utils::construct_block, *}; +/// UI codes to render a page. +/// A `render_*_page` function should follow (not strictly) the below steps +/// 1. get the data from the application's states +/// 2. construct the page's layout +/// 3. construct the page's widgets +/// 4. render the widgets +use super::{utils::construct_and_render_block, *}; pub fn render_search_page( is_active: bool, @@ -7,6 +13,7 @@ pub fn render_search_page( ui: &mut UIStateGuard, rect: Rect, ) -> Result<()> { + // 1. Get the data let data = state.data.read(); let (focus_state, current_query, input) = match ui.current_page() { @@ -20,6 +27,53 @@ pub fn render_search_page( let search_results = data.caches.search.peek(current_query); + // 2. Construct the page's layout + let rect = construct_and_render_block("Search", &ui.theme, state, Borders::ALL, frame, rect); + + // search input's layout + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(0)].as_ref()) + .split(rect); + let search_input_rect = chunks[0]; + let rect = chunks[1]; + + // track/album/artist/playlist search results layout (2x2 table) + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(rect) + .into_iter() + .flat_map(|rect| { + Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(rect) + }) + .collect::>(); + + let track_rect = construct_and_render_block( + "Tracks", + &ui.theme, + state, + Borders::TOP | Borders::RIGHT, + frame, + rect, + ); + let album_rect = + construct_and_render_block("Albums", &ui.theme, state, Borders::TOP, frame, rect); + let artist_rect = construct_and_render_block( + "Artists", + &ui.theme, + state, + Borders::TOP | Borders::RIGHT, + frame, + rect, + ); + let playlist_rect = + construct_and_render_block("Playlists", &ui.theme, state, Borders::TOP, frame, rect); + + // 3. Construct the page's widgets let (track_list, n_tracks) = { let track_items = search_results .map(|s| { @@ -32,14 +86,7 @@ pub fn render_search_page( let is_active = is_active && focus_state == SearchFocusState::Tracks; - utils::construct_list_widget( - state, - &ui.theme, - track_items, - &format!("Tracks{}", if is_active { " [*]" } else { "" }), - is_active, - Some(Borders::TOP | Borders::RIGHT), - ) + utils::construct_list_widget(state, &ui.theme, track_items, is_active) }; let (album_list, n_albums) = { @@ -54,14 +101,7 @@ pub fn render_search_page( let is_active = is_active && focus_state == SearchFocusState::Albums; - utils::construct_list_widget( - state, - &ui.theme, - album_items, - &format!("Albums{}", if is_active { " [*]" } else { "" }), - is_active, - Some(Borders::TOP), - ) + utils::construct_list_widget(state, &ui.theme, album_items, is_active) }; let (artist_list, n_artists) = { @@ -76,14 +116,7 @@ pub fn render_search_page( let is_active = is_active && focus_state == SearchFocusState::Artists; - utils::construct_list_widget( - state, - &ui.theme, - artist_items, - &format!("Artists{}", if is_active { " [*]" } else { "" }), - is_active, - Some(Borders::TOP | Borders::RIGHT), - ) + utils::construct_list_widget(state, &ui.theme, artist_items, is_active) }; let (playlist_list, n_playlists) = { @@ -98,54 +131,21 @@ pub fn render_search_page( let is_active = is_active && focus_state == SearchFocusState::Playlists; - utils::construct_list_widget( - state, - &ui.theme, - playlist_items, - &format!("Playlists{}", if is_active { " [*]" } else { "" }), - is_active, - Some(Borders::TOP), - ) + utils::construct_list_widget(state, &ui.theme, playlist_items, is_active) }; - // renders borders with title - let block = construct_block("Search", &ui.theme, state, None); - frame.render_widget(block, rect); - - // renders the query input box - let rect = { - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints([Constraint::Length(1), Constraint::Min(0)].as_ref()) - .split(rect); - - let is_active = is_active && focus_state == SearchFocusState::Input; - - frame.render_widget( - Paragraph::new(input.clone()).style(ui.theme.selection_style(is_active)), - chunks[0], - ); - - chunks[1] - }; - - // split the given `rect` layout into a 2x2 layout consiting of 4 chunks - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(rect) - .into_iter() - .flat_map(|rect| { - Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(rect) - }) - .collect::>(); + // 4. Render the page's widgets + // Render the query input box + frame.render_widget( + Paragraph::new(input.clone()).style( + ui.theme + .selection_style(is_active && focus_state == SearchFocusState::Input), + ), + chunks[0], + ); - // Render the search page's windows. - // Will need mutable access to the list/table states stored inside the page state for rendering. + // Render the search result windows. + // Need mutable access to the list/table states stored inside the page state for rendering. let page_state = match ui.current_page_mut() { PageState::Search { state, .. } => state, s => anyhow::bail!("expect a search page state, found {s:?}"), @@ -153,28 +153,28 @@ pub fn render_search_page( utils::render_list_window( frame, track_list, - chunks[0], + track_rect, n_tracks, &mut page_state.track_list, ); utils::render_list_window( frame, album_list, - chunks[1], + album_rect, n_albums, &mut page_state.album_list, ); utils::render_list_window( frame, artist_list, - chunks[2], + artist_rect, n_artists, &mut page_state.artist_list, ); utils::render_list_window( frame, playlist_list, - chunks[3], + playlist_rect, n_playlists, &mut page_state.playlist_list, ); @@ -189,6 +189,7 @@ pub fn render_context_page( ui: &mut UIStateGuard, rect: Rect, ) -> Result<()> { + // 1. Get the data let (id, context_page_type) = match ui.current_page() { PageState::Context { id, @@ -198,12 +199,21 @@ pub fn render_context_page( s => anyhow::bail!("expect a context page state, found {s:?}"), }; - let block = construct_block(&context_page_type.title(), &ui.theme, state, None); + // 2. Construct the page's layout + let rect = construct_and_render_block( + &context_page_type.title(), + &ui.theme, + state, + Borders::ALL, + frame, + rect, + ); + // 3+4. Construct and render the page's widgets let id = match id { None => { frame.render_widget( - Paragraph::new("Cannot determine the current page's context").block(block), + Paragraph::new("Cannot determine the current page's context"), rect, ); return Ok(()); @@ -214,17 +224,15 @@ pub fn render_context_page( let data = state.data.read(); match data.caches.context.peek(&id.uri()) { Some(context) => { - frame.render_widget(block, rect); - // render context description let chunks = Layout::default() .direction(Direction::Vertical) - .margin(1) .constraints([Constraint::Length(1), Constraint::Min(0)].as_ref()) .split(rect); - let page_desc = Paragraph::new(context.description()) - .block(Block::default().style(ui.theme.page_desc())); - frame.render_widget(page_desc, chunks[0]); + frame.render_widget( + Paragraph::new(Text::styled(context.description(), ui.theme.page_desc())), + chunks[0], + ); match context { Context::Artist { @@ -279,7 +287,7 @@ pub fn render_context_page( } } None => { - frame.render_widget(Paragraph::new("Loading...").block(block), rect); + frame.render_widget(Paragraph::new("Loading..."), rect); } } @@ -293,6 +301,7 @@ pub fn render_library_page( ui: &mut UIStateGuard, rect: Rect, ) -> Result<()> { + // 1. Get the data let curr_context_uri = state.player.read().playing_context_id().map(|c| c.uri()); let data = state.data.read(); @@ -301,6 +310,7 @@ pub fn render_library_page( s => anyhow::bail!("expect a library page state, found {s:?}"), }; + // 2. Construct the page's layout // Horizontally split the library page into 3 windows: // - a playlists window // - a saved albums window @@ -316,8 +326,26 @@ pub fn render_library_page( .as_ref(), ) .split(rect); - let (playlist_rect, album_rect, artist_rect) = (chunks[0], chunks[1], chunks[2]); + let playlist_rect = construct_and_render_block( + "Playlists", + &ui.theme, + state, + Borders::TOP | Borders::LEFT | Borders::BOTTOM, + frame, + chunks[0], + ); + let album_rect = construct_and_render_block( + "Albums", + &ui.theme, + state, + Borders::TOP | Borders::LEFT | Borders::BOTTOM, + frame, + chunks[0], + ); + let artist_rect = + construct_and_render_block("Artists", &ui.theme, state, Borders::ALL, frame, chunks[0]); + // 3. Construct the page's widgets // Construct the playlist window let (playlist_list, n_playlists) = utils::construct_list_widget( state, @@ -326,9 +354,7 @@ pub fn render_library_page( .into_iter() .map(|p| (p.to_string(), curr_context_uri == Some(p.id.uri()))) .collect(), - "Playlists", is_active && focus_state == LibraryFocusState::Playlists, - Some((Borders::TOP | Borders::LEFT) | Borders::BOTTOM), ); // Construct the saved album window let (album_list, n_albums) = utils::construct_list_widget( @@ -338,9 +364,7 @@ pub fn render_library_page( .into_iter() .map(|a| (a.to_string(), curr_context_uri == Some(a.id.uri()))) .collect(), - "Albums", is_active && focus_state == LibraryFocusState::SavedAlbums, - Some((Borders::TOP | Borders::LEFT) | Borders::BOTTOM), ); // Construct the followed artist window let (artist_list, n_artists) = utils::construct_list_widget( @@ -350,11 +374,10 @@ pub fn render_library_page( .into_iter() .map(|a| (a.to_string(), curr_context_uri == Some(a.id.uri()))) .collect(), - "Artists", is_active && focus_state == LibraryFocusState::FollowedArtists, - None, ); + // 4. Render the page's widgets // Render the library page's windows. // Will need mutable access to the list/table states stored inside the page state for rendering. let page_state = match ui.current_page_mut() { @@ -392,23 +415,34 @@ pub fn render_browse_page( frame: &mut Frame, state: &SharedState, ui: &mut UIStateGuard, - rect: Rect, + mut rect: Rect, ) -> Result<()> { + // 1. Get the data let data = state.data.read(); + // 2+3. Construct the page's layout and widgets let (list, len) = match ui.current_page() { PageState::Browse { state: ui_state } => match ui_state { - BrowsePageUIState::CategoryList { .. } => utils::construct_list_widget( - state, - &ui.theme, - ui.search_filtered_items(&data.browse.categories) - .into_iter() - .map(|c| (c.name.clone(), false)) - .collect(), - "Categories", - is_active, - None, - ), + BrowsePageUIState::CategoryList { .. } => { + rect = construct_and_render_block( + "Categories", + &ui.theme, + &state, + Borders::ALL, + frame, + rect, + ); + + utils::construct_list_widget( + state, + &ui.theme, + ui.search_filtered_items(&data.browse.categories) + .into_iter() + .map(|c| (c.name.clone(), false)) + .collect(), + is_active, + ) + } BrowsePageUIState::CategoryPlaylistList { category, .. } => { let title = format!("{} Playlists", category.name); let playlists = match data.browse.category_playlists.get(&category.id) { @@ -418,6 +452,16 @@ pub fn render_browse_page( return Ok(()); } }; + + rect = construct_and_render_block( + &title, + &ui.theme, + &state, + Borders::ALL, + frame, + rect, + ); + utils::construct_list_widget( state, &ui.theme, @@ -425,15 +469,14 @@ pub fn render_browse_page( .into_iter() .map(|c| (c.name.clone(), false)) .collect(), - &title, is_active, - None, ) } }, s => anyhow::bail!("expect a browse page state, found {s:?}"), }; + // 4. Render the page's widgets let list_state = match ui.current_page_mut().focus_window_state_mut() { Some(MutableWindowState::List(list_state)) => list_state, _ => anyhow::bail!("expect a list for the focused window"), @@ -452,10 +495,9 @@ pub fn render_lyric_page( ui: &mut UIStateGuard, rect: Rect, ) -> Result<()> { + // 1. Get the data let data = state.data.read(); - let block = construct_block("Lyric", &ui.theme, state, None); - let (track, artists, scroll_offset) = match ui.current_page_mut() { PageState::Lyric { track, @@ -465,13 +507,21 @@ pub fn render_lyric_page( s => anyhow::bail!("expect a lyric page state, found {s:?}"), }; + // 2. Construct the app's layout + let rect = construct_and_render_block("Lyric", &ui.theme, state, Borders::ALL, frame, rect); + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(0)].as_ref()) + .split(rect); + + // 3. Construct the app's widgets let (desc, lyric) = match data.caches.lyrics.peek(&format!("{track} {artists}")) { None => { - frame.render_widget(Paragraph::new("Loading...").block(block), rect); + frame.render_widget(Paragraph::new("Loading..."), rect); return Ok(()); } Some(lyric_finder::LyricResult::None) => { - frame.render_widget(Paragraph::new("Lyric not found").block(block), rect); + frame.render_widget(Paragraph::new("Lyric not found"), rect); return Ok(()); } Some(lyric_finder::LyricResult::Some { @@ -488,26 +538,13 @@ pub fn render_lyric_page( } let scroll_offset = *scroll_offset; - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints([Constraint::Length(1), Constraint::Min(0)].as_ref()) - .split(rect); - - // render lyric page borders - frame.render_widget(block, rect); - + // 4. Render the app's widgets // render lyric page description text - frame.render_widget( - Paragraph::new(desc).block(Block::default().style(ui.theme.page_desc())), - chunks[0], - ); + frame.render_widget(Paragraph::new(desc), chunks[0]); // render lyric text frame.render_widget( - Paragraph::new(lyric) - .scroll((scroll_offset as u16, 0)) - .block(Block::default()), + Paragraph::new(lyric).scroll((scroll_offset as u16, 0)), chunks[1], ); @@ -527,6 +564,7 @@ fn render_artist_context_page_windows( rect: Rect, artist_data: (&[Track], &[Album], &[Artist]), ) -> Result<()> { + // 1. Get the data let (tracks, albums, artists) = ( ui.search_filtered_items(artist_data.0), ui.search_filtered_items(artist_data.1), @@ -541,33 +579,32 @@ fn render_artist_context_page_windows( s => anyhow::bail!("expect an artist context page state, found {s:?}"), }; - let rect = { - // render the top tracks table for artist context window - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(12), Constraint::Min(1)].as_ref()) - .split(rect); - - render_track_table_window( - frame, - chunks[0], - is_active && focus_state == ArtistFocusState::TopTracks, - state, - tracks, - ui, - data, - )?; - - chunks[1] - }; + // 2. Construct the app's layout + // top tracks window + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(12), Constraint::Min(1)].as_ref()) + .split(rect); + let top_tracks_rect = chunks[0]; + // albums and related artitsts windows let chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(rect); + .split(chunks[1]); + let albums_rect = + construct_and_render_block("Albums", &ui.theme, state, Borders::TOP, frame, chunks[0]); + let related_artists_rect = construct_and_render_block( + "Related Artists", + &ui.theme, + state, + Borders::TOP | Borders::LEFT, + frame, + chunks[1], + ); - // construct album list widget + // 3. Construct the widgets + // album list widget let (album_list, n_albums) = { let album_items = albums .into_iter() @@ -578,13 +615,11 @@ fn render_artist_context_page_windows( state, &ui.theme, album_items, - "Albums", is_active && focus_state == ArtistFocusState::Albums, - Some(Borders::TOP), ) }; - // construct artist list widget + // artist list widget let (artist_list, n_artists) = { let artist_items = artists .into_iter() @@ -595,12 +630,11 @@ fn render_artist_context_page_windows( state, &ui.theme, artist_items, - "Related Artists", is_active && focus_state == ArtistFocusState::RelatedArtists, - Some(Borders::TOP | Borders::LEFT), ) }; + // 4. Render the widgets let (album_list_state, artist_list_state) = match ui.current_page_mut() { PageState::Context { state: @@ -614,8 +648,24 @@ fn render_artist_context_page_windows( s => anyhow::bail!("expect an artist context page state, found {s:?}"), }; - utils::render_list_window(frame, album_list, chunks[0], n_albums, album_list_state); - utils::render_list_window(frame, artist_list, chunks[1], n_artists, artist_list_state); + render_track_table_window( + frame, + top_tracks_rect, + is_active && focus_state == ArtistFocusState::TopTracks, + state, + tracks, + ui, + data, + )?; + + utils::render_list_window(frame, album_list, albums_rect, n_albums, album_list_state); + utils::render_list_window( + frame, + artist_list, + related_artists_rect, + n_artists, + artist_list_state, + ); Ok(()) } @@ -686,7 +736,6 @@ pub fn render_track_table_window( ]) .style(ui.theme.table_header()), ) - .block(Block::default()) .widths(&[ Constraint::Length(2), Constraint::Length(5), From de6ce4fe42fbb023cbd9fe98f5224f46fe519215 Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Fri, 3 Mar 2023 17:29:31 -0500 Subject: [PATCH 06/13] update playback rendering codes to use `construct_track_actions` --- spotify_player/src/ui/playback.rs | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/spotify_player/src/ui/playback.rs b/spotify_player/src/ui/playback.rs index e520c5c3..271476e8 100644 --- a/spotify_player/src/ui/playback.rs +++ b/spotify_player/src/ui/playback.rs @@ -1,4 +1,4 @@ -use super::{utils::construct_block, *}; +use super::{utils::construct_and_render_block, *}; /// Renders a playback window showing information about the current playback, which includes /// - track title, artists, album @@ -11,18 +11,7 @@ pub fn render_playback_window( ui: &mut UIStateGuard, rect: Rect, ) -> Result<()> { - // render borders and title - let block = construct_block("Playback", &ui.theme, state, None); - frame.render_widget(block, rect); - - let rect = { - // remove top/bot margins - Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0)].as_ref()) - .margin(1) - .split(rect)[0] - }; + let rect = construct_and_render_block("Playback", &ui.theme, state, Borders::ALL, frame, rect); let player = state.player.read(); if let Some(ref playback) = player.playback { @@ -127,8 +116,7 @@ pub fn render_playback_window( "No playback found. \ Please make sure there is a running Spotify client and try to connect to it using the `SwitchDevice` command." ) - .wrap(Wrap { trim: true }) - .block(Block::default()), + .wrap(Wrap { trim: true }), rect, ); }; @@ -210,9 +198,7 @@ fn render_playback_text( playback_text.lines.push(Spans::from(spans)); } - let playback_desc = Paragraph::new(playback_text) - .wrap(Wrap { trim: true }) - .block(Block::default()); + let playback_desc = Paragraph::new(playback_text).wrap(Wrap { trim: true }); frame.render_widget(playback_desc, rect); } @@ -225,7 +211,6 @@ fn render_playback_progress_bar( rect: Rect, ) { let progress_bar = Gauge::default() - .block(Block::default()) .gauge_style(ui.theme.playback_progress_bar()) .ratio(progress.as_secs_f64() / track.duration.as_secs_f64()) .label(Span::styled( From a68b1d7df6448b25f2e9a1a91072415516046fdb Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Fri, 3 Mar 2023 17:34:47 -0500 Subject: [PATCH 07/13] update popup rendering codes to manually render the popup's block --- spotify_player/src/ui/popup.rs | 46 +++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/spotify_player/src/ui/popup.rs b/spotify_player/src/ui/popup.rs index 1caf58a8..ed02d155 100644 --- a/spotify_player/src/ui/popup.rs +++ b/spotify_player/src/ui/popup.rs @@ -1,4 +1,4 @@ -use super::{utils::construct_block, *}; +use super::{utils::construct_and_render_block, *}; use std::collections::{btree_map::Entry, BTreeMap}; const SHORTCUT_TABLE_N_COLUMNS: usize = 3; @@ -33,9 +33,16 @@ pub fn render_popup( .constraints([Constraint::Min(0), Constraint::Length(3)].as_ref()) .split(rect); - let widget = Paragraph::new(format!("/{query}")) - .block(construct_block("Search", &ui.theme, state, None)); - frame.render_widget(widget, chunks[1]); + let rect = construct_and_render_block( + "Search", + &ui.theme, + state, + Borders::ALL, + frame, + chunks[1], + ); + + frame.render_widget(Paragraph::new(format!("/{query}")), rect); (chunks[0], true) } PopupState::CommandHelp { .. } => { @@ -161,12 +168,13 @@ fn render_list_popup( .constraints([Constraint::Min(0), Constraint::Length(length)].as_ref()) .split(rect); - let (list, len) = utils::construct_list_widget(state, &ui.theme, items, title, true, None); + let rect = construct_and_render_block(title, &ui.theme, state, Borders::ALL, frame, chunks[1]); + let (list, len) = utils::construct_list_widget(state, &ui.theme, items, true); utils::render_list_window( frame, list, - chunks[1], + rect, len, ui.popup.as_mut().unwrap().list_state_mut().unwrap(), ); @@ -211,6 +219,15 @@ pub fn render_shortcut_help_popup( .constraints([Constraint::Min(0), Constraint::Length(7)].as_ref()) .split(rect); + let rect = construct_and_render_block( + "Shortcuts", + &ui.theme, + state, + Borders::ALL, + frame, + chunks[1], + ); + let help_table = Table::new( matches .into_iter() @@ -220,9 +237,9 @@ pub fn render_shortcut_help_popup( .map(|c| Row::new(c.iter().map(|i| Cell::from(i.to_owned())))) .collect::>(), ) - .widths(&SHORTCUT_TABLE_CONSTRAINS) - .block(construct_block("Shortcuts", &ui.theme, state, None)); - frame.render_widget(help_table, chunks[1]); + .widths(&SHORTCUT_TABLE_CONSTRAINS); + + frame.render_widget(help_table, rect); chunks[0] } } @@ -259,6 +276,9 @@ pub fn render_commands_help_popup( if *scroll_offset >= map.len() { *scroll_offset = map.len() - 1 } + + let rect = construct_and_render_block("Commands", &ui.theme, state, Borders::ALL, frame, rect); + let help_table = Table::new( map.into_iter() .skip(*scroll_offset) @@ -279,8 +299,8 @@ pub fn render_commands_help_popup( ]) .style(ui.theme.table_header()), ) - .widths(&COMMAND_TABLE_CONSTRAINTS) - .block(construct_block("Commands", &ui.theme, state, None)); + .widths(&COMMAND_TABLE_CONSTRAINTS); + frame.render_widget(help_table, rect); } @@ -306,6 +326,8 @@ pub fn render_queue_popup( _ => return, }; + let rect = construct_and_render_block("Queue", &ui.theme, state, Borders::ALL, frame, rect); + // Minimize the time we have a lock on the player state let queue_table = { let player_state = state.player.read(); @@ -333,7 +355,7 @@ pub fn render_queue_popup( ) .header(Row::new(vec![Cell::from("#"), Cell::from("Title")]).style(ui.theme.table_header())) .widths(&[Constraint::Percentage(10), Constraint::Percentage(90)]) - .block(construct_block("Queue", &ui.theme, state, None)) }; + frame.render_widget(queue_table, rect); } From a6a60c811b83bdda50d6757b8532d4264ef96f64 Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Fri, 3 Mar 2023 17:35:26 -0500 Subject: [PATCH 08/13] fix compile errors --- spotify_player/src/ui/page.rs | 38 ++++++++++++++++++++-------------- spotify_player/src/ui/popup.rs | 19 ++++++++--------- spotify_player/src/ui/utils.rs | 2 +- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/spotify_player/src/ui/page.rs b/spotify_player/src/ui/page.rs index 71843980..e3d7b147 100644 --- a/spotify_player/src/ui/page.rs +++ b/spotify_player/src/ui/page.rs @@ -58,20 +58,26 @@ pub fn render_search_page( state, Borders::TOP | Borders::RIGHT, frame, - rect, + chunks[0], ); let album_rect = - construct_and_render_block("Albums", &ui.theme, state, Borders::TOP, frame, rect); + construct_and_render_block("Albums", &ui.theme, state, Borders::TOP, frame, chunks[1]); let artist_rect = construct_and_render_block( "Artists", &ui.theme, state, Borders::TOP | Borders::RIGHT, frame, - rect, + chunks[2], + ); + let playlist_rect = construct_and_render_block( + "Playlists", + &ui.theme, + state, + Borders::TOP, + frame, + chunks[3], ); - let playlist_rect = - construct_and_render_block("Playlists", &ui.theme, state, Borders::TOP, frame, rect); // 3. Construct the page's widgets let (track_list, n_tracks) = { @@ -141,7 +147,7 @@ pub fn render_search_page( ui.theme .selection_style(is_active && focus_state == SearchFocusState::Input), ), - chunks[0], + search_input_rect, ); // Render the search result windows. @@ -635,6 +641,16 @@ fn render_artist_context_page_windows( }; // 4. Render the widgets + render_track_table_window( + frame, + top_tracks_rect, + is_active && focus_state == ArtistFocusState::TopTracks, + state, + tracks, + ui, + data, + )?; + let (album_list_state, artist_list_state) = match ui.current_page_mut() { PageState::Context { state: @@ -648,16 +664,6 @@ fn render_artist_context_page_windows( s => anyhow::bail!("expect an artist context page state, found {s:?}"), }; - render_track_table_window( - frame, - top_tracks_rect, - is_active && focus_state == ArtistFocusState::TopTracks, - state, - tracks, - ui, - data, - )?; - utils::render_list_window(frame, album_list, albums_rect, n_albums, album_list_state); utils::render_list_window( frame, diff --git a/spotify_player/src/ui/popup.rs b/spotify_player/src/ui/popup.rs index ed02d155..f66930ee 100644 --- a/spotify_player/src/ui/popup.rs +++ b/spotify_player/src/ui/popup.rs @@ -251,12 +251,7 @@ pub fn render_commands_help_popup( ui: &mut UIStateGuard, rect: Rect, ) { - let scroll_offset = match ui.popup { - Some(PopupState::CommandHelp { - ref mut scroll_offset, - }) => scroll_offset, - _ => return, - }; + let rect = construct_and_render_block("Commands", &ui.theme, state, Borders::ALL, frame, rect); let mut map = BTreeMap::new(); state.keymap_config.keymaps.iter().for_each(|km| { @@ -272,13 +267,17 @@ pub fn render_commands_help_popup( } }); + let scroll_offset = match ui.popup { + Some(PopupState::CommandHelp { + ref mut scroll_offset, + }) => scroll_offset, + _ => return, + }; // offset should not be greater than or equal the number of available commands if *scroll_offset >= map.len() { *scroll_offset = map.len() - 1 } - let rect = construct_and_render_block("Commands", &ui.theme, state, Borders::ALL, frame, rect); - let help_table = Table::new( map.into_iter() .skip(*scroll_offset) @@ -319,6 +318,8 @@ pub fn render_queue_popup( } } + let rect = construct_and_render_block("Queue", &ui.theme, state, Borders::ALL, frame, rect); + let scroll_offset = match ui.popup { Some(PopupState::Queue { ref mut scroll_offset, @@ -326,8 +327,6 @@ pub fn render_queue_popup( _ => return, }; - let rect = construct_and_render_block("Queue", &ui.theme, state, Borders::ALL, frame, rect); - // Minimize the time we have a lock on the player state let queue_table = { let player_state = state.player.read(); diff --git a/spotify_player/src/ui/utils.rs b/spotify_player/src/ui/utils.rs index f8b45adf..3734fd8d 100644 --- a/spotify_player/src/ui/utils.rs +++ b/spotify_player/src/ui/utils.rs @@ -106,6 +106,6 @@ pub fn render_loading_window( rect: Rect, title: &str, ) { - let rect = construct_and_render_block(title, theme, state, None, frame, rect); + let rect = construct_and_render_block(title, theme, state, Borders::ALL, frame, rect); frame.render_widget(Paragraph::new("Loading..."), rect); } From 245c8b8af713486da0eb9bcb4f4fef278623b7cf Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Fri, 3 Mar 2023 18:15:17 -0500 Subject: [PATCH 09/13] clean up --- spotify_player/src/ui/page.rs | 37 +++++++++++++++++++++------------- spotify_player/src/ui/utils.rs | 5 ++++- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/spotify_player/src/ui/page.rs b/spotify_player/src/ui/page.rs index e3d7b147..06ecaa55 100644 --- a/spotify_player/src/ui/page.rs +++ b/spotify_player/src/ui/page.rs @@ -346,10 +346,10 @@ pub fn render_library_page( state, Borders::TOP | Borders::LEFT | Borders::BOTTOM, frame, - chunks[0], + chunks[1], ); let artist_rect = - construct_and_render_block("Artists", &ui.theme, state, Borders::ALL, frame, chunks[0]); + construct_and_render_block("Artists", &ui.theme, state, Borders::ALL, frame, chunks[2]); // 3. Construct the page's widgets // Construct the playlist window @@ -504,6 +504,14 @@ pub fn render_lyric_page( // 1. Get the data let data = state.data.read(); + // 2. Construct the app's layout + let rect = construct_and_render_block("Lyric", &ui.theme, state, Borders::ALL, frame, rect); + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(0)].as_ref()) + .split(rect); + + // 3. Construct the app's widgets let (track, artists, scroll_offset) = match ui.current_page_mut() { PageState::Lyric { track, @@ -513,14 +521,6 @@ pub fn render_lyric_page( s => anyhow::bail!("expect a lyric page state, found {s:?}"), }; - // 2. Construct the app's layout - let rect = construct_and_render_block("Lyric", &ui.theme, state, Borders::ALL, frame, rect); - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Min(0)].as_ref()) - .split(rect); - - // 3. Construct the app's widgets let (desc, lyric) = match data.caches.lyrics.peek(&format!("{track} {artists}")) { None => { frame.render_widget(Paragraph::new("Loading..."), rect); @@ -546,7 +546,10 @@ pub fn render_lyric_page( // 4. Render the app's widgets // render lyric page description text - frame.render_widget(Paragraph::new(desc), chunks[0]); + frame.render_widget( + Paragraph::new(Text::styled(desc, ui.theme.page_desc())), + chunks[0], + ); // render lyric text frame.render_widget( @@ -598,13 +601,19 @@ fn render_artist_context_page_windows( .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(chunks[1]); - let albums_rect = - construct_and_render_block("Albums", &ui.theme, state, Borders::TOP, frame, chunks[0]); + let albums_rect = construct_and_render_block( + "Albums", + &ui.theme, + state, + Borders::TOP | Borders::RIGHT, + frame, + chunks[0], + ); let related_artists_rect = construct_and_render_block( "Related Artists", &ui.theme, state, - Borders::TOP | Borders::LEFT, + Borders::TOP, frame, chunks[1], ); diff --git a/spotify_player/src/ui/utils.rs b/spotify_player/src/ui/utils.rs index 3734fd8d..83402fb7 100644 --- a/spotify_player/src/ui/utils.rs +++ b/spotify_player/src/ui/utils.rs @@ -29,7 +29,10 @@ pub fn construct_and_render_block( frame.render_widget(block, rect); // margin to separate the block with its inner widget(s) - Layout::default().margin(1).split(rect)[0] + Layout::default() + .margin(1) + .constraints([Constraint::Min(0)]) + .split(rect)[0] } /// constructs a generic list widget From 5d1a91fa26fb39db93eb12796a607c1d71714c80 Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Fri, 3 Mar 2023 21:49:57 -0500 Subject: [PATCH 10/13] fix clippy errors --- spotify_player/src/ui/page.rs | 27 +++++++-------------------- spotify_player/src/ui/popup.rs | 2 +- spotify_player/src/ui/utils.rs | 1 - 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/spotify_player/src/ui/page.rs b/spotify_player/src/ui/page.rs index 06ecaa55..3d671a7b 100644 --- a/spotify_player/src/ui/page.rs +++ b/spotify_player/src/ui/page.rs @@ -92,7 +92,7 @@ pub fn render_search_page( let is_active = is_active && focus_state == SearchFocusState::Tracks; - utils::construct_list_widget(state, &ui.theme, track_items, is_active) + utils::construct_list_widget(&ui.theme, track_items, is_active) }; let (album_list, n_albums) = { @@ -107,7 +107,7 @@ pub fn render_search_page( let is_active = is_active && focus_state == SearchFocusState::Albums; - utils::construct_list_widget(state, &ui.theme, album_items, is_active) + utils::construct_list_widget(&ui.theme, album_items, is_active) }; let (artist_list, n_artists) = { @@ -122,7 +122,7 @@ pub fn render_search_page( let is_active = is_active && focus_state == SearchFocusState::Artists; - utils::construct_list_widget(state, &ui.theme, artist_items, is_active) + utils::construct_list_widget(&ui.theme, artist_items, is_active) }; let (playlist_list, n_playlists) = { @@ -137,7 +137,7 @@ pub fn render_search_page( let is_active = is_active && focus_state == SearchFocusState::Playlists; - utils::construct_list_widget(state, &ui.theme, playlist_items, is_active) + utils::construct_list_widget(&ui.theme, playlist_items, is_active) }; // 4. Render the page's widgets @@ -354,7 +354,6 @@ pub fn render_library_page( // 3. Construct the page's widgets // Construct the playlist window let (playlist_list, n_playlists) = utils::construct_list_widget( - state, &ui.theme, ui.search_filtered_items(&data.user_data.playlists) .into_iter() @@ -364,7 +363,6 @@ pub fn render_library_page( ); // Construct the saved album window let (album_list, n_albums) = utils::construct_list_widget( - state, &ui.theme, ui.search_filtered_items(&data.user_data.saved_albums) .into_iter() @@ -374,7 +372,6 @@ pub fn render_library_page( ); // Construct the followed artist window let (artist_list, n_artists) = utils::construct_list_widget( - state, &ui.theme, ui.search_filtered_items(&data.user_data.followed_artists) .into_iter() @@ -433,14 +430,13 @@ pub fn render_browse_page( rect = construct_and_render_block( "Categories", &ui.theme, - &state, + state, Borders::ALL, frame, rect, ); utils::construct_list_widget( - state, &ui.theme, ui.search_filtered_items(&data.browse.categories) .into_iter() @@ -459,17 +455,10 @@ pub fn render_browse_page( } }; - rect = construct_and_render_block( - &title, - &ui.theme, - &state, - Borders::ALL, - frame, - rect, - ); + rect = + construct_and_render_block(&title, &ui.theme, state, Borders::ALL, frame, rect); utils::construct_list_widget( - state, &ui.theme, ui.search_filtered_items(playlists) .into_iter() @@ -627,7 +616,6 @@ fn render_artist_context_page_windows( .collect::>(); utils::construct_list_widget( - state, &ui.theme, album_items, is_active && focus_state == ArtistFocusState::Albums, @@ -642,7 +630,6 @@ fn render_artist_context_page_windows( .collect::>(); utils::construct_list_widget( - state, &ui.theme, artist_items, is_active && focus_state == ArtistFocusState::RelatedArtists, diff --git a/spotify_player/src/ui/popup.rs b/spotify_player/src/ui/popup.rs index f66930ee..7b42f0ef 100644 --- a/spotify_player/src/ui/popup.rs +++ b/spotify_player/src/ui/popup.rs @@ -169,7 +169,7 @@ fn render_list_popup( .split(rect); let rect = construct_and_render_block(title, &ui.theme, state, Borders::ALL, frame, chunks[1]); - let (list, len) = utils::construct_list_widget(state, &ui.theme, items, true); + let (list, len) = utils::construct_list_widget(&ui.theme, items, true); utils::render_list_window( frame, diff --git a/spotify_player/src/ui/utils.rs b/spotify_player/src/ui/utils.rs index 83402fb7..acd8c65c 100644 --- a/spotify_player/src/ui/utils.rs +++ b/spotify_player/src/ui/utils.rs @@ -37,7 +37,6 @@ pub fn construct_and_render_block( /// constructs a generic list widget pub fn construct_list_widget<'a>( - state: &SharedState, theme: &config::Theme, items: Vec<(String, bool)>, is_active: bool, From 600cc5d320445317169abb705922146bde71e056 Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Fri, 3 Mar 2023 22:22:55 -0500 Subject: [PATCH 11/13] rename `BorderType::None` to `BorderType::Hidden` --- spotify_player/src/config/mod.rs | 4 ++-- spotify_player/src/ui/utils.rs | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/spotify_player/src/config/mod.rs b/spotify_player/src/config/mod.rs index 344d6e61..465a1464 100644 --- a/spotify_player/src/config/mod.rs +++ b/spotify_player/src/config/mod.rs @@ -77,9 +77,9 @@ pub enum Position { } config_parser_impl!(Position); -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] pub enum BorderType { - None, + Hidden, Plain, Rounded, Double, diff --git a/spotify_player/src/ui/utils.rs b/spotify_player/src/ui/utils.rs index acd8c65c..4f3a553c 100644 --- a/spotify_player/src/ui/utils.rs +++ b/spotify_player/src/ui/utils.rs @@ -13,26 +13,28 @@ pub fn construct_and_render_block( rect: Rect, ) -> Rect { let (borders, border_type) = match state.app_config.border_type { - config::BorderType::None => (Borders::NONE, BorderType::Plain), + config::BorderType::Hidden => (borders, BorderType::Plain), config::BorderType::Plain => (borders, BorderType::Plain), config::BorderType::Rounded => (borders, BorderType::Rounded), config::BorderType::Double => (borders, BorderType::Double), config::BorderType::Thick => (borders, BorderType::Thick), }; - let block = Block::default() + let mut block = Block::default() .title(theme.block_title_with_style(title)) .borders(borders) .border_style(theme.border()) .border_type(border_type); - frame.render_widget(block, rect); + let inner_rect = block.inner(rect); + + // Handle `BorderType::Hidden` after determining the inner rectangle + if state.app_config.border_type == config::BorderType::Hidden { + block = block.borders(Borders::NONE); + } - // margin to separate the block with its inner widget(s) - Layout::default() - .margin(1) - .constraints([Constraint::Min(0)]) - .split(rect)[0] + frame.render_widget(block, rect); + inner_rect } /// constructs a generic list widget From 82c68b47bff9cc4cb1fd25ac341008be9162bd1c Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Fri, 3 Mar 2023 22:35:23 -0500 Subject: [PATCH 12/13] add `progress_bar_type` config option --- spotify_player/src/config/mod.rs | 9 ++++++ spotify_player/src/ui/playback.rs | 46 +++++++++++++++++++++---------- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/spotify_player/src/config/mod.rs b/spotify_player/src/config/mod.rs index 465a1464..9c9b106f 100644 --- a/spotify_player/src/config/mod.rs +++ b/spotify_player/src/config/mod.rs @@ -50,6 +50,7 @@ pub struct AppConfig { // layout configs pub border_type: BorderType, + pub progress_bar_type: ProgressBarType, pub playback_window_position: Position, @@ -87,6 +88,13 @@ pub enum BorderType { } config_parser_impl!(BorderType); +#[derive(Debug, Deserialize, Clone)] +pub enum ProgressBarType { + Line, + Rectangle, +} +config_parser_impl!(ProgressBarType); + #[derive(Debug, Deserialize, ConfigParse, Clone)] pub struct Command { pub command: String, @@ -157,6 +165,7 @@ impl Default for AppConfig { liked_icon: "♥".to_string(), border_type: BorderType::Plain, + progress_bar_type: ProgressBarType::Rectangle, playback_window_position: Position::Top, diff --git a/spotify_player/src/ui/playback.rs b/spotify_player/src/ui/playback.rs index 271476e8..45cd81b5 100644 --- a/spotify_player/src/ui/playback.rs +++ b/spotify_player/src/ui/playback.rs @@ -98,7 +98,7 @@ pub fn render_playback_window( .context("playback should exist")?, track.duration, ); - render_playback_progress_bar(frame, ui, progress, track, progress_bar_rect); + render_playback_progress_bar(frame, state, ui, progress, track, progress_bar_rect); } else { tracing::warn!("Got a non-track playable item: {:?}", playback.item); } @@ -205,27 +205,45 @@ fn render_playback_text( fn render_playback_progress_bar( frame: &mut Frame, + state: &SharedState, ui: &mut UIStateGuard, progress: std::time::Duration, track: &rspotify_model::FullTrack, rect: Rect, ) { - let progress_bar = Gauge::default() - .gauge_style(ui.theme.playback_progress_bar()) - .ratio(progress.as_secs_f64() / track.duration.as_secs_f64()) - .label(Span::styled( - format!( - "{}/{}", - crate::utils::format_duration(progress), - crate::utils::format_duration(track.duration), - ), - Style::default().add_modifier(Modifier::BOLD), - )); + match state.app_config.progress_bar_type { + config::ProgressBarType::Line => frame.render_widget( + LineGauge::default() + .gauge_style(ui.theme.playback_progress_bar()) + .ratio(progress.as_secs_f64() / track.duration.as_secs_f64()) + .label(Span::styled( + format!( + "{}/{}", + crate::utils::format_duration(progress), + crate::utils::format_duration(track.duration), + ), + Style::default().add_modifier(Modifier::BOLD), + )), + rect, + ), + config::ProgressBarType::Rectangle => frame.render_widget( + Gauge::default() + .gauge_style(ui.theme.playback_progress_bar()) + .ratio(progress.as_secs_f64() / track.duration.as_secs_f64()) + .label(Span::styled( + format!( + "{}/{}", + crate::utils::format_duration(progress), + crate::utils::format_duration(track.duration), + ), + Style::default().add_modifier(Modifier::BOLD), + )), + rect, + ), + } // update the progress bar's position stored inside the UI state ui.playback_progress_bar_rect = rect; - - frame.render_widget(progress_bar, rect); } #[cfg(feature = "image")] From bd850b259799550806a4ef63f31d5e1ab5b8df26 Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Fri, 3 Mar 2023 22:37:58 -0500 Subject: [PATCH 13/13] add document for new config options --- docs/config.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/config.md b/docs/config.md index c8b3b420..b528a471 100644 --- a/docs/config.md +++ b/docs/config.md @@ -36,6 +36,8 @@ All configuration files should be placed inside the application's configuration | `play_icon` | the icon to indicate playing state of a Spotify item | `▶` | | `pause_icon` | the icon to indicate pause state of a Spotify item | `▌▌` | | `liked_icon` | the icon to indicate the liked state of a song | `♥` | +| `border_type` | the type of the application's borders | `Plain` | +| `progress_bar_type` | the type of the playback progress bar | `Rectangle` | | `playback_window_position` | the position of the playback window | `Top` | | `playback_window_width` | the width of the playback window | `6` | | `cover_img_width` | the width of the cover image (`image` feature only) | `5` | @@ -67,6 +69,8 @@ The default `app.toml` can be found in the example [`app.toml`](../examples/app. - An example of event that triggers a playback update is the one happening when the current track ends. - `copy_command` is represented by a struct with two fields `command` and `args`. For example, `copy_command = { command = "xclip", args = ["-sel", "c"] }`. The copy command should read input from **standard input**. - `playback_window_position` can only be either `Top` or `Bottom`. +- `border_type` can be either `Hidden`, `Plain`, `Rounded`, `Double`, `Thick`. +- `progress_bar_type` can be either `Rectangle` or `Line`. #### Media control