mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
272 lines
10 KiB
Rust
272 lines
10 KiB
Rust
use crate::*;
|
|
|
|
mod tui_input;
|
|
pub(crate) use tui_input::*;
|
|
pub use tui_input::TuiInput;
|
|
|
|
mod tui_output;
|
|
pub(crate) use tui_output::*;
|
|
pub use tui_output::TuiOutput;
|
|
|
|
////////////////////////////////////////////////////////
|
|
|
|
mod tui_style;
|
|
pub(crate) use tui_style::*;
|
|
mod tui_theme;
|
|
pub(crate) use tui_theme::*;
|
|
mod tui_border;
|
|
pub(crate) use tui_border::*;
|
|
|
|
////////////////////////////////////////////////////////
|
|
|
|
mod app_transport; pub(crate) use app_transport::*;
|
|
mod app_sequencer; pub(crate) use app_sequencer::*;
|
|
mod app_sampler; pub(crate) use app_sampler::*;
|
|
mod app_groovebox; pub(crate) use app_groovebox::*;
|
|
mod app_arranger; pub(crate) use app_arranger::*;
|
|
|
|
////////////////////////////////////////////////////////
|
|
|
|
mod status_bar; pub(crate) use status_bar::*;
|
|
mod file_browser; pub(crate) use file_browser::*;
|
|
mod phrase_editor; pub(crate) use phrase_editor::*;
|
|
mod piano_horizontal; pub(crate) use piano_horizontal::*;
|
|
mod phrase_length; pub(crate) use phrase_length::*;
|
|
mod phrase_rename; pub(crate) use phrase_rename::*;
|
|
mod phrase_list; pub(crate) use phrase_list::*;
|
|
mod port_select; pub(crate) use port_select::*;
|
|
|
|
////////////////////////////////////////////////////////
|
|
|
|
pub fn render <F: Fn(&mut TuiOutput)->Usually<()>+Send+Sync> (render: F) -> impl Render<Tui> {
|
|
Widget::new(|_|Ok(Some([0u16,0u16].into())), render)
|
|
}
|
|
|
|
////////////////////////////////////////////////////////
|
|
|
|
pub struct Tui {
|
|
pub exited: Arc<AtomicBool>,
|
|
pub buffer: Buffer,
|
|
pub backend: CrosstermBackend<Stdout>,
|
|
pub area: [u16;4], // FIXME auto resize
|
|
}
|
|
|
|
impl crate::core::Engine for Tui {
|
|
type Unit = u16;
|
|
type Size = [Self::Unit;2];
|
|
type Area = [Self::Unit;4];
|
|
type Input = TuiInput;
|
|
type Handled = bool;
|
|
type Output = TuiOutput;
|
|
fn exited (&self) -> bool {
|
|
self.exited.fetch_and(true, Ordering::Relaxed)
|
|
}
|
|
fn setup (&mut self) -> Usually<()> {
|
|
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
|
std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{
|
|
stdout().execute(LeaveAlternateScreen).unwrap();
|
|
CrosstermBackend::new(stdout()).show_cursor().unwrap();
|
|
disable_raw_mode().unwrap();
|
|
better_panic_handler(info);
|
|
}));
|
|
stdout().execute(EnterAlternateScreen)?;
|
|
self.backend.hide_cursor()?;
|
|
enable_raw_mode().map_err(Into::into)
|
|
}
|
|
fn teardown (&mut self) -> Usually<()> {
|
|
stdout().execute(LeaveAlternateScreen)?;
|
|
self.backend.show_cursor()?;
|
|
disable_raw_mode().map_err(Into::into)
|
|
}
|
|
}
|
|
|
|
impl Tui {
|
|
/// Run the main loop.
|
|
pub fn run <R: Component<Tui> + Sized + 'static> (
|
|
state: Arc<RwLock<R>>
|
|
) -> Usually<Arc<RwLock<R>>> {
|
|
let backend = CrosstermBackend::new(stdout());
|
|
let area = backend.size()?;
|
|
let engine = Self {
|
|
exited: Arc::new(AtomicBool::new(false)),
|
|
buffer: Buffer::empty(area),
|
|
area: [area.x, area.y, area.width, area.height],
|
|
backend,
|
|
};
|
|
let engine = Arc::new(RwLock::new(engine));
|
|
let _input_thread = Self::spawn_input_thread(&engine, &state, Duration::from_millis(100));
|
|
engine.write().unwrap().setup()?;
|
|
let render_thread = Self::spawn_render_thread(&engine, &state, Duration::from_millis(10));
|
|
render_thread.join().expect("main thread failed");
|
|
engine.write().unwrap().teardown()?;
|
|
Ok(state)
|
|
}
|
|
fn spawn_input_thread <R: Component<Tui> + Sized + 'static> (
|
|
engine: &Arc<RwLock<Self>>, state: &Arc<RwLock<R>>, poll: Duration
|
|
) -> JoinHandle<()> {
|
|
let exited = engine.read().unwrap().exited.clone();
|
|
let state = state.clone();
|
|
spawn(move || loop {
|
|
if exited.fetch_and(true, Ordering::Relaxed) {
|
|
break
|
|
}
|
|
if ::crossterm::event::poll(poll).is_ok() {
|
|
let event = TuiEvent::Input(::crossterm::event::read().unwrap());
|
|
match event {
|
|
key_pat!(Ctrl-KeyCode::Char('c')) => {
|
|
exited.store(true, Ordering::Relaxed);
|
|
},
|
|
_ => {
|
|
let exited = exited.clone();
|
|
if let Err(e) = state.write().unwrap().handle(&TuiInput { event, exited }) {
|
|
panic!("{e}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
fn spawn_render_thread <R: Component<Tui> + Sized + 'static> (
|
|
engine: &Arc<RwLock<Self>>, state: &Arc<RwLock<R>>, sleep: Duration
|
|
) -> JoinHandle<()> {
|
|
let exited = engine.read().unwrap().exited.clone();
|
|
let engine = engine.clone();
|
|
let state = state.clone();
|
|
let size = engine.read().unwrap().backend.size().expect("get size failed");
|
|
let mut buffer = Buffer::empty(size);
|
|
spawn(move || loop {
|
|
if exited.fetch_and(true, Ordering::Relaxed) {
|
|
break
|
|
}
|
|
let size = engine.read().unwrap().backend.size()
|
|
.expect("get size failed");
|
|
if let Ok(state) = state.try_read() {
|
|
if buffer.area != size {
|
|
engine.write().unwrap().backend.clear_region(ClearType::All)
|
|
.expect("clear failed");
|
|
buffer.resize(size);
|
|
buffer.reset();
|
|
}
|
|
let mut output = TuiOutput {
|
|
buffer,
|
|
area: [size.x, size.y, size.width, size.height]
|
|
};
|
|
state.render(&mut output).expect("render failed");
|
|
buffer = engine.write().unwrap().flip(output.buffer, size);
|
|
}
|
|
std::thread::sleep(sleep);
|
|
})
|
|
}
|
|
fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer {
|
|
if self.buffer.area != size {
|
|
self.backend.clear_region(ClearType::All).unwrap();
|
|
self.buffer.resize(size);
|
|
self.buffer.reset();
|
|
}
|
|
let updates = self.buffer.diff(&buffer);
|
|
self.backend.draw(updates.into_iter()).expect("failed to render");
|
|
self.backend.flush().expect("failed to flush output buffer");
|
|
std::mem::swap(&mut self.buffer, &mut buffer);
|
|
buffer.reset();
|
|
buffer
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
//struct Field(&'static str, String);
|
|
|
|
//render!(|self: Field|{
|
|
//Tui::to_east("│", Tui::to_east(
|
|
//Tui::bold(true, self.0),
|
|
//Tui::bg(Color::Rgb(0, 0, 0), self.1.as_str()),
|
|
//))
|
|
//});
|
|
|
|
//pub struct TransportView {
|
|
//pub(crate) state: Option<TransportState>,
|
|
//pub(crate) selected: Option<TransportFocus>,
|
|
//pub(crate) focused: bool,
|
|
//pub(crate) bpm: f64,
|
|
//pub(crate) sync: f64,
|
|
//pub(crate) quant: f64,
|
|
//pub(crate) beat: String,
|
|
//pub(crate) msu: String,
|
|
//}
|
|
////)?;
|
|
////match *state {
|
|
////Some(TransportState::Rolling) => {
|
|
////add(&row!(
|
|
////"│",
|
|
////TuiStyle::fg("▶ PLAYING", Color::Rgb(0, 255, 0)),
|
|
////format!("│0 (0)"),
|
|
////format!("│00m00s000u"),
|
|
////format!("│00B 0b 00/00")
|
|
////))?;
|
|
////add(&row!("│Now ", row!(
|
|
////format!("│0 (0)"), //sample(chunk)
|
|
////format!("│00m00s000u"), //msu
|
|
////format!("│00B 0b 00/00"), //bbt
|
|
////)))?;
|
|
////},
|
|
////_ => {
|
|
////add(&row!("│", TuiStyle::fg("⏹ STOPPED", Color::Rgb(255, 128, 0))))?;
|
|
////add(&"")?;
|
|
////}
|
|
////}
|
|
////Ok(())
|
|
////}).fill_x().bg(Color::Rgb(40, 50, 30))
|
|
////});
|
|
|
|
//impl<'a, T: HasClock> From<&'a T> for TransportView where Option<TransportFocus>: From<&'a T> {
|
|
//fn from (state: &'a T) -> Self {
|
|
//let selected = state.into();
|
|
//Self {
|
|
//selected,
|
|
//focused: selected.is_some(),
|
|
//state: Some(state.clock().transport.query_state().unwrap()),
|
|
//bpm: state.clock().bpm().get(),
|
|
//sync: state.clock().sync.get(),
|
|
//quant: state.clock().quant.get(),
|
|
//beat: state.clock().playhead.format_beat(),
|
|
//msu: state.clock().playhead.usec.format_msu(),
|
|
//}
|
|
//}
|
|
//}
|
|
|
|
//row!(
|
|
////selected.wrap(TransportFocus::PlayPause, &play_pause.fixed_xy(10, 3)),
|
|
//row!(
|
|
//col!(
|
|
//Field("SR ", format!("192000")),
|
|
//Field("BUF ", format!("1024")),
|
|
//Field("LEN ", format!("21300")),
|
|
//Field("CPU ", format!("00.0%"))
|
|
//),
|
|
//col!(
|
|
//Field("PUL ", format!("000000000")),
|
|
//Field("PPQ ", format!("96")),
|
|
//Field("BBT ", format!("00B0b00p"))
|
|
//),
|
|
//col!(
|
|
//Field("SEC ", format!("000000.000")),
|
|
//Field("BPM ", format!("000.000")),
|
|
//Field("MSU ", format!("00m00s00u"))
|
|
//),
|
|
//),
|
|
//selected.wrap(TransportFocus::Bpm, &Outset::X(1u16, {
|
|
//row! {
|
|
//"BPM ",
|
|
//format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0)
|
|
//}
|
|
//})),
|
|
//selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! {
|
|
//"SYNC ", pulses_to_name(*sync as usize)
|
|
//})),
|
|
//selected.wrap(TransportFocus::Quant, &Outset::X(1u16, row! {
|
|
//"QUANT ", pulses_to_name(*quant as usize)
|
|
//})),
|
|
//selected.wrap(TransportFocus::Clock, &{
|
|
//row!("B" , beat.as_str(), " T", msu.as_str()).outset_x(1)
|
|
//}).align_e().fill_x(),
|
|
//).fill_x().bg(Color::Rgb(40, 50, 30))
|