Add a minibuffer (#131)
All checks were successful
Cargo CI / Build and Test (push) Successful in 1m43s
Cargo CI / Lint (push) Successful in 1m15s

Closes #125

Reviewed-on: #131
This commit is contained in:
Wojciech Kozlowski 2024-02-10 23:25:59 +01:00
parent de564eb1a0
commit 6a18c5d9cc
3 changed files with 179 additions and 29 deletions

View File

@ -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<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) {
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<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
fn handle_info_key_event(app: &mut <APP as IAppInteract>::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<APP: IAppInteract> IEventHandlerPrivate<APP> 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.
_ => {}
}

View File

@ -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<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;
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<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>) {
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<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,
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<B: Backend>(
@ -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),