From 64b93c837e2a8aba3ab97fdc3acb6372a0d11782 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 10 Feb 2024 22:46:18 +0100 Subject: [PATCH 1/3] Functional minibuffer --- src/tui/app/app.rs | 4 +- src/tui/handler.rs | 18 ++++-- src/tui/ui.rs | 157 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 150 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..395f797 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,45 @@ impl<'a, 'b> TrackState<'a, 'b> { } } +struct Minibuffer; + +impl Minibuffer { + fn paragraphs(state: &AppPublicState) -> Vec { + match state { + AppState::Browse(_) => vec![ + Paragraph::new("m: show info overlay"), + Paragraph::new("g: show reload menu"), + ], + AppState::Info(_) => vec![Paragraph::new("m: hide info overlay")], + AppState::Reload(_) => vec![ + Paragraph::new("d: reload database"), + Paragraph::new("l: reload library"), + Paragraph::new("g: hide reload menu"), + ], + AppState::Error(_) => { + vec![Paragraph::new("Press any key to dismiss the error message")] + } + AppState::Critical(_) => vec![Paragraph::new("Press ctrl+c to terminate the program")], + } + } +} + +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 +410,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 +434,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 +451,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 +467,55 @@ 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, area: Rect) -> Vec { + let mut x = area.x; + let mut width = area.width; + let mut remaining = paragraphs.len() as u16; + + 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, + active: bool, + area: Rect, + frame: &mut Frame<'_, B>, + ) { + for column in Self::columns(paragraphs, 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 +530,27 @@ 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 paragraphs = Minibuffer::paragraphs(state) + .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(paragraphs, false, area, fr); + } + + fn render_main_frame( artists: &Collection, selection: &mut Selection, + state: &AppPublicState, frame: &mut Frame<'_, B>, ) { let active = selection.active; @@ -480,6 +596,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 +619,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 +643,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), -- 2.45.2 From 78fcbafc04eed77b220175fb0d46b4844b002e96 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 10 Feb 2024 23:17:40 +0100 Subject: [PATCH 2/3] Decouple number of columns from paragraphs --- src/tui/ui.rs | 64 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 395f797..23eaccf 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -347,25 +347,41 @@ impl<'a, 'b> TrackState<'a, 'b> { } } -struct Minibuffer; +struct Minibuffer<'a> { + paragraphs: Vec>, + columns: u16, +} -impl Minibuffer { - fn paragraphs(state: &AppPublicState) -> Vec { +impl Minibuffer<'_> { + fn paragraphs(state: &AppPublicState) -> Self { match state { - AppState::Browse(_) => vec![ - Paragraph::new("m: show info overlay"), - Paragraph::new("g: show reload menu"), - ], - AppState::Info(_) => vec![Paragraph::new("m: hide info overlay")], - AppState::Reload(_) => vec![ - Paragraph::new("d: reload database"), - Paragraph::new("l: reload library"), - Paragraph::new("g: hide reload menu"), - ], - AppState::Error(_) => { - vec![Paragraph::new("Press any key to dismiss the error message")] - } - AppState::Critical(_) => vec![Paragraph::new("Press ctrl+c to terminate the program")], + AppState::Browse(_) => Minibuffer { + paragraphs: vec![ + Paragraph::new("m: show info overlay"), + Paragraph::new("g: show reload menu"), + ], + columns: 3, + }, + AppState::Info(_) => Minibuffer { + paragraphs: vec![Paragraph::new("m: hide info overlay")], + columns: 3, + }, + AppState::Reload(_) => Minibuffer { + paragraphs: vec![ + Paragraph::new("g: hide reload menu"), + Paragraph::new("d: reload database"), + Paragraph::new("l: reload library"), + ], + columns: 3, + }, + 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, + }, } } } @@ -472,10 +488,13 @@ impl Ui { ); } - fn columns(paragraphs: Vec, area: Rect) -> Vec { + 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() { @@ -501,11 +520,12 @@ impl Ui { fn render_columns( paragraphs: Vec, + min: u16, active: bool, area: Rect, frame: &mut Frame<'_, B>, ) { - for column in Self::columns(paragraphs, area).into_iter() { + for column in Self::columns(paragraphs, min, area).into_iter() { frame.render_widget( column .paragraph @@ -531,7 +551,9 @@ impl Ui { } fn render_minibuffer(state: &AppPublicState, ar: Rect, fr: &mut Frame<'_, B>) { - let paragraphs = Minibuffer::paragraphs(state) + let mut mb = Minibuffer::paragraphs(state); + mb.paragraphs = mb + .paragraphs .into_iter() .map(|p| p.alignment(Alignment::Center)) .collect(); @@ -544,7 +566,7 @@ impl Ui { }; Self::render_info_widget("Minibuffer", Paragraph::new(""), false, ar, fr); - Self::render_columns(paragraphs, false, area, fr); + Self::render_columns(mb.paragraphs, mb.columns, false, area, fr); } fn render_main_frame( -- 2.45.2 From adbdcee58c7d46af3c00c5c9e80e661ed6eb6f0b Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 10 Feb 2024 23:20:46 +0100 Subject: [PATCH 3/3] Last fixes --- src/tui/ui.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 23eaccf..e681e08 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -354,17 +354,18 @@ struct Minibuffer<'a> { impl Minibuffer<'_> { fn paragraphs(state: &AppPublicState) -> Self { - match state { + 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: 3, + columns, }, AppState::Info(_) => Minibuffer { paragraphs: vec![Paragraph::new("m: hide info overlay")], - columns: 3, + columns, }, AppState::Reload(_) => Minibuffer { paragraphs: vec![ @@ -372,17 +373,23 @@ impl Minibuffer<'_> { Paragraph::new("d: reload database"), Paragraph::new("l: reload library"), ], - columns: 3, + columns, }, AppState::Error(_) => Minibuffer { - paragraphs: vec![Paragraph::new("Press any key to dismiss the error message...")], + 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 } } -- 2.45.2