This commit is contained in:
+285
@@ -0,0 +1,285 @@
|
||||
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
||||
use gsf_core::ALL_COMMON_PARAMS;
|
||||
use gsf_core::ALL_LINEAR_PARAMS;
|
||||
use gsf_core::ALL_MODES;
|
||||
use gsf_core::ALL_NATURAL_PARAMS;
|
||||
use gsf_core::ALL_NOACCEL_PARAMS;
|
||||
use gsf_core::ALL_SYNCHRONOUS_PARAMS;
|
||||
use gsf_core::Param;
|
||||
use gsf_core::get_param_value_from_ctx;
|
||||
use gsf_core::persist::ParamStore;
|
||||
use gsf_core::persist::SysFsStore;
|
||||
use gsf_core::{ALL_PARAMS, AccelMode, ContextRef, TuiContext};
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::Backend;
|
||||
use ratatui::crossterm::event::{DisableMouseCapture, EnableMouseCapture, KeyCode, KeyEventKind};
|
||||
use std::{io, time::Instant};
|
||||
use std::{panic, time::Duration};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::action::{Action, Actions};
|
||||
use crate::component::TuiComponent;
|
||||
use crate::event::{Event, EventHandler};
|
||||
use crate::graph::SensitivityGraph;
|
||||
use crate::param_input::ParameterInput;
|
||||
use crate::screen::Screen;
|
||||
use crate::utils::CyclingIdx;
|
||||
|
||||
pub struct App {
|
||||
context: ContextRef<SysFsStore>,
|
||||
screens: Vec<Screen<SysFsStore>>,
|
||||
screen_idx: CyclingIdx,
|
||||
pub(crate) is_running: bool,
|
||||
|
||||
last_tick_at: Instant,
|
||||
}
|
||||
|
||||
pub fn collect_inputs_for_params(
|
||||
params: &[Param],
|
||||
context: ContextRef<SysFsStore>,
|
||||
) -> Vec<ParameterInput<SysFsStore>> {
|
||||
ALL_COMMON_PARAMS
|
||||
.iter()
|
||||
.chain(params)
|
||||
.filter_map(|&p| context.get().parameter(p).copied())
|
||||
.map(|param| ParameterInput::new(¶m, context.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
let context = ContextRef::new(
|
||||
TuiContext::init(SysFsStore, ALL_PARAMS).expect("Failed to initialize the Tui Context"),
|
||||
);
|
||||
|
||||
Self {
|
||||
screens: vec![
|
||||
Screen::new(
|
||||
AccelMode::Linear,
|
||||
collect_inputs_for_params(ALL_LINEAR_PARAMS, context.clone()),
|
||||
Box::new(
|
||||
SensitivityGraph::new(context.clone()).on_y_axix_bounds_update(|ctx| {
|
||||
// Appropriate dynamic bounds for the Linear sens graph
|
||||
let upper_bound = f64::from(get_param_value_from_ctx!(ctx, SensMult))
|
||||
* f64::from(get_param_value_from_ctx!(ctx, OutputCap)).max(1.0)
|
||||
* 2.0;
|
||||
|
||||
[0.0, upper_bound]
|
||||
}),
|
||||
),
|
||||
),
|
||||
Screen::new(
|
||||
AccelMode::Natural,
|
||||
collect_inputs_for_params(ALL_NATURAL_PARAMS, context.clone()),
|
||||
Box::new(
|
||||
SensitivityGraph::new(context.clone()).on_y_axix_bounds_update(|ctx| {
|
||||
// Appropriate dynamic bounds for the Natural sens graph
|
||||
let upper_bound = f64::from(get_param_value_from_ctx!(ctx, SensMult))
|
||||
* f64::from(get_param_value_from_ctx!(ctx, Limit)).max(1.0)
|
||||
* 2.0;
|
||||
|
||||
[0.0, upper_bound]
|
||||
}),
|
||||
),
|
||||
),
|
||||
Screen::new(
|
||||
AccelMode::Synchronous,
|
||||
collect_inputs_for_params(ALL_SYNCHRONOUS_PARAMS, context.clone()),
|
||||
Box::new(
|
||||
SensitivityGraph::new(context.clone()).on_y_axix_bounds_update(|ctx| {
|
||||
// Appropriate dynamic bounds for the Synchronous sens graph
|
||||
let upper_bound = f64::from(get_param_value_from_ctx!(ctx, SensMult))
|
||||
* f64::from(get_param_value_from_ctx!(ctx, Motivity)).max(1.0)
|
||||
* 2.0;
|
||||
|
||||
[0.0, upper_bound]
|
||||
}),
|
||||
),
|
||||
),
|
||||
Screen::new(
|
||||
AccelMode::NoAccel,
|
||||
collect_inputs_for_params(ALL_NOACCEL_PARAMS, context.clone()),
|
||||
Box::new(
|
||||
SensitivityGraph::new(context.clone()).on_y_axix_bounds_update(|ctx| {
|
||||
// Appropriate dynamic bounds for the NoAccel sens graph
|
||||
let upper_bound =
|
||||
f64::from(get_param_value_from_ctx!(ctx, SensMult)) * 2.0; // No other factor, sens is 1.0
|
||||
|
||||
[0.0, upper_bound.max(1.0)] // Ensure at least a small visible range
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
screen_idx: CyclingIdx::new_starting_at(
|
||||
ALL_MODES.len(),
|
||||
context.clone().get().current_mode.ordinal() as usize,
|
||||
),
|
||||
context,
|
||||
is_running: true,
|
||||
last_tick_at: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tick(&mut self) -> bool {
|
||||
#[cfg(not(debug_assertions))]
|
||||
let tick_rate = 16;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let tick_rate = 100;
|
||||
|
||||
let do_tick = self.last_tick_at.elapsed() >= Duration::from_millis(tick_rate);
|
||||
do_tick.then(|| {
|
||||
self.last_tick_at = Instant::now();
|
||||
});
|
||||
do_tick
|
||||
}
|
||||
|
||||
fn current_screen_mut(&mut self) -> &mut Screen<SysFsStore> {
|
||||
let screen_idx = self.screen_idx.current();
|
||||
self.screens.get_mut(screen_idx).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Failed to get a Screen for mode id {}: {:?}",
|
||||
screen_idx, ALL_MODES[screen_idx]
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
fn current_screen(&self) -> &Screen<SysFsStore> {
|
||||
let screen_idx = self.screen_idx.current();
|
||||
self.screens.get(screen_idx).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Failed to get a Screen for mode id {}: {:?}",
|
||||
screen_idx, ALL_MODES[screen_idx]
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
fn can_switch_screens(&self) -> bool {
|
||||
self.screens.len() > 1 && !self.current_screen().is_in_editing_mode()
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub(crate) fn handle_event(&mut self, event: &Event, actions: &mut Actions) {
|
||||
debug!("received event: {:?}", event);
|
||||
if let Event::Key(crossterm::event::KeyEvent {
|
||||
kind: KeyEventKind::Press,
|
||||
code,
|
||||
..
|
||||
}) = event
|
||||
{
|
||||
match code {
|
||||
KeyCode::Char('q') => {
|
||||
self.is_running = false;
|
||||
return;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
if self.can_switch_screens() {
|
||||
self.screen_idx.forward();
|
||||
actions.push(Action::SetMode(
|
||||
self.screens[self.screen_idx.current()].accel_mode,
|
||||
));
|
||||
}
|
||||
}
|
||||
KeyCode::Left => {
|
||||
if self.can_switch_screens() {
|
||||
self.screen_idx.back();
|
||||
actions.push(Action::SetMode(
|
||||
self.screens[self.screen_idx.current()].accel_mode,
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
self.current_screen_mut().handle_event(event, actions);
|
||||
}
|
||||
|
||||
pub(crate) fn update(&mut self, actions: &mut Vec<Action>) {
|
||||
debug!("performing actions: {actions:?}");
|
||||
|
||||
for action in actions.drain(..) {
|
||||
if let Action::SetMode(accel_mode) = action {
|
||||
self.context.get_mut().current_mode = accel_mode;
|
||||
SysFsStore
|
||||
.set_current_accel_mode(accel_mode)
|
||||
.expect("Failed to set accel mode in TUI");
|
||||
self.context.get_mut().reset_current_parameters();
|
||||
}
|
||||
|
||||
self.current_screen_mut().update(&action);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn draw(&self, frame: &mut ratatui::Frame, area: ratatui::prelude::Rect) {
|
||||
self.current_screen().draw(frame, area);
|
||||
}
|
||||
}
|
||||
|
||||
/// Representation of a terminal user interface.
|
||||
///
|
||||
/// It is responsible for setting up the terminal,
|
||||
/// initializing the interface and handling the draw events.
|
||||
#[derive(Debug)]
|
||||
pub struct Tui<B: Backend> {
|
||||
/// Interface to the Terminal.
|
||||
terminal: Terminal<B>,
|
||||
/// Terminal event handler.
|
||||
pub events: EventHandler,
|
||||
}
|
||||
|
||||
impl<B: Backend> Tui<B> {
|
||||
/// Constructs a new instance of [`Tui`].
|
||||
pub fn new(terminal: Terminal<B>, events: EventHandler) -> Self {
|
||||
Self { terminal, events }
|
||||
}
|
||||
|
||||
/// Initializes the terminal interface.
|
||||
///
|
||||
/// It enables the raw mode and sets terminal properties.
|
||||
pub fn init(&mut self) -> anyhow::Result<()> {
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?;
|
||||
|
||||
// Define a custom panic hook to reset the terminal properties.
|
||||
// This way, you won't have your terminal messed up if an unexpected error happens.
|
||||
let panic_hook = panic::take_hook();
|
||||
panic::set_hook(Box::new(move |panic| {
|
||||
Self::reset().expect("failed to reset the terminal");
|
||||
panic_hook(panic);
|
||||
}));
|
||||
|
||||
self.terminal.hide_cursor()?;
|
||||
self.terminal.clear()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// [`Draw`] the terminal interface by [`rendering`] the widgets.
|
||||
///
|
||||
/// [`Draw`]: ratatui::Terminal::draw
|
||||
/// [`rendering`]: crate::ui::render
|
||||
pub fn draw(&mut self, app: &mut App) -> anyhow::Result<()> {
|
||||
self.terminal.draw(|frame| app.draw(frame, frame.area()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resets the terminal interface.
|
||||
///
|
||||
/// This function is also used for the panic hook to revert
|
||||
/// the terminal properties if unexpected errors occur.
|
||||
fn reset() -> anyhow::Result<()> {
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Exits the terminal interface.
|
||||
///
|
||||
/// It disables the raw mode and reverts back the terminal properties.
|
||||
pub fn exit(&mut self) -> anyhow::Result<()> {
|
||||
Self::reset()?;
|
||||
self.terminal.show_cursor()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user