Add overlay to the UI

This commit is contained in:
Wojciech Kozlowski 2023-05-21 20:02:30 +02:00
parent fd775372cd
commit 3576d8a12c
2 changed files with 152 additions and 37 deletions

View File

@ -67,6 +67,10 @@ impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
KeyCode::Down => { KeyCode::Down => {
ui.increment_selection(); ui.increment_selection();
} }
// Toggle overlay.
KeyCode::Char('m') | KeyCode::Char('M') => {
ui.toggle_overlay();
}
// Other keys. // Other keys.
_ => {} _ => {}
} }

View File

@ -1,11 +1,11 @@
use std::fmt; use std::fmt;
use musichoard::{Album, Artist, Collection, Format, Track}; use musichoard::{Album, Artist, Collection, Format, IUrl, Track};
use ratatui::{ use ratatui::{
backend::Backend, backend::Backend,
layout::{Alignment, Rect}, layout::{Alignment, Rect},
style::{Color, Style}, style::{Color, Style},
widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph}, widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph},
Frame, Frame,
}; };
@ -42,6 +42,7 @@ pub trait IUi {
fn increment_selection(&mut self); fn increment_selection(&mut self);
fn decrement_selection(&mut self); fn decrement_selection(&mut self);
fn toggle_overlay(&mut self);
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>); fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>);
} }
@ -275,6 +276,7 @@ impl Selection {
pub struct Ui<MH> { pub struct Ui<MH> {
music_hoard: MH, music_hoard: MH,
selection: Selection, selection: Selection,
overlay: bool,
running: bool, running: bool,
} }
@ -299,7 +301,7 @@ struct FrameArea {
} }
impl FrameArea { impl FrameArea {
fn new(frame: Rect) -> FrameArea { fn new(frame: Rect) -> Self {
let width_one_third = frame.width / 3; let width_one_third = frame.width / 3;
let height_one_third = frame.height / 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> { struct ArtistState<'a, 'b> {
active: bool, active: bool,
list: List<'a>, 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<U: IUrl>(opt: Option<Option<&U>>) -> &str {
opt.flatten().map(|item| item.url()).unwrap_or("")
}
fn opt_vec_to_string<U: IUrl>(opt_vec: Option<&Vec<U>>, 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::<Vec<&str>>()
.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> { struct AlbumState<'a, 'b> {
active: bool, active: bool,
list: List<'a>, list: List<'a>,
@ -469,6 +543,7 @@ impl<MH: IMusicHoard> Ui<MH> {
Ok(Ui { Ok(Ui {
music_hoard, music_hoard,
selection, selection,
overlay: false,
running: true, running: true,
}) })
} }
@ -531,6 +606,21 @@ impl<MH: IMusicHoard> Ui<MH> {
); );
} }
fn render_overlay_widget<B: Backend>(
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<B: Backend>(st: ArtistState, ar: ArtistArea, fr: &mut Frame<'_, B>) { fn render_artist_column<B: Backend>(st: ArtistState, ar: ArtistArea, fr: &mut Frame<'_, B>) {
Self::render_list_widget("Artists", st.list, st.state, st.active, ar.list, fr); Self::render_list_widget("Artists", st.list, st.state, st.active, ar.list, fr);
} }
@ -544,41 +634,8 @@ impl<MH: IMusicHoard> Ui<MH> {
Self::render_list_widget("Tracks", st.list, st.state, st.active, ar.list, fr); 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); Self::render_info_widget("Track info", st.info, st.active, ar.info, fr);
} }
}
impl<MH: IMusicHoard> IUi for Ui<MH> { fn render_collection<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
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<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
let active = self.selection.active; let active = self.selection.active;
let areas = FrameArea::new(frame.size()); let areas = FrameArea::new(frame.size());
@ -622,6 +679,60 @@ impl<MH: IMusicHoard> IUi for Ui<MH> {
Self::render_track_column(track_state, areas.track, frame); Self::render_track_column(track_state, areas.track, frame);
} }
fn render_overlay<B: Backend>(&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<MH: IMusicHoard> IUi for Ui<MH> {
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<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
self.render_collection(frame);
if self.overlay {
self.render_overlay(frame);
}
}
} }
#[cfg(test)] #[cfg(test)]