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