Input no longer a state

This commit is contained in:
Wojciech Kozlowski 2024-09-14 20:55:17 +02:00
parent 09e1cb7fda
commit 8acf208968
16 changed files with 172 additions and 220 deletions

View File

@ -11,6 +11,7 @@ impl AppMachine<BrowseState> {
AppMachine { AppMachine {
inner, inner,
state: BrowseState, state: BrowseState,
input: None,
} }
} }
} }
@ -26,6 +27,7 @@ impl<'a> From<&'a mut AppMachine<BrowseState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Browse(()), state: AppState::Browse(()),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }

View File

@ -14,6 +14,7 @@ impl AppMachine<CriticalState> {
state: CriticalState { state: CriticalState {
string: string.into(), string: string.into(),
}, },
input: None
} }
} }
} }
@ -29,6 +30,7 @@ impl<'a> From<&'a mut AppMachine<CriticalState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Critical(&machine.state.string), state: AppState::Critical(&machine.state.string),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }

View File

@ -14,6 +14,7 @@ impl AppMachine<ErrorState> {
state: ErrorState { state: ErrorState {
string: string.into(), string: string.into(),
}, },
input: None,
} }
} }
} }
@ -29,6 +30,7 @@ impl<'a> From<&'a mut AppMachine<ErrorState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Error(&machine.state.string), state: AppState::Error(&machine.state.string),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }

View File

@ -40,7 +40,11 @@ pub type FetchReceiver = mpsc::Receiver<FetchResult>;
impl AppMachine<FetchState> { impl AppMachine<FetchState> {
fn fetch_state(inner: AppInner, state: FetchState) -> Self { fn fetch_state(inner: AppInner, state: FetchState) -> Self {
AppMachine { inner, state } AppMachine {
inner,
state,
input: None,
}
} }
pub fn app_fetch_new(inner: AppInner) -> App { pub fn app_fetch_new(inner: AppInner) -> App {
@ -173,6 +177,7 @@ impl<'a> From<&'a mut AppMachine<FetchState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Fetch(()), state: AppState::Fetch(()),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }

View File

@ -10,6 +10,7 @@ impl AppMachine<InfoState> {
AppMachine { AppMachine {
inner, inner,
state: InfoState, state: InfoState,
input: None,
} }
} }
} }
@ -25,6 +26,7 @@ impl<'a> From<&'a mut AppMachine<InfoState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Info(()), state: AppState::Info(()),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }

View File

@ -0,0 +1,44 @@
use tui_input::backend::crossterm::EventHandler;
use crate::tui::app::{
machine::{App, AppMachine},
IAppInput, InputEvent, InputPublic,
};
#[derive(Default)]
pub struct Input(tui_input::Input);
impl<'app> From<&'app Input> for InputPublic<'app> {
fn from(value: &'app Input) -> Self {
&value.0
}
}
impl<State> IAppInput for AppMachine<State>
where
AppMachine<State>: Into<App>,
{
type APP = App;
fn taking_input(&self) -> bool {
self.input.is_some()
}
fn input(mut self, input: InputEvent) -> Self::APP {
self.input
.as_mut()
.unwrap() // FIXME
.0
.handle_event(&crossterm::event::Event::Key(input));
self.into()
}
fn confirm(self) -> Self::APP {
self.cancel()
}
fn cancel(mut self) -> Self::APP {
self.input = None;
self.into()
}
}

View File

@ -1,78 +0,0 @@
use tui_input::{backend::crossterm::EventHandler, Input};
use crate::tui::app::{
machine::{App, AppInner, AppMachine},
AppPublic, AppState, IAppInteractInput, InputClientPublic, InputEvent, InputStatePublic,
};
use super::match_state::MatchState;
pub struct InputState {
input: Input,
client: InputClient,
}
pub enum InputClient {
Match(MatchState),
}
impl AppMachine<InputState> {
pub fn input_state(inner: AppInner, client: InputClient) -> Self {
AppMachine {
inner,
state: InputState {
input: Input::default(),
client,
},
}
}
}
impl From<AppMachine<InputState>> for App {
fn from(machine: AppMachine<InputState>) -> Self {
AppState::Input(machine)
}
}
impl<'a> From<&'a mut InputClient> for InputClientPublic<'a> {
fn from(client: &'a mut InputClient) -> Self {
match client {
InputClient::Match(state) => InputClientPublic::Match(state.into()),
}
}
}
impl<'a> From<&'a mut AppMachine<InputState>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<InputState>) -> Self {
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Input(InputStatePublic {
input: &machine.state.input,
client: (&mut machine.state.client).into(),
}),
}
}
}
impl IAppInteractInput for AppMachine<InputState> {
type APP = App;
fn input(mut self, input: InputEvent) -> Self::APP {
self.state
.input
.handle_event(&crossterm::event::Event::Key(input));
self.into()
}
fn confirm(self) -> Self::APP {
match self.state.client {
InputClient::Match(state) => AppMachine::match_state(self.inner, state).into(),
}
}
fn cancel(self) -> Self::APP {
match self.state.client {
InputClient::Match(state) => AppMachine::match_state(self.inner, state).into(),
}
}
}

View File

@ -6,7 +6,7 @@ use crate::tui::app::{
MatchStateInfo, MatchStatePublic, WidgetState, MatchStateInfo, MatchStatePublic, WidgetState,
}; };
use super::{fetch_state::FetchState, input_state::InputClient}; use super::{fetch_state::FetchState, input::Input};
impl ArtistMatches { impl ArtistMatches {
fn len(&self) -> usize { fn len(&self) -> usize {
@ -98,7 +98,11 @@ impl MatchState {
impl AppMachine<MatchState> { impl AppMachine<MatchState> {
pub fn match_state(inner: AppInner, state: MatchState) -> Self { pub fn match_state(inner: AppInner, state: MatchState) -> Self {
AppMachine { inner, state } AppMachine {
inner,
state,
input: None,
}
} }
} }
@ -122,6 +126,7 @@ impl<'a> From<&'a mut AppMachine<MatchState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Match((&mut machine.state).into()), state: AppState::Match((&mut machine.state).into()),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }
@ -152,7 +157,7 @@ impl IAppInteractMatch for AppMachine<MatchState> {
self.into() self.into()
} }
fn select(self) -> Self::APP { fn select(mut self) -> Self::APP {
if let Some(index) = self.state.state.list.selected() { if let Some(index) = self.state.state.list.selected() {
// selected() implies current exists // selected() implies current exists
if self if self
@ -162,7 +167,8 @@ impl IAppInteractMatch for AppMachine<MatchState> {
.unwrap() .unwrap()
.is_manual_input_mbid(index) .is_manual_input_mbid(index)
{ {
return AppMachine::input_state(self.inner, InputClient::Match(self.state)).into(); self.input = Some(Input::default());
return self.into();
} }
} }
AppMachine::app_fetch_next(self.inner, self.state.fetch) AppMachine::app_fetch_next(self.inner, self.state.fetch)

View File

@ -3,7 +3,7 @@ mod critical_state;
mod error_state; mod error_state;
mod fetch_state; mod fetch_state;
mod info_state; mod info_state;
mod input_state; mod input;
mod match_state; mod match_state;
mod reload_state; mod reload_state;
mod search_state; mod search_state;
@ -21,11 +21,12 @@ use critical_state::CriticalState;
use error_state::ErrorState; use error_state::ErrorState;
use fetch_state::FetchState; use fetch_state::FetchState;
use info_state::InfoState; use info_state::InfoState;
use input_state::InputState;
use match_state::MatchState; use match_state::MatchState;
use reload_state::ReloadState; use reload_state::ReloadState;
use search_state::SearchState; use search_state::SearchState;
use input::Input;
use super::IAppBase; use super::IAppBase;
pub type App = AppState< pub type App = AppState<
@ -35,7 +36,6 @@ pub type App = AppState<
AppMachine<SearchState>, AppMachine<SearchState>,
AppMachine<FetchState>, AppMachine<FetchState>,
AppMachine<MatchState>, AppMachine<MatchState>,
AppMachine<InputState>,
AppMachine<ErrorState>, AppMachine<ErrorState>,
AppMachine<CriticalState>, AppMachine<CriticalState>,
>; >;
@ -43,6 +43,7 @@ pub type App = AppState<
pub struct AppMachine<STATE> { pub struct AppMachine<STATE> {
inner: AppInner, inner: AppInner,
state: STATE, state: STATE,
input: Option<Input>,
} }
pub struct AppInner { pub struct AppInner {
@ -80,7 +81,6 @@ impl App {
AppState::Search(search_state) => &search_state.inner, AppState::Search(search_state) => &search_state.inner,
AppState::Fetch(fetch_state) => &fetch_state.inner, AppState::Fetch(fetch_state) => &fetch_state.inner,
AppState::Match(match_state) => &match_state.inner, AppState::Match(match_state) => &match_state.inner,
AppState::Input(input_state) => &input_state.inner,
AppState::Error(error_state) => &error_state.inner, AppState::Error(error_state) => &error_state.inner,
AppState::Critical(critical_state) => &critical_state.inner, AppState::Critical(critical_state) => &critical_state.inner,
} }
@ -94,7 +94,6 @@ impl App {
AppState::Search(search_state) => &mut search_state.inner, AppState::Search(search_state) => &mut search_state.inner,
AppState::Fetch(fetch_state) => &mut fetch_state.inner, AppState::Fetch(fetch_state) => &mut fetch_state.inner,
AppState::Match(match_state) => &mut match_state.inner, AppState::Match(match_state) => &mut match_state.inner,
AppState::Input(input_state) => &mut input_state.inner,
AppState::Error(error_state) => &mut error_state.inner, AppState::Error(error_state) => &mut error_state.inner,
AppState::Critical(critical_state) => &mut critical_state.inner, AppState::Critical(critical_state) => &mut critical_state.inner,
} }
@ -108,7 +107,6 @@ impl IApp for App {
type SearchState = AppMachine<SearchState>; type SearchState = AppMachine<SearchState>;
type FetchState = AppMachine<FetchState>; type FetchState = AppMachine<FetchState>;
type MatchState = AppMachine<MatchState>; type MatchState = AppMachine<MatchState>;
type InputState = AppMachine<InputState>;
type ErrorState = AppMachine<ErrorState>; type ErrorState = AppMachine<ErrorState>;
type CriticalState = AppMachine<CriticalState>; type CriticalState = AppMachine<CriticalState>;
@ -130,7 +128,6 @@ impl IApp for App {
Self::SearchState, Self::SearchState,
Self::FetchState, Self::FetchState,
Self::MatchState, Self::MatchState,
Self::InputState,
Self::ErrorState, Self::ErrorState,
Self::CriticalState, Self::CriticalState,
> { > {
@ -155,7 +152,6 @@ impl IAppAccess for App {
AppState::Search(state) => state.into(), AppState::Search(state) => state.into(),
AppState::Fetch(state) => state.into(), AppState::Fetch(state) => state.into(),
AppState::Match(state) => state.into(), AppState::Match(state) => state.into(),
AppState::Input(state) => state.into(),
AppState::Error(state) => state.into(), AppState::Error(state) => state.into(),
AppState::Critical(state) => state.into(), AppState::Critical(state) => state.into(),
} }
@ -192,7 +188,6 @@ impl<'a> From<&'a mut AppInner> for AppPublicInner<'a> {
mod tests { mod tests {
use std::sync::mpsc; use std::sync::mpsc;
use input_state::InputClient;
use musichoard::collection::Collection; use musichoard::collection::Collection;
use crate::tui::{ use crate::tui::{
@ -210,7 +205,6 @@ mod tests {
SearchState, SearchState,
FetchState, FetchState,
MatchState, MatchState,
InputState,
ErrorState, ErrorState,
CriticalState, CriticalState,
> >
@ -221,7 +215,6 @@ mod tests {
SearchState, SearchState,
FetchState, FetchState,
MatchState, MatchState,
InputState,
ErrorState, ErrorState,
CriticalState, CriticalState,
> >
@ -415,7 +408,12 @@ mod tests {
let (_, rx) = mpsc::channel(); let (_, rx) = mpsc::channel();
let inner = app.unwrap_browse().inner; let inner = app.unwrap_browse().inner;
let state = FetchState::new(rx); let state = FetchState::new(rx);
app = AppMachine { inner, state }.into(); app = AppMachine {
inner,
state,
input: None,
}
.into();
let state = app.state(); let state = app.state();
assert!(matches!(state, AppState::Fetch(_))); assert!(matches!(state, AppState::Fetch(_)));
@ -459,33 +457,6 @@ mod tests {
assert!(!app.is_running()); assert!(!app.is_running());
} }
#[test]
fn state_input() {
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
assert!(app.is_running());
let (_, rx) = mpsc::channel();
let fetch = FetchState::new(rx);
let match_state = MatchState::new(None, fetch);
let input_client = InputClient::Match(match_state);
app = AppMachine::input_state(app.unwrap_browse().inner, input_client).into();
let state = app.state();
assert!(matches!(state, AppState::Input(_)));
app = state;
app = app.no_op();
let state = app.state();
assert!(matches!(state, AppState::Input(_)));
app = state;
let public = app.get();
assert!(matches!(public.state, AppState::Input(_)));
app = app.force_quit();
assert!(!app.is_running());
}
#[test] #[test]
fn state_error() { fn state_error() {
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());

View File

@ -11,6 +11,7 @@ impl AppMachine<ReloadState> {
AppMachine { AppMachine {
inner, inner,
state: ReloadState, state: ReloadState,
input: None,
} }
} }
} }
@ -25,6 +26,7 @@ impl<'a> From<&'a mut AppMachine<ReloadState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Reload(()), state: AppState::Reload(()),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }

View File

@ -40,6 +40,7 @@ impl AppMachine<SearchState> {
orig, orig,
memo: vec![], memo: vec![],
}, },
input: None,
} }
} }
} }
@ -55,6 +56,7 @@ impl<'a> From<&'a mut AppMachine<SearchState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Search(&machine.state.string), state: AppState::Search(&machine.state.string),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }

View File

@ -15,7 +15,6 @@ pub enum AppState<
SearchState, SearchState,
FetchState, FetchState,
MatchState, MatchState,
InputState,
ErrorState, ErrorState,
CriticalState, CriticalState,
> { > {
@ -25,7 +24,6 @@ pub enum AppState<
Search(SearchState), Search(SearchState),
Fetch(FetchState), Fetch(FetchState),
Match(MatchState), Match(MatchState),
Input(InputState),
Error(ErrorState), Error(ErrorState),
Critical(CriticalState), Critical(CriticalState),
} }
@ -38,8 +36,7 @@ pub trait IApp {
type FetchState: IAppBase<APP = Self> type FetchState: IAppBase<APP = Self>
+ IAppInteractFetch<APP = Self> + IAppInteractFetch<APP = Self>
+ IAppEventFetch<APP = Self>; + IAppEventFetch<APP = Self>;
type MatchState: IAppBase<APP = Self> + IAppInteractMatch<APP = Self>; type MatchState: IAppBase<APP = Self> + IAppInteractMatch<APP = Self> + IAppInput<APP = Self>;
type InputState: IAppBase<APP = Self> + IAppInteractInput<APP = Self>;
type ErrorState: IAppBase<APP = Self> + IAppInteractError<APP = Self>; type ErrorState: IAppBase<APP = Self> + IAppInteractError<APP = Self>;
type CriticalState: IAppBase<APP = Self>; type CriticalState: IAppBase<APP = Self>;
@ -56,7 +53,6 @@ pub trait IApp {
Self::SearchState, Self::SearchState,
Self::FetchState, Self::FetchState,
Self::MatchState, Self::MatchState,
Self::InputState,
Self::ErrorState, Self::ErrorState,
Self::CriticalState, Self::CriticalState,
>; >;
@ -134,9 +130,11 @@ pub trait IAppInteractMatch {
} }
type InputEvent = crossterm::event::KeyEvent; type InputEvent = crossterm::event::KeyEvent;
pub trait IAppInteractInput { pub trait IAppInput {
type APP: IApp; type APP: IApp;
fn taking_input(&self) -> bool;
fn input(self, input: InputEvent) -> Self::APP; fn input(self, input: InputEvent) -> Self::APP;
fn confirm(self) -> Self::APP; fn confirm(self) -> Self::APP;
fn cancel(self) -> Self::APP; fn cancel(self) -> Self::APP;
@ -159,6 +157,7 @@ pub trait IAppAccess {
pub struct AppPublic<'app> { pub struct AppPublic<'app> {
pub inner: AppPublicInner<'app>, pub inner: AppPublicInner<'app>,
pub state: AppPublicState<'app>, pub state: AppPublicState<'app>,
pub input: AppPublicInput<'app>,
} }
pub struct AppPublicInner<'app> { pub struct AppPublicInner<'app> {
@ -214,27 +213,8 @@ pub struct MatchStatePublic<'app> {
pub state: &'app mut WidgetState, pub state: &'app mut WidgetState,
} }
pub enum InputClientPublic<'app> { pub type AppPublicState<'app> =
Match(MatchStatePublic<'app>), AppState<(), (), (), &'app str, (), MatchStatePublic<'app>, &'app str, &'app str>;
}
pub type Input = tui_input::Input;
pub struct InputStatePublic<'app> {
pub input: &'app Input,
pub client: InputClientPublic<'app>
}
pub type AppPublicState<'app> = AppState<
(),
(),
(),
&'app str,
(),
MatchStatePublic<'app>,
InputStatePublic<'app>,
&'app str,
&'app str,
>;
impl< impl<
BrowseState, BrowseState,
@ -243,7 +223,6 @@ impl<
SearchState, SearchState,
FetchState, FetchState,
MatchState, MatchState,
InputState,
ErrorState, ErrorState,
CriticalState, CriticalState,
> >
@ -254,7 +233,6 @@ impl<
SearchState, SearchState,
FetchState, FetchState,
MatchState, MatchState,
InputState,
ErrorState, ErrorState,
CriticalState, CriticalState,
> >
@ -264,6 +242,9 @@ impl<
} }
} }
pub type InputPublic<'app> = &'app tui_input::Input;
pub type AppPublicInput<'app> = Option<InputPublic<'app>>;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -11,7 +11,7 @@ use crate::tui::{
event::{Event, EventError, EventReceiver}, event::{Event, EventError, EventReceiver},
}; };
use super::app::{IAppBase, IAppEventFetch, IAppInteractInput}; use super::app::{IAppBase, IAppEventFetch, IAppInput};
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IEventHandler<APP: IApp> { pub trait IEventHandler<APP: IApp> {
@ -26,11 +26,12 @@ trait IEventHandlerPrivate<APP: IApp> {
fn handle_search_key_event(app: <APP as IApp>::SearchState, key_event: KeyEvent) -> APP; fn handle_search_key_event(app: <APP as IApp>::SearchState, key_event: KeyEvent) -> APP;
fn handle_fetch_key_event(app: <APP as IApp>::FetchState, key_event: KeyEvent) -> APP; fn handle_fetch_key_event(app: <APP as IApp>::FetchState, key_event: KeyEvent) -> APP;
fn handle_match_key_event(app: <APP as IApp>::MatchState, key_event: KeyEvent) -> APP; fn handle_match_key_event(app: <APP as IApp>::MatchState, key_event: KeyEvent) -> APP;
fn handle_input_key_event(app: <APP as IApp>::InputState, key_event: KeyEvent) -> APP;
fn handle_error_key_event(app: <APP as IApp>::ErrorState, key_event: KeyEvent) -> APP; fn handle_error_key_event(app: <APP as IApp>::ErrorState, key_event: KeyEvent) -> APP;
fn handle_critical_key_event(app: <APP as IApp>::CriticalState, key_event: KeyEvent) -> APP; fn handle_critical_key_event(app: <APP as IApp>::CriticalState, key_event: KeyEvent) -> APP;
fn handle_fetch_result_ready_event(app: APP) -> APP; fn handle_fetch_result_ready_event(app: APP) -> APP;
fn handle_input_key_event<Input: IAppInput<APP = APP>>(app: Input, key_event: KeyEvent) -> APP;
} }
pub struct EventHandler { pub struct EventHandler {
@ -75,8 +76,13 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
Self::handle_search_key_event(search_state, key_event) Self::handle_search_key_event(search_state, key_event)
} }
AppState::Fetch(fetch_state) => Self::handle_fetch_key_event(fetch_state, key_event), AppState::Fetch(fetch_state) => Self::handle_fetch_key_event(fetch_state, key_event),
AppState::Match(match_state) => Self::handle_match_key_event(match_state, key_event), AppState::Match(match_state) => {
AppState::Input(input_state) => Self::handle_input_key_event(input_state, key_event), if match_state.taking_input() {
Self::handle_input_key_event(match_state, key_event)
} else {
Self::handle_match_key_event(match_state, key_event)
}
}
AppState::Error(error_state) => Self::handle_error_key_event(error_state, key_event), AppState::Error(error_state) => Self::handle_error_key_event(error_state, key_event),
AppState::Critical(critical_state) => { AppState::Critical(critical_state) => {
Self::handle_critical_key_event(critical_state, key_event) Self::handle_critical_key_event(critical_state, key_event)
@ -92,7 +98,6 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
AppState::Search(state) => state.no_op(), AppState::Search(state) => state.no_op(),
AppState::Fetch(fetch_state) => fetch_state.fetch_result_ready(), AppState::Fetch(fetch_state) => fetch_state.fetch_result_ready(),
AppState::Match(state) => state.no_op(), AppState::Match(state) => state.no_op(),
AppState::Input(state) => state.no_op(),
AppState::Error(state) => state.no_op(), AppState::Error(state) => state.no_op(),
AppState::Critical(state) => state.no_op(), AppState::Critical(state) => state.no_op(),
} }
@ -178,6 +183,13 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
} }
fn handle_fetch_key_event(app: <APP as IApp>::FetchState, key_event: KeyEvent) -> APP { fn handle_fetch_key_event(app: <APP as IApp>::FetchState, key_event: KeyEvent) -> APP {
if key_event.modifiers == KeyModifiers::CONTROL {
return match key_event.code {
KeyCode::Char('g') | KeyCode::Char('G') => app.abort(),
_ => app.no_op(),
}
}
match key_event.code { match key_event.code {
// Abort. // Abort.
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.abort(), KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.abort(),
@ -187,6 +199,13 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
} }
fn handle_match_key_event(app: <APP as IApp>::MatchState, key_event: KeyEvent) -> APP { fn handle_match_key_event(app: <APP as IApp>::MatchState, key_event: KeyEvent) -> APP {
if key_event.modifiers == KeyModifiers::CONTROL {
return match key_event.code {
KeyCode::Char('g') | KeyCode::Char('G') => app.abort(),
_ => app.no_op(),
}
}
match key_event.code { match key_event.code {
// Abort. // Abort.
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.abort(), KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.abort(),
@ -199,22 +218,6 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
} }
} }
fn handle_input_key_event(app: <APP as IApp>::InputState, key_event: KeyEvent) -> APP {
if key_event.modifiers == KeyModifiers::CONTROL {
match key_event.code {
KeyCode::Char('g') | KeyCode::Char('G') => return app.cancel(),
_ => {},
};
}
match key_event.code {
// Return.
KeyCode::Esc | KeyCode::Enter => app.confirm(),
// Othey keys.
_ => app.input(key_event),
}
}
fn handle_error_key_event(app: <APP as IApp>::ErrorState, _key_event: KeyEvent) -> APP { fn handle_error_key_event(app: <APP as IApp>::ErrorState, _key_event: KeyEvent) -> APP {
// Any key dismisses the error. // Any key dismisses the error.
app.dismiss_error() app.dismiss_error()
@ -224,5 +227,22 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
// No action is allowed. // No action is allowed.
app.no_op() app.no_op()
} }
fn handle_input_key_event<Input: IAppInput<APP = APP>>(app: Input, key_event: KeyEvent) -> APP {
if key_event.modifiers == KeyModifiers::CONTROL {
match key_event.code {
KeyCode::Char('g') | KeyCode::Char('G') => return app.cancel(),
_ => {}
};
}
match key_event.code {
// Return.
KeyCode::Esc => app.cancel(),
KeyCode::Enter => app.confirm(),
// Othey keys.
_ => app.input(key_event),
}
}
} }
// GRCOV_EXCL_STOP // GRCOV_EXCL_STOP

View File

@ -28,10 +28,14 @@ pub struct TrackArea {
pub info: Rect, pub info: Rect,
} }
pub struct FrameArea { pub struct BrowseArea {
pub artist: ArtistArea, pub artist: ArtistArea,
pub album: AlbumArea, pub album: AlbumArea,
pub track: TrackArea, pub track: TrackArea,
}
pub struct FrameArea {
pub browse: BrowseArea,
pub minibuffer: Rect, pub minibuffer: Rect,
} }
@ -91,14 +95,16 @@ impl FrameArea {
}; };
FrameArea { FrameArea {
artist: ArtistArea { list: artist_list }, browse: BrowseArea {
album: AlbumArea { artist: ArtistArea { list: artist_list },
list: album_list, album: AlbumArea {
info: album_info, list: album_list,
}, info: album_info,
track: TrackArea { },
list: track_list, track: TrackArea {
info: track_info, list: track_list,
info: track_info,
},
}, },
minibuffer, minibuffer,
} }

View File

@ -57,20 +57,16 @@ impl Minibuffer<'_> {
columns, columns,
}, },
AppState::Fetch(()) => Minibuffer { AppState::Fetch(()) => Minibuffer {
paragraphs: vec![Paragraph::new("fetching..."), Paragraph::new("q: abort")], paragraphs: vec![
Paragraph::new("fetching..."),
Paragraph::new("ctrl+g: abort"),
],
columns: 2, columns: 2,
}, },
AppState::Match(public) => Minibuffer { AppState::Match(public) => Minibuffer {
paragraphs: vec![ paragraphs: vec![
Paragraph::new(UiDisplay::display_matching_info(public.info)), Paragraph::new(UiDisplay::display_matching_info(public.info)),
Paragraph::new("q: abort"), Paragraph::new("ctrl+g: abort"),
],
columns: 2,
},
AppState::Input(_) => Minibuffer {
paragraphs: vec![
Paragraph::new("enter: confirm"),
Paragraph::new("ctrl+g: cancel"),
], ],
columns: 2, columns: 2,
}, },

View File

@ -10,6 +10,7 @@ mod reload_state;
mod style; mod style;
mod widgets; mod widgets;
use browse_state::BrowseArea;
use ratatui::{layout::Rect, widgets::Paragraph, Frame}; use ratatui::{layout::Rect, widgets::Paragraph, Frame};
use musichoard::collection::{album::Album, Collection}; use musichoard::collection::{album::Album, Collection};
@ -32,7 +33,7 @@ use crate::tui::{
}, },
}; };
use super::app::{Input, InputClientPublic}; use super::app::InputPublic;
pub trait IUi { pub trait IUi {
fn render<APP: IAppAccess>(app: &mut APP, frame: &mut Frame); fn render<APP: IAppAccess>(app: &mut APP, frame: &mut Frame);
@ -66,11 +67,10 @@ impl Ui {
fn render_browse_frame( fn render_browse_frame(
artists: &Collection, artists: &Collection,
selection: &mut Selection, selection: &mut Selection,
state: &AppPublicState, areas: BrowseArea,
frame: &mut Frame, frame: &mut Frame,
) { ) {
let active = selection.category(); let active = selection.category();
let areas = FrameArea::new(frame.area());
let artist_state = ArtistState::new( let artist_state = ArtistState::new(
active == Category::Artist, active == Category::Artist,
@ -103,8 +103,6 @@ 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(artists: &Collection, selection: &mut Selection, frame: &mut Frame) { fn render_info_overlay(artists: &Collection, selection: &mut Selection, frame: &mut Frame) {
@ -149,7 +147,7 @@ impl Ui {
UiWidget::render_overlay_list_widget(&st.matching, st.list, st.state, true, area, frame) UiWidget::render_overlay_list_widget(&st.matching, st.list, st.state, true, area, frame)
} }
fn render_input_overlay(input: &Input, frame: &mut Frame) { fn render_input_overlay(input: InputPublic, frame: &mut Frame) {
let area = OverlayBuilder::default() let area = OverlayBuilder::default()
.with_width(OverlaySize::MarginFactor(4)) .with_width(OverlaySize::MarginFactor(4))
.with_height(OverlaySize::Value(3)) .with_height(OverlaySize::Value(3))
@ -182,24 +180,24 @@ impl IUi for Ui {
let selection = app.inner.selection; let selection = app.inner.selection;
let state = app.state; let state = app.state;
Self::render_browse_frame(collection, selection, &state, frame); let areas = FrameArea::new(frame.area());
Self::render_browse_frame(collection, selection, areas.browse, frame);
Self::render_minibuffer(&state, areas.minibuffer, frame);
match 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::Fetch(()) => Self::render_fetch_overlay(frame), AppState::Fetch(()) => Self::render_fetch_overlay(frame),
AppState::Match(public) => Self::render_match_overlay(public.info, public.state, frame), AppState::Match(public) => Self::render_match_overlay(public.info, public.state, frame),
AppState::Input(input) => {
match input.client {
InputClientPublic::Match(public) => {
Self::render_match_overlay(public.info, public.state, frame)
}
}
Self::render_input_overlay(input.input, frame);
}
AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame), AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame),
AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame), AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame),
_ => {} _ => {}
} }
if let Some(input) = app.input {
Self::render_input_overlay(input, frame);
}
} }
} }
@ -211,10 +209,7 @@ mod tests {
}; };
use crate::tui::{ use crate::tui::{
app::{ app::{AppPublic, AppPublicInner, Delta, MatchOption, MatchStatePublic},
AppPublic, AppPublicInner, Delta, InputClientPublic, InputStatePublic, MatchOption,
MatchStatePublic,
},
lib::interface::musicbrainz::Match, lib::interface::musicbrainz::Match,
testmod::COLLECTION, testmod::COLLECTION,
tests::terminal, tests::terminal,
@ -240,20 +235,10 @@ mod tests {
info: m.info, info: m.info,
state: m.state, state: m.state,
}), }),
AppState::Input(ref mut i) => AppState::Input(InputStatePublic {
input: i.input,
client: match i.client {
InputClientPublic::Match(ref mut m) => {
InputClientPublic::Match(MatchStatePublic {
info: m.info,
state: m.state,
})
}
},
}),
AppState::Error(s) => AppState::Error(s), AppState::Error(s) => AppState::Error(s),
AppState::Critical(s) => AppState::Critical(s), AppState::Critical(s) => AppState::Critical(s),
}, },
input: self.input,
} }
} }
} }
@ -286,6 +271,7 @@ mod tests {
let mut app = AppPublic { let mut app = AppPublic {
inner: public_inner(collection, selection), inner: public_inner(collection, selection),
state: AppState::Browse(()), state: AppState::Browse(()),
input: None,
}; };
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
@ -364,6 +350,7 @@ mod tests {
info: None, info: None,
state: &mut widget_state, state: &mut widget_state,
}), }),
input: None,
}; };
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
} }
@ -393,6 +380,7 @@ mod tests {
info: Some(&artist_matches), info: Some(&artist_matches),
state: &mut widget_state, state: &mut widget_state,
}), }),
input: None,
}; };
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
} }
@ -427,6 +415,7 @@ mod tests {
info: Some(&album_matches), info: Some(&album_matches),
state: &mut widget_state, state: &mut widget_state,
}), }),
input: None,
}; };
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
} }