Add a minibuffer #131
@ -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> {
|
||||
|
@ -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.
|
||||
_ => {}
|
||||
}
|
||||
|
186
src/tui/ui.rs
186
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<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),
|
||||
|
Loading…
Reference in New Issue
Block a user