From 6a18c5d9cc8476728f72ae500351623ea4abb7ef Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 10 Feb 2024 23:25:59 +0100 Subject: [PATCH] Add a minibuffer (#131) Closes #125 Reviewed-on: https://git.thenineworlds.net/wojtek/musichoard/pulls/131 --- src/tui/app/app.rs | 4 +- src/tui/handler.rs | 18 +++-- src/tui/ui.rs | 186 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 179 insertions(+), 29 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index b6857ef..114abca 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -87,10 +87,12 @@ pub trait IAppAccess { fn get(&mut self) -> AppPublic; } +pub type AppPublicState = AppState<(), (), (), String, String>; + pub struct AppPublic<'app> { pub collection: &'app Collection, pub selection: &'app mut Selection, - pub state: &'app AppState<(), (), (), String, String>, + pub state: &'app AppPublicState, } pub struct App { diff --git a/src/tui/handler.rs b/src/tui/handler.rs index a0352c6..322206e 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -84,7 +84,7 @@ impl IEventHandlerPrivate for EventHandler { fn handle_browse_key_event(app: &mut ::BS, key_event: KeyEvent) { match key_event.code { // Exit application on `ESC` or `q`. - KeyCode::Esc | KeyCode::Char('q') => { + KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => { app.save(); app.quit(); } @@ -108,9 +108,11 @@ impl IEventHandlerPrivate for EventHandler { fn handle_info_key_event(app: &mut ::IS, key_event: KeyEvent) { match key_event.code { // Toggle overlay. - KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('m') | KeyCode::Char('M') => { - app.hide_info_overlay() - } + KeyCode::Esc + | KeyCode::Char('q') + | KeyCode::Char('Q') + | KeyCode::Char('m') + | KeyCode::Char('M') => app.hide_info_overlay(), // Othey keys. _ => {} } @@ -122,9 +124,11 @@ impl IEventHandlerPrivate for EventHandler { KeyCode::Char('l') | KeyCode::Char('L') => app.reload_library(), KeyCode::Char('d') | KeyCode::Char('D') => app.reload_database(), // Return. - KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('g') | KeyCode::Char('G') => { - app.hide_reload_menu() - } + KeyCode::Esc + | KeyCode::Char('q') + | KeyCode::Char('Q') + | KeyCode::Char('g') + | KeyCode::Char('G') => app.hide_reload_menu(), // Othey keys. _ => {} } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 2aa2d78..e681e08 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -15,7 +15,7 @@ use ratatui::{ }; use crate::tui::app::{ - app::{AppState, IAppAccess}, + app::{AppPublicState, AppState, IAppAccess}, selection::{Category, Selection, WidgetState}, }; @@ -41,23 +41,27 @@ struct FrameArea { artist: ArtistArea, album: AlbumArea, track: TrackArea, + minibuffer: Rect, } impl FrameArea { fn new(frame: Rect) -> Self { + let minibuffer_height = 3; + let buffer_height = frame.height.saturating_sub(minibuffer_height); + let width_one_third = frame.width / 3; - let height_one_third = frame.height / 3; + let height_one_third = buffer_height / 3; let panel_width = width_one_third; - let panel_width_last = frame.width - 2 * panel_width; - let panel_height_top = frame.height - height_one_third; + let panel_width_last = frame.width.saturating_sub(2 * panel_width); + let panel_height_top = buffer_height.saturating_sub(height_one_third); let panel_height_bottom = height_one_third; let artist_list = Rect { x: frame.x, y: frame.y, width: panel_width, - height: frame.height, + height: buffer_height, }; let album_list = Rect { @@ -88,6 +92,13 @@ impl FrameArea { height: panel_height_bottom, }; + let minibuffer = Rect { + x: frame.x, + y: frame.y + buffer_height, + width: frame.width, + height: minibuffer_height, + }; + FrameArea { artist: ArtistArea { list: artist_list }, album: AlbumArea { @@ -98,6 +109,7 @@ impl FrameArea { list: track_list, info: track_info, }, + minibuffer, } } } @@ -118,10 +130,10 @@ impl OverlaySize { match self { OverlaySize::MarginFactor(margin_factor) => { let margin = full / margin_factor; - (margin, full - (2 * margin)) + (margin, full.saturating_sub(2 * margin)) } OverlaySize::Value(value) => { - let margin = (full - value) / 2; + let margin = (full.saturating_sub(*value)) / 2; (margin, *value) } } @@ -335,6 +347,68 @@ impl<'a, 'b> TrackState<'a, 'b> { } } +struct Minibuffer<'a> { + paragraphs: Vec>, + columns: u16, +} + +impl Minibuffer<'_> { + fn paragraphs(state: &AppPublicState) -> Self { + let columns = 3; + let mb = match state { + AppState::Browse(_) => Minibuffer { + paragraphs: vec![ + Paragraph::new("m: show info overlay"), + Paragraph::new("g: show reload menu"), + ], + columns, + }, + AppState::Info(_) => Minibuffer { + paragraphs: vec![Paragraph::new("m: hide info overlay")], + columns, + }, + AppState::Reload(_) => Minibuffer { + paragraphs: vec![ + Paragraph::new("g: hide reload menu"), + Paragraph::new("d: reload database"), + Paragraph::new("l: reload library"), + ], + columns, + }, + AppState::Error(_) => Minibuffer { + paragraphs: vec![Paragraph::new( + "Press any key to dismiss the error message...", + )], + columns: 1, + }, + AppState::Critical(_) => Minibuffer { + paragraphs: vec![Paragraph::new("Press ctrl+c to terminate the program...")], + columns: 1, + }, + }; + + // Callers will assume this. + assert!(mb.columns >= mb.paragraphs.len() as u16); + mb + } +} + +struct ReloadMenu; + +impl ReloadMenu { + fn paragraph<'a>() -> Paragraph<'a> { + Paragraph::new( + "d: database\n\ + l: library", + ) + } +} + +struct Column<'a> { + paragraph: Paragraph<'a>, + area: Rect, +} + pub struct Ui; impl Ui { @@ -359,12 +433,15 @@ impl Ui { } } - fn block<'a>(title: &str, active: bool, error: bool) -> Block<'a> { - Block::default() + fn block<'a>(active: bool, error: bool) -> Block<'a> { + Block::default().style(Self::block_style(active, error)) + } + + fn block_with_borders<'a>(title: &str, active: bool, error: bool) -> Block<'a> { + Self::block(active, error) .title_alignment(Alignment::Center) .borders(Borders::ALL) .border_type(BorderType::Rounded) - .style(Self::block_style(active, error)) .title(format!(" {title} ")) } @@ -380,7 +457,7 @@ impl Ui { list.highlight_style(Self::highlight_style(active)) .highlight_symbol(">> ") .style(Self::style(active, false)) - .block(Self::block(title, active, false)), + .block(Self::block_with_borders(title, active, false)), area, &mut state.list, ); @@ -397,7 +474,7 @@ impl Ui { frame.render_widget( paragraph .style(Self::style(active, false)) - .block(Self::block(title, active, false)), + .block(Self::block_with_borders(title, active, false)), area, ); } @@ -413,11 +490,59 @@ impl Ui { frame.render_widget( paragraph .style(Self::style(true, error)) - .block(Self::block(title, true, error)), + .block(Self::block_with_borders(title, true, error)), area, ); } + fn columns(paragraphs: Vec, min: u16, area: Rect) -> Vec { + let mut x = area.x; + let mut width = area.width; + let mut remaining = paragraphs.len() as u16; + if remaining < min { + remaining = min; + } + + let mut blocks = vec![]; + for paragraph in paragraphs.into_iter() { + let block_width = width / remaining; + + blocks.push(Column { + paragraph, + area: Rect { + x, + y: area.y, + width: block_width, + height: area.height, + }, + }); + + x = x.saturating_add(block_width); + width = width.saturating_sub(block_width); + remaining -= 1; + } + + blocks + } + + fn render_columns( + paragraphs: Vec, + min: u16, + active: bool, + area: Rect, + frame: &mut Frame<'_, B>, + ) { + for column in Self::columns(paragraphs, min, area).into_iter() { + frame.render_widget( + column + .paragraph + .style(Self::style(active, false)) + .block(Self::block(active, false)), + column.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); } @@ -432,9 +557,29 @@ impl Ui { Self::render_info_widget("Track info", st.info, st.active, ar.info, fr); } - fn render_collection( + fn render_minibuffer(state: &AppPublicState, ar: Rect, fr: &mut Frame<'_, B>) { + let mut mb = Minibuffer::paragraphs(state); + mb.paragraphs = mb + .paragraphs + .into_iter() + .map(|p| p.alignment(Alignment::Center)) + .collect(); + + let area = Rect { + x: ar.x + 1, + y: ar.y + 1, + width: ar.width.saturating_sub(2), + height: 1, + }; + + Self::render_info_widget("Minibuffer", Paragraph::new(""), false, ar, fr); + Self::render_columns(mb.paragraphs, mb.columns, false, area, fr); + } + + fn render_main_frame( artists: &Collection, selection: &mut Selection, + state: &AppPublicState, frame: &mut Frame<'_, B>, ) { let active = selection.active; @@ -480,6 +625,8 @@ impl Ui { ); Self::render_track_column(track_state, areas.track, frame); + + Self::render_minibuffer(state, areas.minibuffer, frame); } fn render_info_overlay( @@ -501,11 +648,7 @@ impl Ui { .with_height(OverlaySize::Value(4)) .build(frame.size()); - let reload_text = Paragraph::new( - "d: database\n\ - l: library", - ) - .alignment(Alignment::Center); + let reload_text = ReloadMenu::paragraph().alignment(Alignment::Center); Self::render_overlay_widget("Reload", reload_text, area, false, frame); } @@ -529,9 +672,10 @@ impl IUi for Ui { let collection = app.collection; let selection = app.selection; + let state = app.state; - Self::render_collection(collection, selection, frame); - match app.state { + Self::render_main_frame(collection, selection, state, frame); + match state { AppState::Info(_) => Self::render_info_overlay(collection, selection, frame), AppState::Reload(_) => Self::render_reload_overlay(frame), AppState::Error(ref msg) => Self::render_error_overlay("Error", msg, frame),