Add a minibuffer #131

Merged
wojtek merged 3 commits from 125---add-a-minibuffer into main 2024-02-10 23:26:00 +01:00
3 changed files with 179 additions and 29 deletions

View File

@ -87,10 +87,12 @@ pub trait IAppAccess {
fn get(&mut self) -> AppPublic; fn get(&mut self) -> AppPublic;
} }
pub type AppPublicState = AppState<(), (), (), String, String>;
pub struct AppPublic<'app> { pub struct AppPublic<'app> {
pub collection: &'app Collection, pub collection: &'app Collection,
pub selection: &'app mut Selection, pub selection: &'app mut Selection,
pub state: &'app AppState<(), (), (), String, String>, pub state: &'app AppPublicState,
} }
pub struct App<MH: IMusicHoard> { pub struct App<MH: IMusicHoard> {

View File

@ -84,7 +84,7 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
fn handle_browse_key_event(app: &mut <APP as IAppInteract>::BS, key_event: KeyEvent) { fn handle_browse_key_event(app: &mut <APP as IAppInteract>::BS, key_event: KeyEvent) {
match key_event.code { match key_event.code {
// Exit application on `ESC` or `q`. // Exit application on `ESC` or `q`.
KeyCode::Esc | KeyCode::Char('q') => { KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
app.save(); app.save();
app.quit(); app.quit();
} }
@ -108,9 +108,11 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
fn handle_info_key_event(app: &mut <APP as IAppInteract>::IS, key_event: KeyEvent) { fn handle_info_key_event(app: &mut <APP as IAppInteract>::IS, key_event: KeyEvent) {
match key_event.code { match key_event.code {
// Toggle overlay. // Toggle overlay.
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('m') | KeyCode::Char('M') => { KeyCode::Esc
app.hide_info_overlay() | KeyCode::Char('q')
} | KeyCode::Char('Q')
| KeyCode::Char('m')
| KeyCode::Char('M') => app.hide_info_overlay(),
// Othey keys. // Othey keys.
_ => {} _ => {}
} }
@ -122,9 +124,11 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
KeyCode::Char('l') | KeyCode::Char('L') => app.reload_library(), KeyCode::Char('l') | KeyCode::Char('L') => app.reload_library(),
KeyCode::Char('d') | KeyCode::Char('D') => app.reload_database(), KeyCode::Char('d') | KeyCode::Char('D') => app.reload_database(),
// Return. // Return.
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('g') | KeyCode::Char('G') => { KeyCode::Esc
app.hide_reload_menu() | KeyCode::Char('q')
} | KeyCode::Char('Q')
| KeyCode::Char('g')
| KeyCode::Char('G') => app.hide_reload_menu(),
// Othey keys. // Othey keys.
_ => {} _ => {}
} }

View File

@ -15,7 +15,7 @@ use ratatui::{
}; };
use crate::tui::app::{ use crate::tui::app::{
app::{AppState, IAppAccess}, app::{AppPublicState, AppState, IAppAccess},
selection::{Category, Selection, WidgetState}, selection::{Category, Selection, WidgetState},
}; };
@ -41,23 +41,27 @@ struct FrameArea {
artist: ArtistArea, artist: ArtistArea,
album: AlbumArea, album: AlbumArea,
track: TrackArea, track: TrackArea,
minibuffer: Rect,
} }
impl FrameArea { impl FrameArea {
fn new(frame: Rect) -> Self { 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 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 = width_one_third;
let panel_width_last = frame.width - 2 * panel_width; let panel_width_last = frame.width.saturating_sub(2 * panel_width);
let panel_height_top = frame.height - height_one_third; let panel_height_top = buffer_height.saturating_sub(height_one_third);
let panel_height_bottom = height_one_third; let panel_height_bottom = height_one_third;
let artist_list = Rect { let artist_list = Rect {
x: frame.x, x: frame.x,
y: frame.y, y: frame.y,
width: panel_width, width: panel_width,
height: frame.height, height: buffer_height,
}; };
let album_list = Rect { let album_list = Rect {
@ -88,6 +92,13 @@ impl FrameArea {
height: panel_height_bottom, height: panel_height_bottom,
}; };
let minibuffer = Rect {
x: frame.x,
y: frame.y + buffer_height,
width: frame.width,
height: minibuffer_height,
};
FrameArea { FrameArea {
artist: ArtistArea { list: artist_list }, artist: ArtistArea { list: artist_list },
album: AlbumArea { album: AlbumArea {
@ -98,6 +109,7 @@ impl FrameArea {
list: track_list, list: track_list,
info: track_info, info: track_info,
}, },
minibuffer,
} }
} }
} }
@ -118,10 +130,10 @@ impl OverlaySize {
match self { match self {
OverlaySize::MarginFactor(margin_factor) => { OverlaySize::MarginFactor(margin_factor) => {
let margin = full / margin_factor; let margin = full / margin_factor;
(margin, full - (2 * margin)) (margin, full.saturating_sub(2 * margin))
} }
OverlaySize::Value(value) => { OverlaySize::Value(value) => {
let margin = (full - value) / 2; let margin = (full.saturating_sub(*value)) / 2;
(margin, *value) (margin, *value)
} }
} }
@ -335,6 +347,68 @@ impl<'a, 'b> TrackState<'a, 'b> {
} }
} }
struct Minibuffer<'a> {
paragraphs: Vec<Paragraph<'a>>,
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; pub struct Ui;
impl Ui { impl Ui {
@ -359,12 +433,15 @@ impl Ui {
} }
} }
fn block<'a>(title: &str, active: bool, error: bool) -> Block<'a> { fn block<'a>(active: bool, error: bool) -> Block<'a> {
Block::default() 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) .title_alignment(Alignment::Center)
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.style(Self::block_style(active, error))
.title(format!(" {title} ")) .title(format!(" {title} "))
} }
@ -380,7 +457,7 @@ impl Ui {
list.highlight_style(Self::highlight_style(active)) list.highlight_style(Self::highlight_style(active))
.highlight_symbol(">> ") .highlight_symbol(">> ")
.style(Self::style(active, false)) .style(Self::style(active, false))
.block(Self::block(title, active, false)), .block(Self::block_with_borders(title, active, false)),
area, area,
&mut state.list, &mut state.list,
); );
@ -397,7 +474,7 @@ impl Ui {
frame.render_widget( frame.render_widget(
paragraph paragraph
.style(Self::style(active, false)) .style(Self::style(active, false))
.block(Self::block(title, active, false)), .block(Self::block_with_borders(title, active, false)),
area, area,
); );
} }
@ -413,11 +490,59 @@ impl Ui {
frame.render_widget( frame.render_widget(
paragraph paragraph
.style(Self::style(true, error)) .style(Self::style(true, error))
.block(Self::block(title, true, error)), .block(Self::block_with_borders(title, true, error)),
area, area,
); );
} }
fn columns(paragraphs: Vec<Paragraph>, min: u16, area: Rect) -> Vec<Column> {
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<B: Backend>(
paragraphs: Vec<Paragraph>,
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<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);
} }
@ -432,9 +557,29 @@ impl Ui {
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);
} }
fn render_collection<B: Backend>( fn render_minibuffer<B: Backend>(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<B: Backend>(
artists: &Collection, artists: &Collection,
selection: &mut Selection, selection: &mut Selection,
state: &AppPublicState,
frame: &mut Frame<'_, B>, frame: &mut Frame<'_, B>,
) { ) {
let active = selection.active; let active = selection.active;
@ -480,6 +625,8 @@ impl Ui {
); );
Self::render_track_column(track_state, areas.track, frame); Self::render_track_column(track_state, areas.track, frame);
Self::render_minibuffer(state, areas.minibuffer, frame);
} }
fn render_info_overlay<B: Backend>( fn render_info_overlay<B: Backend>(
@ -501,11 +648,7 @@ impl Ui {
.with_height(OverlaySize::Value(4)) .with_height(OverlaySize::Value(4))
.build(frame.size()); .build(frame.size());
let reload_text = Paragraph::new( let reload_text = ReloadMenu::paragraph().alignment(Alignment::Center);
"d: database\n\
l: library",
)
.alignment(Alignment::Center);
Self::render_overlay_widget("Reload", reload_text, area, false, frame); Self::render_overlay_widget("Reload", reload_text, area, false, frame);
} }
@ -529,9 +672,10 @@ impl IUi for Ui {
let collection = app.collection; let collection = app.collection;
let selection = app.selection; let selection = app.selection;
let state = app.state;
Self::render_collection(collection, selection, frame); Self::render_main_frame(collection, selection, state, frame);
match app.state { match state {
AppState::Info(_) => Self::render_info_overlay(collection, selection, frame), AppState::Info(_) => Self::render_info_overlay(collection, selection, frame),
AppState::Reload(_) => Self::render_reload_overlay(frame), AppState::Reload(_) => Self::render_reload_overlay(frame),
AppState::Error(ref msg) => Self::render_error_overlay("Error", msg, frame), AppState::Error(ref msg) => Self::render_error_overlay("Error", msg, frame),