From bf95239532388133ff3f4d1bee0db28b99d99e7d Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 21 May 2023 20:02:30 +0200 Subject: [PATCH 1/3] Add overlay to the UI --- src/tui/handler.rs | 4 + src/tui/ui.rs | 185 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 152 insertions(+), 37 deletions(-) diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 5da457f..657c226 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -67,6 +67,10 @@ impl IEventHandlerPrivate for EventHandler { KeyCode::Down => { ui.increment_selection(); } + // Toggle overlay. + KeyCode::Char('m') | KeyCode::Char('M') => { + ui.toggle_overlay(); + } // Other keys. _ => {} } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 2827147..f996ec9 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -1,11 +1,11 @@ use std::fmt; -use musichoard::{Album, Artist, Collection, Format, Track}; +use musichoard::{Album, Artist, Collection, Format, IUrl, Track}; use ratatui::{ backend::Backend, layout::{Alignment, Rect}, style::{Color, Style}, - widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph}, + widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph}, Frame, }; @@ -42,6 +42,7 @@ pub trait IUi { fn increment_selection(&mut self); fn decrement_selection(&mut self); + fn toggle_overlay(&mut self); fn render(&mut self, frame: &mut Frame<'_, B>); } @@ -275,6 +276,7 @@ impl Selection { pub struct Ui { music_hoard: MH, selection: Selection, + overlay: bool, running: bool, } @@ -299,7 +301,7 @@ struct FrameArea { } impl FrameArea { - fn new(frame: Rect) -> FrameArea { + fn new(frame: Rect) -> Self { let width_one_third = frame.width / 3; let height_one_third = frame.height / 3; @@ -357,6 +359,28 @@ impl FrameArea { } } +struct OverlayArea { + artist: Rect, +} + +impl OverlayArea { + fn new(frame: Rect) -> Self { + let margin_factor = 8; + + let width_margin = frame.width / margin_factor; + let height_margin = frame.height / margin_factor; + + let artist = Rect { + x: width_margin, + y: height_margin, + width: frame.width - (2 * width_margin), + height: frame.height - (2 * height_margin), + }; + + OverlayArea { artist } + } +} + struct ArtistState<'a, 'b> { active: bool, list: List<'a>, @@ -380,6 +404,56 @@ impl<'a, 'b> ArtistState<'a, 'b> { } } +struct ArtistOverlay<'a> { + properties: Paragraph<'a>, +} + +impl<'a> ArtistOverlay<'a> { + fn opt_opt_to_str(opt: Option>) -> &str { + opt.flatten().map(|item| item.url()).unwrap_or("") + } + + fn opt_vec_to_string(opt_vec: Option<&Vec>, indent: &str) -> String { + match opt_vec { + Some(vec) => { + if vec.len() < 2 { + vec.get(0).map(|item| item.url()).unwrap_or("").to_string() + } else { + let indent = format!("\n{indent}"); + let list = vec + .iter() + .map(|item| item.url()) + .collect::>() + .join(&indent); + format!("{indent}{list}") + } + } + None => String::from(""), + } + } + + fn new(artists: &'a [Artist], state: &ListState) -> ArtistOverlay<'a> { + let artist = state.selected().map(|i| &artists[i]); + + let item_indent = " "; + let list_indent = " - "; + let properties = Paragraph::new(format!( + "Artist: {}\n\n{item_indent}\ + MusicBrainz: {}\n{item_indent}\ + MusicButler: {}\n{item_indent}\ + Bandcamp: {}\n{item_indent}\ + Qobuz: {}", + artist.map(|a| a.id.name.as_str()).unwrap_or(""), + Self::opt_opt_to_str(artist.map(|a| a.properties.musicbrainz.as_ref())), + Self::opt_vec_to_string(artist.map(|a| &a.properties.musicbutler), list_indent), + Self::opt_vec_to_string(artist.map(|a| &a.properties.bandcamp), list_indent), + Self::opt_opt_to_str(artist.map(|a| a.properties.qobuz.as_ref())), + )); + + ArtistOverlay { properties } + } +} + struct AlbumState<'a, 'b> { active: bool, list: List<'a>, @@ -469,6 +543,7 @@ impl Ui { Ok(Ui { music_hoard, selection, + overlay: false, running: true, }) } @@ -531,6 +606,21 @@ impl Ui { ); } + fn render_overlay_widget( + title: &str, + paragraph: Paragraph, + area: Rect, + frame: &mut Frame<'_, B>, + ) { + frame.render_widget(Clear, area); + frame.render_widget( + paragraph + .style(Self::style(true)) + .block(Self::block(title, true)), + area, + ); + } + fn render_artist_column(st: ArtistState, ar: ArtistArea, fr: &mut Frame<'_, B>) { Self::render_list_widget("Artists", st.list, st.state, st.active, ar.list, fr); } @@ -544,41 +634,8 @@ impl Ui { Self::render_list_widget("Tracks", st.list, st.state, st.active, ar.list, fr); Self::render_info_widget("Track info", st.info, st.active, ar.info, fr); } -} -impl IUi for Ui { - fn is_running(&self) -> bool { - self.running - } - - fn quit(&mut self) { - self.running = false; - } - - fn save(&mut self) -> Result<(), UiError> { - self.music_hoard.save_to_database()?; - Ok(()) - } - - fn increment_category(&mut self) { - self.selection.increment_category(); - } - - fn decrement_category(&mut self) { - self.selection.decrement_category(); - } - - fn increment_selection(&mut self) { - self.selection - .increment_selection(self.music_hoard.get_collection()); - } - - fn decrement_selection(&mut self) { - self.selection - .decrement_selection(self.music_hoard.get_collection()); - } - - fn render(&mut self, frame: &mut Frame<'_, B>) { + fn render_collection(&mut self, frame: &mut Frame<'_, B>) { let active = self.selection.active; let areas = FrameArea::new(frame.size()); @@ -622,6 +679,60 @@ impl IUi for Ui { Self::render_track_column(track_state, areas.track, frame); } + + fn render_overlay(&mut self, frame: &mut Frame<'_, B>) { + let areas = OverlayArea::new(frame.size()); + + let artists = self.music_hoard.get_collection(); + let artist_selection = &mut self.selection.artist; + + let artist_overlay = ArtistOverlay::new(&artists, &artist_selection.state); + Self::render_overlay_widget("Artist", artist_overlay.properties, areas.artist, frame); + } +} + +impl IUi for Ui { + fn is_running(&self) -> bool { + self.running + } + + fn quit(&mut self) { + self.running = false; + } + + fn save(&mut self) -> Result<(), UiError> { + self.music_hoard.save_to_database()?; + Ok(()) + } + + fn increment_category(&mut self) { + self.selection.increment_category(); + } + + fn decrement_category(&mut self) { + self.selection.decrement_category(); + } + + fn increment_selection(&mut self) { + self.selection + .increment_selection(self.music_hoard.get_collection()); + } + + fn decrement_selection(&mut self) { + self.selection + .decrement_selection(self.music_hoard.get_collection()); + } + + fn toggle_overlay(&mut self) { + self.overlay = !self.overlay; + } + + fn render(&mut self, frame: &mut Frame<'_, B>) { + self.render_collection(frame); + if self.overlay { + self.render_overlay(frame); + } + } } #[cfg(test)] -- 2.45.2 From e23c99ed628eea73d6da35a26be06ca17e5acfc5 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 21 May 2023 22:47:36 +0200 Subject: [PATCH 2/3] Unit test --- src/tui/ui.rs | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/tui/ui.rs b/src/tui/ui.rs index f996ec9..3c8d26b 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -414,8 +414,8 @@ impl<'a> ArtistOverlay<'a> { } fn opt_vec_to_string(opt_vec: Option<&Vec>, indent: &str) -> String { - match opt_vec { - Some(vec) => { + opt_vec + .map(|vec| { if vec.len() < 2 { vec.get(0).map(|item| item.url()).unwrap_or("").to_string() } else { @@ -427,9 +427,8 @@ impl<'a> ArtistOverlay<'a> { .join(&indent); format!("{indent}{list}") } - } - None => String::from(""), - } + }) + .unwrap_or_else(|| String::from("")) } fn new(artists: &'a [Artist], state: &ListState) -> ArtistOverlay<'a> { @@ -686,7 +685,7 @@ impl Ui { let artists = self.music_hoard.get_collection(); let artist_selection = &mut self.selection.artist; - let artist_overlay = ArtistOverlay::new(&artists, &artist_selection.state); + let artist_overlay = ArtistOverlay::new(artists, &artist_selection.state); Self::render_overlay_widget("Artist", artist_overlay.properties, areas.artist, frame); } } @@ -1171,6 +1170,27 @@ mod tests { terminal.draw(|frame| ui.render(frame)).unwrap(); } + #[test] + fn overlay() { + let mut terminal = terminal(); + let mut ui = ui(COLLECTION.to_owned()); + + terminal.draw(|frame| ui.render(frame)).unwrap(); + + ui.toggle_overlay(); + + terminal.draw(|frame| ui.render(frame)).unwrap(); + + // Change the artist (which has a multi-link entry). + ui.increment_selection(); + + terminal.draw(|frame| ui.render(frame)).unwrap(); + + ui.toggle_overlay(); + + terminal.draw(|frame| ui.render(frame)).unwrap(); + } + #[test] fn errors() { let ui_err: UiError = musichoard::Error::DatabaseError(String::from("get rekt")).into(); -- 2.45.2 From 729272a3e307174bab98b23d9f52ef3589545556 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 21 May 2023 22:47:39 +0200 Subject: [PATCH 3/3] Update readme about text selection --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 3535a34..acf805b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # Music Hoard +## Usage notes + +### Text selection + +To select and copy text use the terminal-specific modifier key (on Linux this is usually the Shift key). + ## Code Coverage ### Pre-requisites -- 2.45.2