Improve generics in TUI

This commit is contained in:
Wojciech Kozlowski 2023-04-13 15:27:00 +02:00
parent f58ffb036a
commit d8f1db4e42
5 changed files with 158 additions and 109 deletions

View File

@ -13,7 +13,7 @@ use musichoard::{
mod tui; mod tui;
use tui::{ use tui::{
app::App, event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, ui::Ui, app::TuiApp, event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, ui::Ui,
Tui, Tui,
}; };
@ -59,7 +59,7 @@ fn main() {
let ui = Ui::new(); let ui = Ui::new();
let app = App::new(collection_manager).expect("failed to initialise app"); let app = TuiApp::new(collection_manager).expect("failed to initialise app");
// Run the TUI application. // Run the TUI application.
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui"); Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");

View File

@ -110,31 +110,74 @@ struct Selection {
artist: Option<ArtistSelection>, artist: Option<ArtistSelection>,
} }
pub struct App<CM> { pub trait App {
fn is_running(&self) -> bool;
fn quit(&mut self);
fn increment_category(&mut self);
fn decrement_category(&mut self);
fn increment_selection(&mut self);
fn decrement_selection(&mut self);
fn get_active_category(&self) -> Category;
fn get_artist_ids(&self) -> Vec<&ArtistId>;
fn get_album_ids(&self) -> Vec<&AlbumId>;
fn get_track_ids(&self) -> Vec<&Track>;
fn selected_artist(&self) -> Option<usize>;
fn selected_album(&self) -> Option<usize>;
fn selected_track(&self) -> Option<usize>;
}
trait AppPrivate {
fn increment_artist_selection(&mut self);
fn decrement_artist_selection(&mut self);
fn increment_album_selection(&mut self);
fn decrement_album_selection(&mut self);
fn increment_track_selection(&mut self);
fn decrement_track_selection(&mut self);
fn get_artists(&self) -> &Vec<Artist>;
fn get_albums(&self) -> Option<&Vec<Album>>;
fn get_tracks(&self) -> Option<&Vec<Track>>;
}
pub struct TuiApp<CM> {
collection_manager: CM, collection_manager: CM,
selection: Selection, selection: Selection,
running: bool, running: bool,
} }
impl<CM: CollectionManager> App<CM> { impl<CM: CollectionManager> TuiApp<CM> {
pub fn new(mut collection_manager: CM) -> Result<Self, Error> { pub fn new(mut collection_manager: CM) -> Result<Self, Error> {
collection_manager.rescan_library()?; collection_manager.rescan_library()?;
let selection = Selection { let selection = Selection {
active: Category::Artist, active: Category::Artist,
artist: ArtistSelection::initialise(collection_manager.get_collection()), artist: ArtistSelection::initialise(collection_manager.get_collection()),
}; };
Ok(App { Ok(TuiApp {
collection_manager, collection_manager,
selection, selection,
running: true, running: true,
}) })
} }
}
pub fn is_running(&self) -> bool { impl<CM: CollectionManager> App for TuiApp<CM> {
fn is_running(&self) -> bool {
self.running self.running
} }
pub fn increment_category(&mut self) { fn quit(&mut self) {
self.running = false;
}
fn increment_category(&mut self) {
self.selection.active = match self.selection.active { self.selection.active = match self.selection.active {
Category::Artist => Category::Album, Category::Artist => Category::Album,
Category::Album => Category::Track, Category::Album => Category::Track,
@ -142,7 +185,7 @@ impl<CM: CollectionManager> App<CM> {
}; };
} }
pub fn decrement_category(&mut self) { fn decrement_category(&mut self) {
self.selection.active = match self.selection.active { self.selection.active = match self.selection.active {
Category::Artist => Category::Artist, Category::Artist => Category::Artist,
Category::Album => Category::Artist, Category::Album => Category::Artist,
@ -150,7 +193,7 @@ impl<CM: CollectionManager> App<CM> {
}; };
} }
pub fn increment_selection(&mut self) { fn increment_selection(&mut self) {
match self.selection.active { match self.selection.active {
Category::Artist => self.increment_artist_selection(), Category::Artist => self.increment_artist_selection(),
Category::Album => self.increment_album_selection(), Category::Album => self.increment_album_selection(),
@ -158,7 +201,7 @@ impl<CM: CollectionManager> App<CM> {
} }
} }
pub fn decrement_selection(&mut self) { fn decrement_selection(&mut self) {
match self.selection.active { match self.selection.active {
Category::Artist => self.decrement_artist_selection(), Category::Artist => self.decrement_artist_selection(),
Category::Album => self.decrement_album_selection(), Category::Album => self.decrement_album_selection(),
@ -166,6 +209,57 @@ impl<CM: CollectionManager> App<CM> {
} }
} }
fn get_active_category(&self) -> Category {
self.selection.active
}
fn get_artist_ids(&self) -> Vec<&ArtistId> {
let artists = self.get_artists();
artists.iter().map(|a| &a.id).collect()
}
fn get_album_ids(&self) -> Vec<&AlbumId> {
if let Some(albums) = self.get_albums() {
albums.iter().map(|a| &a.id).collect()
} else {
vec![]
}
}
fn get_track_ids(&self) -> Vec<&Track> {
if let Some(tracks) = self.get_tracks() {
tracks.iter().collect()
} else {
vec![]
}
}
fn selected_artist(&self) -> Option<usize> {
self.selection.artist.as_ref().map(|s| s.index)
}
fn selected_album(&self) -> Option<usize> {
if let Some(ref artist_selection) = self.selection.artist {
artist_selection.album.as_ref().map(|s| s.index)
} else {
None
}
}
fn selected_track(&self) -> Option<usize> {
if let Some(ref artist_selection) = self.selection.artist {
if let Some(ref album_selection) = artist_selection.album {
album_selection.track.as_ref().map(|s| s.index)
} else {
None
}
} else {
None
}
}
}
impl<CM: CollectionManager> AppPrivate for TuiApp<CM> {
fn increment_artist_selection(&mut self) { fn increment_artist_selection(&mut self) {
if let Some(ref mut artist_selection) = self.selection.artist { if let Some(ref mut artist_selection) = self.selection.artist {
let artists = &self.collection_manager.get_collection(); let artists = &self.collection_manager.get_collection();
@ -226,10 +320,6 @@ impl<CM: CollectionManager> App<CM> {
} }
} }
pub fn get_active_category(&self) -> Category {
self.selection.active
}
fn get_artists(&self) -> &Vec<Artist> { fn get_artists(&self) -> &Vec<Artist> {
self.collection_manager.get_collection() self.collection_manager.get_collection()
} }
@ -254,55 +344,6 @@ impl<CM: CollectionManager> App<CM> {
None None
} }
} }
pub fn get_artist_ids(&self) -> Vec<&ArtistId> {
let artists = self.get_artists();
artists.iter().map(|a| &a.id).collect()
}
pub fn get_album_ids(&self) -> Vec<&AlbumId> {
if let Some(albums) = self.get_albums() {
albums.iter().map(|a| &a.id).collect()
} else {
vec![]
}
}
pub fn get_track_ids(&self) -> Vec<&Track> {
if let Some(tracks) = self.get_tracks() {
tracks.iter().collect()
} else {
vec![]
}
}
pub fn selected_artist(&self) -> Option<usize> {
self.selection.artist.as_ref().map(|s| s.index)
}
pub fn selected_album(&self) -> Option<usize> {
if let Some(ref artist_selection) = self.selection.artist {
artist_selection.album.as_ref().map(|s| s.index)
} else {
None
}
}
pub fn selected_track(&self) -> Option<usize> {
if let Some(ref artist_selection) = self.selection.artist {
if let Some(ref album_selection) = artist_selection.album {
album_selection.track.as_ref().map(|s| s.index)
} else {
None
}
} else {
None
}
}
pub fn quit(&mut self) {
self.running = false;
}
} }
#[cfg(test)] #[cfg(test)]
@ -516,7 +557,7 @@ mod tests {
.expect_get_collection() .expect_get_collection()
.return_const(COLLECTION.to_owned()); .return_const(COLLECTION.to_owned());
let mut app = App::new(collection_manager).unwrap(); let mut app = TuiApp::new(collection_manager).unwrap();
assert!(app.is_running()); assert!(app.is_running());
app.quit(); app.quit();
@ -535,7 +576,7 @@ mod tests {
.expect_get_collection() .expect_get_collection()
.return_const(COLLECTION.to_owned()); .return_const(COLLECTION.to_owned());
let mut app = App::new(collection_manager).unwrap(); let mut app = TuiApp::new(collection_manager).unwrap();
assert!(app.is_running()); assert!(app.is_running());
assert!(!app.get_artist_ids().is_empty()); assert!(!app.get_artist_ids().is_empty());
@ -640,7 +681,7 @@ mod tests {
.expect_get_collection() .expect_get_collection()
.return_const(collection); .return_const(collection);
let mut app = App::new(collection_manager).unwrap(); let mut app = TuiApp::new(collection_manager).unwrap();
assert!(app.is_running()); assert!(app.is_running());
assert!(!app.get_artist_ids().is_empty()); assert!(!app.get_artist_ids().is_empty());
@ -682,7 +723,7 @@ mod tests {
.expect_get_collection() .expect_get_collection()
.return_const(collection); .return_const(collection);
let mut app = App::new(collection_manager).unwrap(); let mut app = TuiApp::new(collection_manager).unwrap();
assert!(app.is_running()); assert!(app.is_running());
assert!(!app.get_artist_ids().is_empty()); assert!(!app.get_artist_ids().is_empty());
@ -736,7 +777,7 @@ mod tests {
.expect_get_collection() .expect_get_collection()
.return_const(collection); .return_const(collection);
let mut app = App::new(collection_manager).unwrap(); let mut app = TuiApp::new(collection_manager).unwrap();
assert!(app.is_running()); assert!(app.is_running());
assert!(app.get_artist_ids().is_empty()); assert!(app.get_artist_ids().is_empty());

View File

@ -2,7 +2,6 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
use musichoard::collection::CollectionManager;
use super::{ use super::{
app::App, app::App,
@ -10,12 +9,12 @@ use super::{
}; };
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait EventHandler<CM> { pub trait EventHandler<APP> {
fn handle_next_event(&self, app: &mut App<CM>) -> Result<(), EventError>; fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError>;
} }
trait EventHandlerPrivate<CM> { trait EventHandlerPrivate<APP> {
fn handle_key_event(app: &mut App<CM>, key_event: KeyEvent); fn handle_key_event(app: &mut APP, key_event: KeyEvent);
} }
pub struct TuiEventHandler { pub struct TuiEventHandler {
@ -29,8 +28,8 @@ impl TuiEventHandler {
} }
} }
impl<CM: CollectionManager> EventHandler<CM> for TuiEventHandler { impl<APP: App> EventHandler<APP> for TuiEventHandler {
fn handle_next_event(&self, app: &mut App<CM>) -> Result<(), EventError> { fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError> {
match self.events.recv()? { match self.events.recv()? {
Event::Key(key_event) => Self::handle_key_event(app, key_event), Event::Key(key_event) => Self::handle_key_event(app, key_event),
Event::Mouse(_) => {} Event::Mouse(_) => {}
@ -40,8 +39,8 @@ impl<CM: CollectionManager> EventHandler<CM> for TuiEventHandler {
} }
} }
impl<CM: CollectionManager> EventHandlerPrivate<CM> for TuiEventHandler { impl<APP: App> EventHandlerPrivate<APP> for TuiEventHandler {
fn handle_key_event(app: &mut App<CM>, key_event: KeyEvent) { fn handle_key_event(app: &mut APP, 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') => {

View File

@ -1,6 +1,6 @@
use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
use musichoard::collection::{self, CollectionManager}; use musichoard::collection;
use ratatui::backend::Backend; use ratatui::backend::Backend;
use ratatui::Terminal; use ratatui::Terminal;
use std::io; use std::io;
@ -44,12 +44,12 @@ impl From<EventError> for Error {
} }
} }
pub struct Tui<B: Backend, CM> { pub struct Tui<B: Backend, APP> {
terminal: Terminal<B>, terminal: Terminal<B>,
_phantom: PhantomData<CM>, _phantom: PhantomData<APP>,
} }
impl<B: Backend, CM: CollectionManager> Tui<B, CM> { impl<B: Backend, APP: App> Tui<B, APP> {
fn init(&mut self) -> Result<(), Error> { fn init(&mut self) -> Result<(), Error> {
self.terminal.hide_cursor()?; self.terminal.hide_cursor()?;
self.terminal.clear()?; self.terminal.clear()?;
@ -68,9 +68,9 @@ impl<B: Backend, CM: CollectionManager> Tui<B, CM> {
fn main_loop( fn main_loop(
&mut self, &mut self,
mut app: App<CM>, mut app: APP,
ui: Ui<CM>, ui: Ui<APP>,
handler: impl EventHandler<CM>, handler: impl EventHandler<APP>,
) -> Result<(), Error> { ) -> Result<(), Error> {
while app.is_running() { while app.is_running() {
self.terminal.draw(|frame| ui.render(&app, frame))?; self.terminal.draw(|frame| ui.render(&app, frame))?;
@ -82,9 +82,9 @@ impl<B: Backend, CM: CollectionManager> Tui<B, CM> {
fn main( fn main(
term: Terminal<B>, term: Terminal<B>,
app: App<CM>, app: APP,
ui: Ui<CM>, ui: Ui<APP>,
handler: impl EventHandler<CM>, handler: impl EventHandler<APP>,
listener: impl EventListener, listener: impl EventListener,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut tui = Tui { let mut tui = Tui {
@ -142,9 +142,9 @@ impl<B: Backend, CM: CollectionManager> Tui<B, CM> {
pub fn run( pub fn run(
term: Terminal<B>, term: Terminal<B>,
app: App<CM>, app: APP,
ui: Ui<CM>, ui: Ui<APP>,
handler: impl EventHandler<CM>, handler: impl EventHandler<APP>,
listener: impl EventListener, listener: impl EventListener,
) -> Result<(), Error> { ) -> Result<(), Error> {
Self::enable()?; Self::enable()?;
@ -175,8 +175,12 @@ mod tests {
use crate::tests::{MockCollectionManager, COLLECTION}; use crate::tests::{MockCollectionManager, COLLECTION};
use super::{ use super::{
app::App, event::EventError, handler::MockEventHandler, listener::MockEventListener, app::{App, TuiApp},
ui::Ui, Error, Tui, event::EventError,
handler::MockEventHandler,
listener::MockEventListener,
ui::Ui,
Error, Tui,
}; };
pub fn terminal() -> Terminal<TestBackend> { pub fn terminal() -> Terminal<TestBackend> {
@ -184,7 +188,7 @@ mod tests {
Terminal::new(backend).unwrap() Terminal::new(backend).unwrap()
} }
pub fn app(collection: Collection) -> App<MockCollectionManager> { pub fn app(collection: Collection) -> TuiApp<MockCollectionManager> {
let mut collection_manager = MockCollectionManager::new(); let mut collection_manager = MockCollectionManager::new();
collection_manager collection_manager
@ -194,7 +198,7 @@ mod tests {
.expect_get_collection() .expect_get_collection()
.return_const(collection); .return_const(collection);
App::new(collection_manager).unwrap() TuiApp::new(collection_manager).unwrap()
} }
fn listener() -> MockEventListener { fn listener() -> MockEventListener {
@ -208,12 +212,14 @@ mod tests {
listener listener
} }
fn handler() -> MockEventHandler<MockCollectionManager> { fn handler() -> MockEventHandler<TuiApp<MockCollectionManager>> {
let mut handler = MockEventHandler::new(); let mut handler = MockEventHandler::new();
handler.expect_handle_next_event().return_once(|app| { handler.expect_handle_next_event().return_once(
|app: &mut TuiApp<MockCollectionManager>| {
app.quit(); app.quit();
Ok(()) Ok(())
}); },
);
handler handler
} }

View File

@ -1,6 +1,6 @@
use std::marker::PhantomData; use std::marker::PhantomData;
use musichoard::{collection::CollectionManager, TrackFormat}; use musichoard::TrackFormat;
use ratatui::{ use ratatui::{
backend::Backend, backend::Backend,
layout::{Alignment, Rect}, layout::{Alignment, Rect},
@ -59,11 +59,11 @@ struct AppState<'a> {
tracks: TrackState<'a>, tracks: TrackState<'a>,
} }
pub struct Ui<CM> { pub struct Ui<APP> {
_phantom: PhantomData<CM>, _phantom: PhantomData<APP>,
} }
impl<CM: CollectionManager> Ui<CM> { impl<APP: App> Ui<APP> {
pub fn new() -> Self { pub fn new() -> Self {
Ui { Ui {
_phantom: PhantomData, _phantom: PhantomData,
@ -127,7 +127,7 @@ impl<CM: CollectionManager> Ui<CM> {
} }
} }
fn construct_artist_list(app: &App<CM>) -> ArtistState { fn construct_artist_list(app: &APP) -> ArtistState {
let artists = app.get_artist_ids(); let artists = app.get_artist_ids();
let list = List::new( let list = List::new(
artists artists
@ -149,7 +149,7 @@ impl<CM: CollectionManager> Ui<CM> {
} }
} }
fn construct_album_list(app: &App<CM>) -> AlbumState { fn construct_album_list(app: &APP) -> AlbumState {
let albums = app.get_album_ids(); let albums = app.get_album_ids();
let list = List::new( let list = List::new(
albums albums
@ -182,7 +182,7 @@ impl<CM: CollectionManager> Ui<CM> {
} }
} }
fn construct_track_list(app: &App<CM>) -> TrackState { fn construct_track_list(app: &APP) -> TrackState {
let tracks = app.get_track_ids(); let tracks = app.get_track_ids();
let list = List::new( let list = List::new(
tracks tracks
@ -226,7 +226,7 @@ impl<CM: CollectionManager> Ui<CM> {
} }
} }
fn construct_app_state(app: &App<CM>) -> AppState { fn construct_app_state(app: &APP) -> AppState {
AppState { AppState {
artists: Self::construct_artist_list(app), artists: Self::construct_artist_list(app),
albums: Self::construct_album_list(app), albums: Self::construct_album_list(app),
@ -318,7 +318,7 @@ impl<CM: CollectionManager> Ui<CM> {
Self::render_info_widget("Track info", state.info, state.active, area.info, frame); Self::render_info_widget("Track info", state.info, state.active, area.info, frame);
} }
pub fn render<B: Backend>(&self, app: &App<CM>, frame: &mut Frame<'_, B>) { pub fn render<B: Backend>(&self, app: &APP, frame: &mut Frame<'_, B>) {
let areas = Self::construct_areas(frame.size()); let areas = Self::construct_areas(frame.size());
let app_state = Self::construct_app_state(app); let app_state = Self::construct_app_state(app);
@ -334,7 +334,10 @@ mod tests {
use crate::{ use crate::{
tests::COLLECTION, tests::COLLECTION,
tui::tests::{app, terminal}, tui::{
app::App,
tests::{app, terminal},
},
}; };
use super::Ui; use super::Ui;