Add manual input elements to the app an ui #216
@ -2,7 +2,7 @@ use tui_input::{backend::crossterm::EventHandler, Input};
|
|||||||
|
|
||||||
use crate::tui::app::{
|
use crate::tui::app::{
|
||||||
machine::{App, AppInner, AppMachine},
|
machine::{App, AppInner, AppMachine},
|
||||||
AppPublic, AppState, IAppInteractInput, InputEvent,
|
AppPublic, AppState, IAppInteractInput, InputClientPublic, InputEvent, InputStatePublic,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::match_state::MatchState;
|
use super::match_state::MatchState;
|
||||||
@ -34,11 +34,22 @@ impl From<AppMachine<InputState>> for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
impl<'a> From<&'a mut AppMachine<InputState>> for AppPublic<'a> {
|
||||||
fn from(machine: &'a mut AppMachine<InputState>) -> Self {
|
fn from(machine: &'a mut AppMachine<InputState>) -> Self {
|
||||||
AppPublic {
|
AppPublic {
|
||||||
inner: (&mut machine.inner).into(),
|
inner: (&mut machine.inner).into(),
|
||||||
state: AppState::Input(&machine.state.input),
|
state: AppState::Input(InputStatePublic {
|
||||||
|
input: &machine.state.input,
|
||||||
|
client: (&mut machine.state.client).into(),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,19 +58,21 @@ impl IAppInteractInput for AppMachine<InputState> {
|
|||||||
type APP = App;
|
type APP = App;
|
||||||
|
|
||||||
fn input(mut self, input: InputEvent) -> Self::APP {
|
fn input(mut self, input: InputEvent) -> Self::APP {
|
||||||
self.state.input.handle_event(&crossterm::event::Event::Key(input));
|
self.state
|
||||||
|
.input
|
||||||
|
.handle_event(&crossterm::event::Event::Key(input));
|
||||||
self.into()
|
self.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(self) -> Self::APP {
|
fn confirm(self) -> Self::APP {
|
||||||
match self.state.client {
|
match self.state.client {
|
||||||
InputClient::Match(state) => AppMachine::match_state(self.inner, state).into()
|
InputClient::Match(state) => AppMachine::match_state(self.inner, state).into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cancel(self) -> Self::APP {
|
fn cancel(self) -> Self::APP {
|
||||||
match self.state.client {
|
match self.state.client {
|
||||||
InputClient::Match(state) => AppMachine::match_state(self.inner, state).into()
|
InputClient::Match(state) => AppMachine::match_state(self.inner, state).into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,14 +108,20 @@ impl From<AppMachine<MatchState>> for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a mut MatchState> for MatchStatePublic<'a> {
|
||||||
|
fn from(state: &'a mut MatchState) -> Self {
|
||||||
|
MatchStatePublic {
|
||||||
|
info: state.current.as_ref().map(Into::into),
|
||||||
|
state: &mut state.state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a mut AppMachine<MatchState>> for AppPublic<'a> {
|
impl<'a> From<&'a mut AppMachine<MatchState>> for AppPublic<'a> {
|
||||||
fn from(machine: &'a mut AppMachine<MatchState>) -> Self {
|
fn from(machine: &'a mut AppMachine<MatchState>) -> Self {
|
||||||
AppPublic {
|
AppPublic {
|
||||||
inner: (&mut machine.inner).into(),
|
inner: (&mut machine.inner).into(),
|
||||||
state: AppState::Match(MatchStatePublic {
|
state: AppState::Match((&mut machine.state).into()),
|
||||||
info: machine.state.current.as_ref().map(Into::into),
|
|
||||||
state: &mut machine.state.state,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,8 +155,14 @@ impl IAppInteractMatch for AppMachine<MatchState> {
|
|||||||
fn select(self) -> Self::APP {
|
fn select(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.state.current.as_ref().unwrap().is_manual_input_mbid(index) {
|
if self
|
||||||
return AppMachine::input_state(self.inner, InputClient::Match(self.state)).into()
|
.state
|
||||||
|
.current
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.is_manual_input_mbid(index)
|
||||||
|
{
|
||||||
|
return AppMachine::input_state(self.inner, InputClient::Match(self.state)).into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppMachine::app_fetch_next(self.inner, self.state.fetch)
|
AppMachine::app_fetch_next(self.inner, self.state.fetch)
|
||||||
|
@ -192,6 +192,7 @@ 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::{
|
||||||
@ -267,13 +268,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unwrap_input(self) -> InputState {
|
|
||||||
match self {
|
|
||||||
AppState::Input(input) => input,
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unwrap_error(self) -> ErrorState {
|
pub fn unwrap_error(self) -> ErrorState {
|
||||||
match self {
|
match self {
|
||||||
AppState::Error(error) => error,
|
AppState::Error(error) => error,
|
||||||
@ -440,7 +434,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn state_matches() {
|
fn state_match() {
|
||||||
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
|
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
@ -465,6 +459,33 @@ 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());
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
mod machine;
|
mod machine;
|
||||||
mod selection;
|
mod selection;
|
||||||
|
|
||||||
use crossterm::event::KeyEvent;
|
|
||||||
pub use machine::App;
|
pub use machine::App;
|
||||||
pub use selection::{Category, Delta, Selection, WidgetState};
|
pub use selection::{Category, Delta, Selection, WidgetState};
|
||||||
|
|
||||||
use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, Collection};
|
use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, Collection};
|
||||||
use tui_input::Input;
|
|
||||||
|
|
||||||
use crate::tui::lib::interface::musicbrainz::Match;
|
use crate::tui::lib::interface::musicbrainz::Match;
|
||||||
|
|
||||||
@ -135,7 +133,7 @@ pub trait IAppInteractMatch {
|
|||||||
fn abort(self) -> Self::APP;
|
fn abort(self) -> Self::APP;
|
||||||
}
|
}
|
||||||
|
|
||||||
type InputEvent = KeyEvent;
|
type InputEvent = crossterm::event::KeyEvent;
|
||||||
pub trait IAppInteractInput {
|
pub trait IAppInteractInput {
|
||||||
type APP: IApp;
|
type APP: IApp;
|
||||||
|
|
||||||
@ -216,7 +214,15 @@ pub struct MatchStatePublic<'app> {
|
|||||||
pub state: &'app mut WidgetState,
|
pub state: &'app mut WidgetState,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type InputStatePublic<'app> = &'app Input;
|
pub enum InputClientPublic<'app> {
|
||||||
|
Match(MatchStatePublic<'app>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Input = tui_input::Input;
|
||||||
|
pub struct InputStatePublic<'app> {
|
||||||
|
pub input: &'app Input,
|
||||||
|
pub client: InputClientPublic<'app>
|
||||||
|
}
|
||||||
|
|
||||||
pub type AppPublicState<'app> = AppState<
|
pub type AppPublicState<'app> = AppState<
|
||||||
(),
|
(),
|
||||||
|
@ -32,7 +32,7 @@ use crate::tui::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::app::InputStatePublic;
|
use super::app::{Input, InputClientPublic};
|
||||||
|
|
||||||
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);
|
||||||
@ -149,16 +149,18 @@ 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: InputStatePublic, frame: &mut Frame) {
|
fn render_input_overlay(input: &Input, frame: &mut Frame) {
|
||||||
let area = OverlayBuilder::default()
|
let area = OverlayBuilder::default()
|
||||||
|
.with_width(OverlaySize::MarginFactor(4))
|
||||||
.with_height(OverlaySize::Value(3))
|
.with_height(OverlaySize::Value(3))
|
||||||
.build(frame.area());
|
.build(frame.area());
|
||||||
UiWidget::render_overlay_widget("Input", Paragraph::new(input.value()), area, false, frame);
|
let text_area = format!(" {}", input.value());
|
||||||
|
UiWidget::render_overlay_widget("Input", Paragraph::new(text_area), area, false, frame);
|
||||||
|
|
||||||
let width = area.width.max(3) - 3; // keep 2 for borders and 1 for cursor
|
let width = area.width.max(4) - 4; // keep 2 for borders, 1 for left-pad, and 1 for cursor
|
||||||
let scroll = input.visual_scroll(width as usize);
|
let scroll = input.visual_scroll(width as usize);
|
||||||
frame.set_cursor_position((
|
frame.set_cursor_position((
|
||||||
area.x + ((input.visual_cursor()).max(scroll) - scroll) as u16 + 1,
|
area.x + ((input.visual_cursor()).max(scroll) - scroll) as u16 + 2,
|
||||||
area.y + 1,
|
area.y + 1,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -186,7 +188,14 @@ impl IUi for Ui {
|
|||||||
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) => Self::render_input_overlay(input, 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),
|
||||||
_ => {}
|
_ => {}
|
||||||
@ -202,7 +211,10 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{AppPublic, AppPublicInner, Delta, MatchOption, MatchStatePublic},
|
app::{
|
||||||
|
AppPublic, AppPublicInner, Delta, InputClientPublic, InputStatePublic, MatchOption,
|
||||||
|
MatchStatePublic,
|
||||||
|
},
|
||||||
lib::interface::musicbrainz::Match,
|
lib::interface::musicbrainz::Match,
|
||||||
testmod::COLLECTION,
|
testmod::COLLECTION,
|
||||||
tests::terminal,
|
tests::terminal,
|
||||||
@ -228,7 +240,17 @@ mod tests {
|
|||||||
info: m.info,
|
info: m.info,
|
||||||
state: m.state,
|
state: m.state,
|
||||||
}),
|
}),
|
||||||
AppState::Input(ref i) => AppState::Input(i),
|
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),
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user