Working alternative type-based ui state
All checks were successful
Cargo CI / Build and Test (pull_request) Successful in 1m4s
Cargo CI / Lint (pull_request) Successful in 45s

This commit is contained in:
Wojciech Kozlowski 2024-01-28 13:54:47 +01:00
parent 9679efefc5
commit fdd6954ead
3 changed files with 132 additions and 129 deletions

View File

@ -5,21 +5,25 @@ use mockall::automock;
use crate::tui::{ use crate::tui::{
event::{Event, EventError, EventReceiver}, event::{Event, EventError, EventReceiver},
ui::{IUiBrowse, IUiCore, IUiInfo, Ui}, ui::{IUi, IUiBrowse, IUiInfo, UiState},
}; };
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IEventHandler<BS: IUiBrowse<IS>, IS: IUiInfo<BS>> { pub trait IEventHandler<UI: IUi> {
fn handle_next_event(&self, ui: Ui<BS, IS>) -> Result<Ui<BS, IS>, EventError>; fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError>;
} }
trait IEventHandlerPrivate<BS: IUiBrowse<IS>, IS: IUiInfo<BS>> { trait IEventHandlerPrivate<UI: IUi> {
fn handle_key_event(ui: Ui<BS, IS>, key_event: KeyEvent) -> Result<Ui<BS, IS>, EventError>; fn handle_key_event(ui: &mut UI, key_event: KeyEvent) -> Result<(), EventError>;
fn handle_browse_key_event(ui: BS, key_event: KeyEvent) -> Result<Ui<BS, IS>, EventError>; fn handle_browse_key_event(
fn handle_info_key_event(ui: IS, key_event: KeyEvent) -> Result<Ui<BS, IS>, EventError>; ui: &mut <UI as IUi>::BS,
fn handle_core_key_event<C: IUiCore>(ui: &mut C, key_event: KeyEvent) key_event: KeyEvent,
-> Result<(), EventError>; ) -> Result<(), EventError>;
fn quit<C: IUiCore>(ui: &mut C) -> Result<(), EventError>; fn handle_info_key_event(
ui: &mut <UI as IUi>::IS,
key_event: KeyEvent,
) -> Result<(), EventError>;
fn quit(ui: &mut UI) -> Result<(), EventError>;
} }
pub struct EventHandler { pub struct EventHandler {
@ -33,26 +37,46 @@ impl EventHandler {
} }
} }
impl<BS: IUiBrowse<IS>, IS: IUiInfo<BS>> IEventHandler<BS, IS> for EventHandler { impl<UI: IUi> IEventHandler<UI> for EventHandler {
fn handle_next_event(&self, mut ui: Ui<BS, IS>) -> Result<Ui<BS, IS>, EventError> { fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError> {
match self.events.recv()? { match self.events.recv()? {
Event::Key(key_event) => ui = Self::handle_key_event(ui, key_event)?, Event::Key(key_event) => Self::handle_key_event(ui, key_event)?,
Event::Mouse(_) => {} Event::Mouse(_) => {}
Event::Resize(_, _) => {} Event::Resize(_, _) => {}
}; };
Ok(ui) Ok(())
} }
} }
impl<BS: IUiBrowse<IS>, IS: IUiInfo<BS>> IEventHandlerPrivate<BS, IS> for EventHandler { impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
fn handle_key_event(ui: Ui<BS, IS>, key_event: KeyEvent) -> Result<Ui<BS, IS>, EventError> { fn handle_key_event(ui: &mut UI, key_event: KeyEvent) -> Result<(), EventError> {
match ui { match key_event.code {
Ui::Browse(browse) => Self::handle_browse_key_event(browse, key_event), // Exit application on `ESC` or `q`.
Ui::Info(info) => Self::handle_info_key_event(info, key_event), KeyCode::Esc | KeyCode::Char('q') => {
Self::quit(ui)?;
} }
// Exit application on `Ctrl-C`.
KeyCode::Char('c') | KeyCode::Char('C') => {
if key_event.modifiers == KeyModifiers::CONTROL {
Self::quit(ui)?;
}
}
_ => match ui.state() {
UiState::Browse(browse) => {
<Self as IEventHandlerPrivate<UI>>::handle_browse_key_event(browse, key_event)?;
}
UiState::Info(info) => {
<Self as IEventHandlerPrivate<UI>>::handle_info_key_event(info, key_event)?;
}
},
};
Ok(())
} }
fn handle_browse_key_event(mut ui: BS, key_event: KeyEvent) -> Result<Ui<BS, IS>, EventError> { fn handle_browse_key_event(
ui: &mut <UI as IUi>::BS,
key_event: KeyEvent,
) -> Result<(), EventError> {
match key_event.code { match key_event.code {
// Category change. // Category change.
KeyCode::Left => ui.decrement_category(), KeyCode::Left => ui.decrement_category(),
@ -61,52 +85,29 @@ impl<BS: IUiBrowse<IS>, IS: IUiInfo<BS>> IEventHandlerPrivate<BS, IS> for EventH
KeyCode::Up => ui.decrement_selection(), KeyCode::Up => ui.decrement_selection(),
KeyCode::Down => ui.increment_selection(), KeyCode::Down => ui.increment_selection(),
// Toggle overlay. // Toggle overlay.
KeyCode::Char('m') | KeyCode::Char('M') => { KeyCode::Char('m') | KeyCode::Char('M') => ui.show_info_overlay(),
return Ok(ui.display_info_overlay()); // Othey keys.
}
// Other keys.
_ => <Self as IEventHandlerPrivate<BS, IS>>::handle_core_key_event(&mut ui, key_event)?,
}
Ok(Ui::Browse(ui))
}
fn handle_info_key_event(mut ui: IS, key_event: KeyEvent) -> Result<Ui<BS, IS>, EventError> {
match key_event.code {
// Toggle overlay.
KeyCode::Char('m') | KeyCode::Char('M') => {
return Ok(ui.hide_info_overlay());
}
// Other keys.
_ => <Self as IEventHandlerPrivate<BS, IS>>::handle_core_key_event(&mut ui, key_event)?,
}
Ok(Ui::Info(ui))
}
fn handle_core_key_event<C: IUiCore>(
ui: &mut C,
key_event: KeyEvent,
) -> Result<(), EventError> {
match key_event.code {
// Exit application on `ESC` or `q`.
KeyCode::Esc | KeyCode::Char('q') => {
<Self as IEventHandlerPrivate<BS, IS>>::quit(ui)?;
}
// Exit application on `Ctrl-C`.
KeyCode::Char('c') | KeyCode::Char('C') => {
if key_event.modifiers == KeyModifiers::CONTROL {
<Self as IEventHandlerPrivate<BS, IS>>::quit(ui)?;
}
}
// Other keys.
_ => {} _ => {}
} }
Ok(()) Ok(())
} }
fn quit<C: IUiCore>(ui: &mut C) -> Result<(), EventError> { fn handle_info_key_event(
ui: &mut <UI as IUi>::IS,
key_event: KeyEvent,
) -> Result<(), EventError> {
match key_event.code {
// Toggle overlay.
KeyCode::Char('m') | KeyCode::Char('M') => ui.hide_info_overlay(),
// Othey keys.
_ => {}
}
Ok(())
}
fn quit(ui: &mut UI) -> Result<(), EventError> {
ui.quit(); ui.quit();
ui.save()?; ui.save()?;
Ok(()) Ok(())

View File

@ -15,7 +15,7 @@ use std::marker::PhantomData;
use self::event::EventError; use self::event::EventError;
use self::handler::IEventHandler; use self::handler::IEventHandler;
use self::listener::IEventListener; use self::listener::IEventListener;
use self::ui::{IUiBrowse, IUiInfo, Ui}; use self::ui::IUi;
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Error { pub enum Error {
@ -43,12 +43,12 @@ impl From<EventError> for Error {
} }
} }
pub struct Tui<B: Backend, BS: IUiBrowse<IS>, IS: IUiInfo<BS>> { pub struct Tui<B: Backend, UI: IUi> {
terminal: Terminal<B>, terminal: Terminal<B>,
_phantom: PhantomData<Ui<BS, IS>>, _phantom: PhantomData<UI>,
} }
impl<B: Backend, BS: IUiBrowse<IS>, IS: IUiInfo<BS>> Tui<B, BS, IS> { impl<B: Backend, UI: IUi> Tui<B, UI> {
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()?;
@ -65,14 +65,10 @@ impl<B: Backend, BS: IUiBrowse<IS>, IS: IUiInfo<BS>> Tui<B, BS, IS> {
self.exit(); self.exit();
} }
fn main_loop( fn main_loop(&mut self, mut ui: UI, handler: impl IEventHandler<UI>) -> Result<(), Error> {
&mut self,
mut ui: Ui<BS, IS>,
handler: impl IEventHandler<BS, IS>,
) -> Result<(), Error> {
while ui.is_running() { while ui.is_running() {
self.terminal.draw(|frame| ui.render(frame))?; self.terminal.draw(|frame| ui.render(frame))?;
ui = handler.handle_next_event(ui)?; handler.handle_next_event(&mut ui)?;
} }
Ok(()) Ok(())
@ -80,8 +76,8 @@ impl<B: Backend, BS: IUiBrowse<IS>, IS: IUiInfo<BS>> Tui<B, BS, IS> {
fn main( fn main(
term: Terminal<B>, term: Terminal<B>,
ui: Ui<BS, IS>, ui: UI,
handler: impl IEventHandler<BS, IS>, handler: impl IEventHandler<UI>,
listener: impl IEventListener, listener: impl IEventListener,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut tui = Tui { let mut tui = Tui {
@ -139,8 +135,8 @@ impl<B: Backend, BS: IUiBrowse<IS>, IS: IUiInfo<BS>> Tui<B, BS, IS> {
pub fn run( pub fn run(
term: Terminal<B>, term: Terminal<B>,
ui: Ui<BS, IS>, ui: UI,
handler: impl IEventHandler<BS, IS>, handler: impl IEventHandler<UI>,
listener: impl IEventListener, listener: impl IEventListener,
) -> Result<(), Error> { ) -> Result<(), Error> {
Self::enable()?; Self::enable()?;

View File

@ -35,48 +35,46 @@ impl From<musichoard::Error> for UiError {
} }
} }
pub enum Ui<BS: IUiBrowse<IS>, IS: IUiInfo<BS>> { pub enum UiState<BS, IS> {
Browse(BS), Browse(BS),
Info(IS), Info(IS),
} }
impl<BS: IUiBrowse<IS>, IS: IUiInfo<BS>> Ui<BS, IS> { impl<BS, IS> UiState<BS, IS> {
pub fn is_running(&self) -> bool { fn is_browse(&self) -> bool {
match self { matches!(self, UiState::Browse(_))
Ui::Browse(ref browse) => browse.is_running(),
Ui::Info(ref info) => info.is_running(),
}
} }
pub fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>) { fn is_info(&self) -> bool {
match self { matches!(self, UiState::Info(_))
Ui::Browse(ref mut browse) => browse.render_browse_frame(frame),
Ui::Info(ref mut info) => info.render_info_frame(frame),
}
} }
} }
pub trait IUiCore { pub trait IUi {
type BS: IUiBrowse;
type IS: IUiInfo;
fn is_running(&self) -> bool; fn is_running(&self) -> bool;
fn quit(&mut self); fn quit(&mut self);
fn save(&mut self) -> Result<(), UiError>; fn save(&mut self) -> Result<(), UiError>;
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>);
fn state(&mut self) -> UiState<&mut Self::BS, &mut Self::IS>;
} }
pub trait IUiBrowse<IS: IUiInfo<Self>>: IUiCore + Sized { pub trait IUiBrowse {
fn increment_category(&mut self); fn increment_category(&mut self);
fn decrement_category(&mut self); fn decrement_category(&mut self);
fn increment_selection(&mut self); fn increment_selection(&mut self);
fn decrement_selection(&mut self); fn decrement_selection(&mut self);
fn display_info_overlay(self) -> Ui<Self, IS>; fn show_info_overlay(&mut self);
fn render_browse_frame<B: Backend>(&mut self, frame: &mut Frame<'_, B>);
} }
pub trait IUiInfo<BS: IUiBrowse<Self>>: IUiCore + Sized { pub trait IUiInfo {
fn hide_info_overlay(self) -> Ui<BS, Self>; fn hide_info_overlay(&mut self);
fn render_info_frame<B: Backend>(&mut self, frame: &mut Frame<'_, B>);
} }
struct TrackSelection { struct TrackSelection {
@ -306,12 +304,6 @@ impl Selection {
} }
} }
pub struct UiImpl<MH> {
music_hoard: MH,
selection: Selection,
running: bool,
}
struct ArtistArea { struct ArtistArea {
list: Rect, list: Rect,
} }
@ -563,21 +555,23 @@ impl<'a, 'b> TrackState<'a, 'b> {
} }
} }
impl<MH: IMusicHoard> Ui<UiImpl<MH>, UiImpl<MH>> { pub struct Ui<MH> {
pub fn new(mh: MH) -> Result<Self, Error> { running: bool,
Ok(Ui::Browse(UiImpl::new(mh)?)) music_hoard: MH,
} selection: Selection,
state: UiState<(), ()>,
} }
impl<MH: IMusicHoard> UiImpl<MH> { impl<MH: IMusicHoard> Ui<MH> {
pub fn new(mut music_hoard: MH) -> Result<Self, Error> { pub fn new(mut music_hoard: MH) -> Result<Self, Error> {
music_hoard.load_from_database()?; music_hoard.load_from_database()?;
music_hoard.rescan_library()?; music_hoard.rescan_library()?;
let selection = Selection::new(Some(music_hoard.get_collection())); let selection = Selection::new(Some(music_hoard.get_collection()));
Ok(UiImpl { Ok(Ui {
running: true,
music_hoard, music_hoard,
selection, selection,
running: true, state: UiState::Browse(()),
}) })
} }
@ -724,7 +718,10 @@ impl<MH: IMusicHoard> UiImpl<MH> {
} }
} }
impl<MH: IMusicHoard> IUiCore for UiImpl<MH> { impl<MH: IMusicHoard> IUi for Ui<MH> {
type BS = Self;
type IS = Self;
fn is_running(&self) -> bool { fn is_running(&self) -> bool {
self.running self.running
} }
@ -734,12 +731,25 @@ impl<MH: IMusicHoard> IUiCore for UiImpl<MH> {
} }
fn save(&mut self) -> Result<(), UiError> { fn save(&mut self) -> Result<(), UiError> {
self.music_hoard.save_to_database()?; Ok(self.music_hoard.save_to_database()?)
Ok(()) }
fn state(&mut self) -> UiState<&mut Self::BS, &mut Self::IS> {
match self.state {
UiState::Browse(_) => UiState::Browse(self),
UiState::Info(_) => UiState::Info(self),
}
}
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
self.render_collection(frame);
if self.state.is_info() {
self.render_overlay(frame);
}
} }
} }
impl<MH: IMusicHoard> IUiBrowse<Self> for UiImpl<MH> { impl<MH: IMusicHoard> IUiBrowse for Ui<MH> {
fn increment_category(&mut self) { fn increment_category(&mut self) {
self.selection.increment_category(); self.selection.increment_category();
} }
@ -758,23 +768,16 @@ impl<MH: IMusicHoard> IUiBrowse<Self> for UiImpl<MH> {
.decrement_selection(self.music_hoard.get_collection()); .decrement_selection(self.music_hoard.get_collection());
} }
fn display_info_overlay(self) -> Ui<Self, Self> { fn show_info_overlay(&mut self) {
Ui::Info(self) assert!(self.state.is_browse());
} self.state = UiState::Info(());
fn render_browse_frame<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
self.render_collection(frame);
} }
} }
impl<MH: IMusicHoard> IUiInfo<Self> for UiImpl<MH> { impl<MH: IMusicHoard> IUiInfo for Ui<MH> {
fn hide_info_overlay(self) -> Ui<Self, Self> { fn hide_info_overlay(&mut self) {
Ui::Browse(self) assert!(self.state.is_info());
} self.state = UiState::Browse(());
fn render_info_frame<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
self.render_collection(frame);
self.render_overlay(frame);
} }
} }
@ -1218,10 +1221,12 @@ mod tests {
fn overlay() { fn overlay() {
let mut terminal = terminal(); let mut terminal = terminal();
let mut ui = ui(COLLECTION.to_owned()); let mut ui = ui(COLLECTION.to_owned());
assert!(ui.state().is_browse());
terminal.draw(|frame| ui.render(frame)).unwrap(); terminal.draw(|frame| ui.render(frame)).unwrap();
ui.toggle_overlay(); ui.show_info_overlay();
assert!(ui.state().is_info());
terminal.draw(|frame| ui.render(frame)).unwrap(); terminal.draw(|frame| ui.render(frame)).unwrap();
@ -1230,7 +1235,8 @@ mod tests {
terminal.draw(|frame| ui.render(frame)).unwrap(); terminal.draw(|frame| ui.render(frame)).unwrap();
ui.toggle_overlay(); ui.hide_info_overlay();
assert!(ui.state().is_browse());
terminal.draw(|frame| ui.render(frame)).unwrap(); terminal.draw(|frame| ui.render(frame)).unwrap();
} }