mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-01-31 16:36:40 +01:00
wip(108e): layout refactor
This commit is contained in:
parent
265d4a3953
commit
ddb3c28c01
21 changed files with 1141 additions and 825 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -2711,7 +2711,6 @@ dependencies = [
|
||||||
"better-panic",
|
"better-panic",
|
||||||
"clap",
|
"clap",
|
||||||
"clojure-reader",
|
"clojure-reader",
|
||||||
"crossterm",
|
|
||||||
"jack",
|
"jack",
|
||||||
"midly",
|
"midly",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
@ -2726,6 +2725,8 @@ dependencies = [
|
||||||
name = "tek_tui"
|
name = "tek_tui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"better-panic",
|
||||||
|
"crossterm",
|
||||||
"livi",
|
"livi",
|
||||||
"suil-rs",
|
"suil-rs",
|
||||||
"symphonia",
|
"symphonia",
|
||||||
|
|
|
||||||
|
|
@ -33,18 +33,35 @@ impl<T: HasClock> Command<T> for ClockCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Timeline {
|
||||||
|
pub timebase: Arc<Timebase>,
|
||||||
|
pub started: Arc<RwLock<Option<Moment>>>,
|
||||||
|
pub loopback: Arc<RwLock<Option<Moment>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Timeline {
|
||||||
|
fn default () -> Self {
|
||||||
|
Self {
|
||||||
|
timebase: Arc::new(Timebase::default()),
|
||||||
|
started: RwLock::new(None).into(),
|
||||||
|
loopback: RwLock::new(None).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ClockModel {
|
pub struct ClockModel {
|
||||||
/// JACK transport handle.
|
/// JACK transport handle.
|
||||||
pub transport: Arc<Transport>,
|
pub transport: Arc<Transport>,
|
||||||
/// Global temporal resolution (shared by [Instant] fields)
|
/// Global temporal resolution (shared by [Moment] fields)
|
||||||
pub timebase: Arc<Timebase>,
|
pub timebase: Arc<Timebase>,
|
||||||
/// Current global sample and usec (monotonic from JACK clock)
|
/// Current global sample and usec (monotonic from JACK clock)
|
||||||
pub global: Arc<Instant>,
|
pub global: Arc<Moment>,
|
||||||
/// Global sample and usec at which playback started
|
/// Global sample and usec at which playback started
|
||||||
pub started: Arc<RwLock<Option<Instant>>>,
|
pub started: Arc<RwLock<Option<Moment>>>,
|
||||||
/// Current playhead position
|
/// Current playhead position
|
||||||
pub playhead: Arc<Instant>,
|
pub playhead: Arc<Moment>,
|
||||||
/// Note quantization factor
|
/// Note quantization factor
|
||||||
pub quant: Arc<Quantize>,
|
pub quant: Arc<Quantize>,
|
||||||
/// Launch quantization factor
|
/// Launch quantization factor
|
||||||
|
|
@ -64,8 +81,8 @@ impl From<&Arc<RwLock<JackClient>>> for ClockModel {
|
||||||
sync: Arc::new(384.into()),
|
sync: Arc::new(384.into()),
|
||||||
transport: Arc::new(transport),
|
transport: Arc::new(transport),
|
||||||
chunk: Arc::new((chunk as usize).into()),
|
chunk: Arc::new((chunk as usize).into()),
|
||||||
global: Arc::new(Instant::zero(&timebase)),
|
global: Arc::new(Moment::zero(&timebase)),
|
||||||
playhead: Arc::new(Instant::zero(&timebase)),
|
playhead: Arc::new(Moment::zero(&timebase)),
|
||||||
started: RwLock::new(None).into(),
|
started: RwLock::new(None).into(),
|
||||||
timebase,
|
timebase,
|
||||||
}
|
}
|
||||||
|
|
@ -147,7 +164,7 @@ impl ClockModel {
|
||||||
match self.transport.query_state()? {
|
match self.transport.query_state()? {
|
||||||
TransportState::Rolling => {
|
TransportState::Rolling => {
|
||||||
if started.is_none() {
|
if started.is_none() {
|
||||||
*started = Some(Instant::from_sample(&self.timebase, current_frames as f64));
|
*started = Some(Moment::from_sample(&self.timebase, current_frames as f64));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TransportState::Stopped => {
|
TransportState::Stopped => {
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,10 @@ pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {}
|
||||||
pub trait HasPlayPhrase: HasClock {
|
pub trait HasPlayPhrase: HasClock {
|
||||||
fn reset (&self) -> bool;
|
fn reset (&self) -> bool;
|
||||||
fn reset_mut (&mut self) -> &mut bool;
|
fn reset_mut (&mut self) -> &mut bool;
|
||||||
fn play_phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)>;
|
fn play_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
||||||
fn play_phrase_mut (&mut self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)>;
|
fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
||||||
fn next_phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)>;
|
fn next_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
||||||
fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)>;
|
fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
||||||
fn pulses_since_start (&self) -> Option<f64> {
|
fn pulses_since_start (&self) -> Option<f64> {
|
||||||
if let Some((started, Some(_))) = self.play_phrase().as_ref() {
|
if let Some((started, Some(_))) = self.play_phrase().as_ref() {
|
||||||
Some(self.clock().playhead.pulse.get() - started.pulse.get())
|
Some(self.clock().playhead.pulse.get() - started.pulse.get())
|
||||||
|
|
@ -23,7 +23,7 @@ pub trait HasPlayPhrase: HasClock {
|
||||||
}
|
}
|
||||||
fn enqueue_next (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
|
fn enqueue_next (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
|
||||||
let start = self.clock().next_launch_pulse() as f64;
|
let start = self.clock().next_launch_pulse() as f64;
|
||||||
let instant = Instant::from_pulse(&self.clock().timebase(), start);
|
let instant = Moment::from_pulse(&self.clock().timebase(), start);
|
||||||
let phrase = phrase.map(|p|p.clone());
|
let phrase = phrase.map(|p|p.clone());
|
||||||
*self.next_phrase_mut() = Some((instant, phrase));
|
*self.next_phrase_mut() = Some((instant, phrase));
|
||||||
*self.reset_mut() = true;
|
*self.reset_mut() = true;
|
||||||
|
|
@ -206,7 +206,7 @@ pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts {
|
||||||
// Samples elapsed since phrase was supposed to start
|
// Samples elapsed since phrase was supposed to start
|
||||||
let skipped = sample0 - start;
|
let skipped = sample0 - start;
|
||||||
// Switch over to enqueued phrase
|
// Switch over to enqueued phrase
|
||||||
let started = Instant::from_sample(&self.clock().timebase(), start as f64);
|
let started = Moment::from_sample(&self.clock().timebase(), start as f64);
|
||||||
*self.play_phrase_mut() = Some((started, phrase.clone()));
|
*self.play_phrase_mut() = Some((started, phrase.clone()));
|
||||||
// Unset enqueuement (TODO: where to implement looping?)
|
// Unset enqueuement (TODO: where to implement looping?)
|
||||||
*self.next_phrase_mut() = None
|
*self.next_phrase_mut() = None
|
||||||
|
|
@ -306,9 +306,9 @@ impl<'a, T: MidiPlayerApi> Audio for PlayerAudio<'a, T> {
|
||||||
///// Global timebase
|
///// Global timebase
|
||||||
//pub clock: Arc<Clock>,
|
//pub clock: Arc<Clock>,
|
||||||
///// Start time and phrase being played
|
///// Start time and phrase being played
|
||||||
//pub play_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
|
//pub play_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
||||||
///// Start time and next phrase
|
///// Start time and next phrase
|
||||||
//pub next_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
|
//pub next_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
||||||
///// Play input through output.
|
///// Play input through output.
|
||||||
//pub monitoring: bool,
|
//pub monitoring: bool,
|
||||||
///// Write input to sequence.
|
///// Write input to sequence.
|
||||||
|
|
|
||||||
0
crates/tek_api/src/api_timeline.ts
Normal file
0
crates/tek_api/src/api_timeline.ts
Normal file
|
|
@ -455,8 +455,8 @@ fn query_ports(client: &Client, names: Vec<String>) -> BTreeMap<String, Port<Uno
|
||||||
//}
|
//}
|
||||||
//}
|
//}
|
||||||
|
|
||||||
//impl From<Instant> for Clock {
|
//impl From<Moment> for Clock {
|
||||||
//fn from (current: Instant) -> Self {
|
//fn from (current: Moment) -> Self {
|
||||||
//Self {
|
//Self {
|
||||||
//playing: Some(TransportState::Stopped).into(),
|
//playing: Some(TransportState::Stopped).into(),
|
||||||
//started: None.into(),
|
//started: None.into(),
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ backtrace = "0.3.72"
|
||||||
better-panic = "0.3.0"
|
better-panic = "0.3.0"
|
||||||
clap = { version = "4.5.4", features = [ "derive" ] }
|
clap = { version = "4.5.4", features = [ "derive" ] }
|
||||||
clojure-reader = "0.1.0"
|
clojure-reader = "0.1.0"
|
||||||
crossterm = "0.27"
|
|
||||||
jack = "0.13"
|
jack = "0.13"
|
||||||
midly = "0.5"
|
midly = "0.5"
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ impl Demo<Tui> {
|
||||||
|
|
||||||
impl Content for Demo<Tui> {
|
impl Content for Demo<Tui> {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Render<Engine = Tui> {
|
fn content (&self) -> dyn Render<Engine = Tui> {
|
||||||
let border_style = Style::default().fg(Color::Rgb(0,0,0));
|
let border_style = Style::default().fg(Color::Rgb(0,0,0));
|
||||||
Align::Center(Layers::new(move|add|{
|
Align::Center(Layers::new(move|add|{
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use rand::{thread_rng, distributions::uniform::UniformSampler};
|
use rand::{thread_rng, distributions::uniform::UniformSampler};
|
||||||
|
pub use ratatui::prelude::Color;
|
||||||
|
|
||||||
/// A color in OKHSL and RGB representations.
|
/// A color in OKHSL and RGB representations.
|
||||||
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
||||||
|
|
|
||||||
76
crates/tek_core/src/layout.rs
Normal file
76
crates/tek_core/src/layout.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub enum Collect<'a, E: Engine, const N: usize> {
|
||||||
|
Callback(CallbackCollection<'a, E>),
|
||||||
|
//Iterator(IteratorCollection<'a, E>),
|
||||||
|
Array(ArrayCollection<'a, E, N>),
|
||||||
|
Slice(SliceCollection<'a, E>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, E: Engine, const N: usize> Collect<'a, E, N> {
|
||||||
|
pub fn iter (&'a self) -> CollectIterator<'a, E, N> {
|
||||||
|
CollectIterator(0, &self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, E: Engine, const N: usize> From<CallbackCollection<'a, E>> for Collect<'a, E, N> {
|
||||||
|
fn from (callback: CallbackCollection<'a, E>) -> Self {
|
||||||
|
Self::Callback(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, E: Engine, const N: usize> From<SliceCollection<'a, E>> for Collect<'a, E, N> {
|
||||||
|
fn from (slice: SliceCollection<'a, E>) -> Self {
|
||||||
|
Self::Slice(slice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, E: Engine, const N: usize> From<ArrayCollection<'a, E, N>> for Collect<'a, E, N>{
|
||||||
|
fn from (array: ArrayCollection<'a, E, N>) -> Self {
|
||||||
|
Self::Array(array)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallbackCollection<'a, E> =
|
||||||
|
&'a dyn Fn(&'a mut dyn FnMut(&dyn Render<Engine = E>)->Usually<()>);
|
||||||
|
|
||||||
|
//type IteratorCollection<'a, E> =
|
||||||
|
//&'a mut dyn Iterator<Item = dyn Render<Engine = E>>;
|
||||||
|
|
||||||
|
type SliceCollection<'a, E> =
|
||||||
|
&'a [&'a dyn Render<Engine = E>];
|
||||||
|
|
||||||
|
type ArrayCollection<'a, E, const N: usize> =
|
||||||
|
[&'a dyn Render<Engine = E>; N];
|
||||||
|
|
||||||
|
pub struct CollectIterator<'a, E: Engine, const N: usize>(usize, &'a Collect<'a, E, N>);
|
||||||
|
|
||||||
|
impl<'a, E: Engine, const N: usize> Iterator for CollectIterator<'a, E, N> {
|
||||||
|
type Item = &'a dyn Render<Engine = E>;
|
||||||
|
fn next (&mut self) -> Option<Self::Item> {
|
||||||
|
match self.1 {
|
||||||
|
Collect::Callback(callback) => {
|
||||||
|
todo!()
|
||||||
|
},
|
||||||
|
//Collection::Iterator(iterator) => {
|
||||||
|
//iterator.next()
|
||||||
|
//},
|
||||||
|
Collect::Array(array) => {
|
||||||
|
if let Some(item) = array.get(self.0) {
|
||||||
|
self.0 += 1;
|
||||||
|
Some(item)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Collect::Slice(slice) => {
|
||||||
|
if let Some(item) = slice.get(self.0) {
|
||||||
|
self.0 += 1;
|
||||||
|
Some(item)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
pub use ratatui;
|
pub use ratatui;
|
||||||
pub use crossterm;
|
|
||||||
pub use jack;
|
pub use jack;
|
||||||
pub use midly;
|
pub use midly;
|
||||||
pub use clap;
|
pub use clap;
|
||||||
|
|
@ -11,12 +10,8 @@ pub use std::rc::Rc;
|
||||||
pub use std::cell::{Cell, RefCell};
|
pub use std::cell::{Cell, RefCell};
|
||||||
pub use std::marker::PhantomData;
|
pub use std::marker::PhantomData;
|
||||||
pub(crate) use std::error::Error;
|
pub(crate) use std::error::Error;
|
||||||
pub(crate) use std::io::{stdout};
|
|
||||||
pub(crate) use std::thread::{spawn, JoinHandle};
|
|
||||||
pub(crate) use std::time::Duration;
|
|
||||||
pub(crate) use atomic_float::*;
|
pub(crate) use atomic_float::*;
|
||||||
pub(crate) use palette::{*, convert::*, okhsl::*};
|
pub(crate) use palette::{*, convert::*, okhsl::*};
|
||||||
use better_panic::{Settings, Verbosity};
|
|
||||||
use std::ops::{Add, Sub, Mul, Div, Rem};
|
use std::ops::{Add, Sub, Mul, Div, Rem};
|
||||||
use std::cmp::{Ord, Eq, PartialEq};
|
use std::cmp::{Ord, Eq, PartialEq};
|
||||||
use std::fmt::{Debug, Display};
|
use std::fmt::{Debug, Display};
|
||||||
|
|
@ -46,7 +41,8 @@ submod! {
|
||||||
pitch
|
pitch
|
||||||
space
|
space
|
||||||
time
|
time
|
||||||
tui
|
//tui
|
||||||
|
layout
|
||||||
}
|
}
|
||||||
|
|
||||||
testmod! {
|
testmod! {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ pub trait Coordinate: Send + Sync + Copy
|
||||||
0.into()
|
0.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn ZERO () -> Self {
|
||||||
|
0.into()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Coordinate for T where T: Send + Sync + Copy
|
impl<T> Coordinate for T where T: Send + Sync + Copy
|
||||||
|
|
@ -702,6 +705,9 @@ impl<
|
||||||
#[inline] pub fn down (build: F) -> Self {
|
#[inline] pub fn down (build: F) -> Self {
|
||||||
Self::new(Direction::Down, build)
|
Self::new(Direction::Down, build)
|
||||||
}
|
}
|
||||||
|
#[inline] pub fn up (build: F) -> Self {
|
||||||
|
Self::new(Direction::Up, build)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: Engine, F> Render for Stack<E, F>
|
impl<E: Engine, F> Render for Stack<E, F>
|
||||||
|
|
@ -710,63 +716,119 @@ where
|
||||||
{
|
{
|
||||||
type Engine = E;
|
type Engine = E;
|
||||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||||
let mut w = 0.into();
|
|
||||||
let mut h = 0.into();
|
|
||||||
match self.1 {
|
match self.1 {
|
||||||
|
|
||||||
Direction::Down => {
|
Direction::Down => {
|
||||||
(self.0)(&mut |component| {
|
let mut w: E::Unit = 0.into();
|
||||||
if h >= to.h() { return Ok(()) }
|
let mut h: E::Unit = 0.into();
|
||||||
let size = component.push_y(h).max_y(to.h() - h).min_size(to)?;
|
(self.0)(&mut |component: &dyn Render<Engine = E>| {
|
||||||
if let Some([width, height]) = size.map(|size|size.wh()) {
|
let max = to.h().minus(h);
|
||||||
h = h + height.into();
|
if max > E::Unit::ZERO() {
|
||||||
if width > w { w = width; }
|
let item = component.push_y(h).max_y(max);
|
||||||
|
let size = item.min_size(to)?.map(|size|size.wh());
|
||||||
|
if let Some([width, height]) = size {
|
||||||
|
h = h + height.into();
|
||||||
|
w = w.max(width);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
Ok(Some([w, h].into()))
|
||||||
},
|
},
|
||||||
|
|
||||||
Direction::Right => {
|
Direction::Right => {
|
||||||
(self.0)(&mut |component| {
|
let mut w: E::Unit = 0.into();
|
||||||
if w >= to.w() { return Ok(()) }
|
let mut h: E::Unit = 0.into();
|
||||||
let size = component.push_x(w).max_x(to.w() - w).min_size(to)?;
|
(self.0)(&mut |component: &dyn Render<Engine = E>| {
|
||||||
if let Some([width, height]) = size.map(|size|size.wh()) {
|
let max = to.w().minus(w);
|
||||||
w = w + width.into();
|
if max > E::Unit::ZERO() {
|
||||||
if height > h { h = height }
|
let item = component.push_x(w).max_x(max);
|
||||||
|
let size = item.min_size(to)?.map(|size|size.wh());
|
||||||
|
if let Some([width, height]) = size {
|
||||||
|
w = w + width.into();
|
||||||
|
h = h.max(height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
Ok(Some([w, h].into()))
|
||||||
},
|
},
|
||||||
_ => todo!()
|
|
||||||
};
|
Direction::Up => {
|
||||||
Ok(Some([w, h].into()))
|
let mut w: E::Unit = 0.into();
|
||||||
|
let mut h: E::Unit = 0.into();
|
||||||
|
(self.0)(&mut |component: &dyn Render<Engine = E>| {
|
||||||
|
let max = to.h().minus(h);
|
||||||
|
if max > E::Unit::ZERO() {
|
||||||
|
let item = component.max_y(to.h() - h);
|
||||||
|
let size = item.min_size(to)?.map(|size|size.wh());
|
||||||
|
if let Some([width, height]) = size {
|
||||||
|
h = h + height.into();
|
||||||
|
w = w.max(width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
Ok(Some([w, h].into()))
|
||||||
|
},
|
||||||
|
|
||||||
|
Direction::Left => {
|
||||||
|
let mut w: E::Unit = 0.into();
|
||||||
|
let mut h: E::Unit = 0.into();
|
||||||
|
(self.0)(&mut |component: &dyn Render<Engine = E>| {
|
||||||
|
if w < to.w() {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
Ok(Some([w, h].into()))
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||||
let area = to.area();
|
let area = to.area();
|
||||||
let mut w = 0.into();
|
let mut w = 0.into();
|
||||||
let mut h = 0.into();
|
let mut h = 0.into();
|
||||||
match self.1 {
|
match self.1 {
|
||||||
Direction::Down => {
|
Direction::Down => {
|
||||||
(self.0)(&mut |component| {
|
(self.0)(&mut |item| {
|
||||||
if h >= area.h() { return Ok(()) }
|
if h < area.h() {
|
||||||
let item = component.push_y(h).max_y(area.h() - h);
|
let item = item.push_y(h).max_y(area.h() - h);
|
||||||
let size = item.min_size(area.wh().into())?;
|
let show = item.min_size(area.wh().into())?.map(|s|s.wh());
|
||||||
if let Some([width, height]) = size.map(|size|size.wh()) {
|
if let Some([width, height]) = show {
|
||||||
item.render(to)?;
|
item.render(to)?;
|
||||||
h = h + height;
|
h = h + height;
|
||||||
if width > w { w = width }
|
if width > w { w = width }
|
||||||
};
|
};
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
},
|
},
|
||||||
Direction::Right => {
|
Direction::Right => {
|
||||||
(self.0)(&mut |component| {
|
(self.0)(&mut |item| {
|
||||||
if w >= area.w() { return Ok(()) }
|
if w < area.w() {
|
||||||
let item = component.push_x(w).max_x(area.w() - w);
|
let item = item.push_x(w).max_x(area.w() - w);
|
||||||
let size = item.min_size(area.wh().into())?;
|
let show = item.min_size(area.wh().into())?.map(|s|s.wh());
|
||||||
if let Some([width, height]) = size.map(|size|size.wh()) {
|
if let Some([width, height]) = show {
|
||||||
item.render(to)?;
|
item.render(to)?;
|
||||||
w = width + w;
|
w = width + w;
|
||||||
if height > h { h = height }
|
if height > h { h = height }
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
},
|
||||||
|
Direction::Up => {
|
||||||
|
(self.0)(&mut |item| {
|
||||||
|
if h < area.h() {
|
||||||
|
let show = item.min_size([area.w(), area.h().minus(h)].into())?.map(|s|s.wh());
|
||||||
|
if let Some([width, height]) = show {
|
||||||
|
item.push_y(area.h() - height).shrink_y(height).render(to)?;
|
||||||
|
h = h + height;
|
||||||
|
if width > w { w = width }
|
||||||
|
};
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -245,9 +245,19 @@ impl Timebase {
|
||||||
impl Default for Timebase {
|
impl Default for Timebase {
|
||||||
fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) }
|
fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Moment2 {
|
||||||
|
None,
|
||||||
|
Zero,
|
||||||
|
Usec(Microsecond),
|
||||||
|
Sample(SampleCount),
|
||||||
|
Pulse(Pulse),
|
||||||
|
}
|
||||||
|
|
||||||
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
|
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct Instant {
|
pub struct Moment {
|
||||||
pub timebase: Arc<Timebase>,
|
pub timebase: Arc<Timebase>,
|
||||||
/// Current time in microseconds
|
/// Current time in microseconds
|
||||||
pub usec: Microsecond,
|
pub usec: Microsecond,
|
||||||
|
|
@ -256,7 +266,7 @@ pub struct Instant {
|
||||||
/// Current time in MIDI pulses
|
/// Current time in MIDI pulses
|
||||||
pub pulse: Pulse,
|
pub pulse: Pulse,
|
||||||
}
|
}
|
||||||
impl Instant {
|
impl Moment {
|
||||||
pub fn zero (timebase: &Arc<Timebase>) -> Self {
|
pub fn zero (timebase: &Arc<Timebase>) -> Self {
|
||||||
Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() }
|
Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,690 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
pub(crate) use ratatui::buffer::Cell;
|
|
||||||
pub(crate) use crossterm::{ExecutableCommand};
|
|
||||||
pub use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState};
|
|
||||||
pub use ratatui::prelude::{Rect, Style, Color, Buffer};
|
|
||||||
pub use ratatui::style::{Stylize, Modifier};
|
|
||||||
use ratatui::backend::{Backend, CrosstermBackend, ClearType};
|
|
||||||
use std::io::Stdout;
|
|
||||||
use crossterm::terminal::{
|
|
||||||
EnterAlternateScreen, LeaveAlternateScreen,
|
|
||||||
enable_raw_mode, disable_raw_mode
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Tui {
|
|
||||||
pub exited: Arc<AtomicBool>,
|
|
||||||
pub buffer: Buffer,
|
|
||||||
pub backend: CrosstermBackend<Stdout>,
|
|
||||||
pub area: [u16;4], // FIXME auto resize
|
|
||||||
}
|
|
||||||
impl 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.xywh(),
|
|
||||||
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!(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.xywh() };
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub struct TuiInput { event: TuiEvent, exited: Arc<AtomicBool>, }
|
|
||||||
impl Input<Tui> for TuiInput {
|
|
||||||
type Event = TuiEvent;
|
|
||||||
fn event (&self) -> &TuiEvent { &self.event }
|
|
||||||
fn is_done (&self) -> bool { self.exited.fetch_and(true, Ordering::Relaxed) }
|
|
||||||
fn done (&self) { self.exited.store(true, Ordering::Relaxed); }
|
|
||||||
}
|
|
||||||
impl TuiInput {
|
|
||||||
// TODO remove
|
|
||||||
pub fn handle_keymap <T> (&self, state: &mut T, keymap: &KeyMap<T>) -> Usually<bool> {
|
|
||||||
match self.event() {
|
|
||||||
TuiEvent::Input(crossterm::event::Event::Key(event)) => {
|
|
||||||
for (code, modifiers, _, _, command) in keymap.iter() {
|
|
||||||
if *code == event.code && modifiers.bits() == event.modifiers.bits() {
|
|
||||||
return command(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub type KeyHandler<T> = &'static dyn Fn(&mut T)->Usually<bool>;
|
|
||||||
pub type KeyBinding<T> = (KeyCode, KeyModifiers, &'static str, &'static str, KeyHandler<T>);
|
|
||||||
pub type KeyMap<T> = [KeyBinding<T>];
|
|
||||||
pub struct TuiOutput { pub buffer: Buffer, pub area: [u16;4] }
|
|
||||||
impl Output<Tui> for TuiOutput {
|
|
||||||
#[inline] fn area (&self) -> [u16;4] { self.area }
|
|
||||||
#[inline] fn area_mut (&mut self) -> &mut [u16;4] { &mut self.area }
|
|
||||||
#[inline] fn render_in (&mut self,
|
|
||||||
area: [u16;4],
|
|
||||||
widget: &dyn Render<Engine = Tui>
|
|
||||||
) -> Usually<()> {
|
|
||||||
let last = self.area();
|
|
||||||
*self.area_mut() = area;
|
|
||||||
widget.render(self)?;
|
|
||||||
*self.area_mut() = last;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl TuiOutput {
|
|
||||||
pub fn buffer_update (&mut self,
|
|
||||||
area: [u16;4],
|
|
||||||
callback: &impl Fn(&mut Cell, u16, u16)
|
|
||||||
) {
|
|
||||||
buffer_update(&mut self.buffer, area, callback);
|
|
||||||
}
|
|
||||||
pub fn fill_bold (&mut self, area: [u16;4], on: bool) {
|
|
||||||
if on {
|
|
||||||
self.buffer_update(area, &|cell,_,_|cell.modifier.insert(Modifier::BOLD))
|
|
||||||
} else {
|
|
||||||
self.buffer_update(area, &|cell,_,_|cell.modifier.remove(Modifier::BOLD))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn fill_bg (&mut self, area: [u16;4], color: Color) {
|
|
||||||
self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);})
|
|
||||||
}
|
|
||||||
pub fn fill_fg (&mut self, area: [u16;4], color: Color) {
|
|
||||||
self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);})
|
|
||||||
}
|
|
||||||
pub fn fill_ul (&mut self, area: [u16;4], color: Color) {
|
|
||||||
self.buffer_update(area, &|cell,_,_|{
|
|
||||||
cell.modifier = ratatui::prelude::Modifier::UNDERLINED;
|
|
||||||
cell.underline_color = color;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn fill_char (&mut self, area: [u16;4], c: char) {
|
|
||||||
self.buffer_update(area, &|cell,_,_|{cell.set_char(c);})
|
|
||||||
}
|
|
||||||
pub fn make_dim (&mut self) {
|
|
||||||
for cell in self.buffer.content.iter_mut() {
|
|
||||||
cell.bg = ratatui::style::Color::Rgb(30,30,30);
|
|
||||||
cell.fg = ratatui::style::Color::Rgb(100,100,100);
|
|
||||||
cell.modifier = ratatui::style::Modifier::DIM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn blit (
|
|
||||||
&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>
|
|
||||||
) {
|
|
||||||
let text = text.as_ref();
|
|
||||||
let buf = &mut self.buffer;
|
|
||||||
if x < buf.area.width && y < buf.area.height {
|
|
||||||
buf.set_string(x, y, text, style.unwrap_or(Style::default()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn with_rect (&mut self, area: [u16;4]) -> &mut Self {
|
|
||||||
self.area = area;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum TuiEvent {
|
|
||||||
/// Terminal input
|
|
||||||
Input(::crossterm::event::Event),
|
|
||||||
/// Update values but not the whole form.
|
|
||||||
Update,
|
|
||||||
/// Update the whole form.
|
|
||||||
Redraw,
|
|
||||||
/// Device gains focus
|
|
||||||
Focus,
|
|
||||||
/// Device loses focus
|
|
||||||
Blur,
|
|
||||||
// /// JACK notification
|
|
||||||
// Jack(JackEvent)
|
|
||||||
}
|
|
||||||
impl Area<u16> for Rect {
|
|
||||||
fn x (&self) -> u16 { self.x }
|
|
||||||
fn y (&self) -> u16 { self.y }
|
|
||||||
fn w (&self) -> u16 { self.width }
|
|
||||||
fn h (&self) -> u16 { self.height }
|
|
||||||
}
|
|
||||||
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
|
|
||||||
match (lower, upper) {
|
|
||||||
(true, true) => Some('█'),
|
|
||||||
(true, false) => Some('▄'),
|
|
||||||
(false, true) => Some('▀'),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct BigBuffer {
|
|
||||||
pub width: usize,
|
|
||||||
pub height: usize,
|
|
||||||
pub content: Vec<Cell>
|
|
||||||
}
|
|
||||||
impl BigBuffer {
|
|
||||||
pub fn new (width: usize, height: usize) -> Self {
|
|
||||||
Self { width, height, content: vec![Cell::default(); width*height] }
|
|
||||||
}
|
|
||||||
pub fn get (&self, x: usize, y: usize) -> Option<&Cell> {
|
|
||||||
let i = self.index_of(x, y);
|
|
||||||
self.content.get(i)
|
|
||||||
}
|
|
||||||
pub fn get_mut (&mut self, x: usize, y: usize) -> Option<&mut Cell> {
|
|
||||||
let i = self.index_of(x, y);
|
|
||||||
self.content.get_mut(i)
|
|
||||||
}
|
|
||||||
pub fn index_of (&self, x: usize, y: usize) -> usize {
|
|
||||||
y * self.width + x
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
|
|
||||||
for row in 0..area.h() {
|
|
||||||
let y = area.y() + row;
|
|
||||||
for col in 0..area.w() {
|
|
||||||
let x = area.x() + col;
|
|
||||||
if x < buf.area.width && y < buf.area.height {
|
|
||||||
callback(buf.get_mut(x, y), col, row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Render for &str {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
|
|
||||||
// TODO: line breaks
|
|
||||||
Ok(Some([self.chars().count() as u16, 1]))
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
||||||
let [x, y, ..] = to.area();
|
|
||||||
//let [w, h] = self.min_size(to.area().wh())?.unwrap();
|
|
||||||
Ok(to.blit(&self, x, y, None))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Render for String {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
|
|
||||||
// TODO: line breaks
|
|
||||||
Ok(Some([self.chars().count() as u16, 1]))
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
||||||
let [x, y, ..] = to.area();
|
|
||||||
//let [w, h] = self.min_size(to.area().wh())?.unwrap();
|
|
||||||
Ok(to.blit(&self, x, y, None))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T: Render<Engine = Tui>> Render for DebugOverlay<Tui, T> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
|
||||||
self.0.min_size(to)
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
||||||
let [x, y, w, h] = to.area();
|
|
||||||
self.0.render(to)?;
|
|
||||||
Ok(to.blit(&format!("{w}x{h}+{x}+{y}"), x, y, Some(Style::default().green())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub struct Styled<T: Render<Engine = Tui>>(pub Option<Style>, pub T);
|
|
||||||
impl Render for Styled<&str> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
|
|
||||||
Ok(Some([self.1.chars().count() as u16, 1]))
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
||||||
// FIXME
|
|
||||||
let [x, y, ..] = to.area();
|
|
||||||
//let [w, h] = self.min_size(to.area().wh())?.unwrap();
|
|
||||||
Ok(to.blit(&self.1, x, y, None))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub trait TuiStyle: Render<Engine = Tui> + Sized {
|
|
||||||
fn fg (self, color: Color) -> impl Render<Engine = Tui> {
|
|
||||||
Layers::new(move |add|{ add(&Foreground(color))?; add(&self) })
|
|
||||||
}
|
|
||||||
fn bg (self, color: Color) -> impl Render<Engine = Tui> {
|
|
||||||
Layers::new(move |add|{ add(&Background(color))?; add(&self) })
|
|
||||||
}
|
|
||||||
fn bold (self, on: bool) -> impl Render<Engine = Tui> {
|
|
||||||
Layers::new(move |add|{ add(&Bold(on))?; add(&self) })
|
|
||||||
}
|
|
||||||
fn border (self, style: impl BorderStyle) -> impl Render<Engine = Tui> {
|
|
||||||
Bordered(style, self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<W: Render<Engine = Tui>> TuiStyle for W {}
|
|
||||||
pub struct Bold(pub bool);
|
|
||||||
impl Render for Bold {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(Some([0,0])) }
|
|
||||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> { Ok(to.fill_bold(to.area(), self.0)) }
|
|
||||||
}
|
|
||||||
pub struct Foreground(pub Color);
|
|
||||||
impl Render for Foreground {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(Some([0,0])) }
|
|
||||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> { Ok(to.fill_fg(to.area(), self.0)) }
|
|
||||||
}
|
|
||||||
pub struct Background(pub Color);
|
|
||||||
impl Render for Background {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(Some([0,0])) }
|
|
||||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> { Ok(to.fill_bg(to.area(), self.0)) }
|
|
||||||
}
|
|
||||||
pub struct Border<S: BorderStyle>(pub S);
|
|
||||||
impl<S: BorderStyle> Render for Border<S> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
|
|
||||||
Ok(Some([0, 0]))
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
||||||
let area = to.area();
|
|
||||||
if area.w() > 0 && area.y() > 0 {
|
|
||||||
to.blit(&self.0.nw(), area.x(), area.y(), self.0.style());
|
|
||||||
to.blit(&self.0.ne(), area.x() + area.w() - 1, area.y(), self.0.style());
|
|
||||||
to.blit(&self.0.sw(), area.x(), area.y() + area.h() - 1, self.0.style());
|
|
||||||
to.blit(&self.0.se(), area.x() + area.w() - 1, area.y() + area.h() - 1, self.0.style());
|
|
||||||
for x in area.x()+1..area.x()+area.w()-1 {
|
|
||||||
to.blit(&self.0.n(), x, area.y(), self.0.style());
|
|
||||||
to.blit(&self.0.s(), x, area.y() + area.h() - 1, self.0.style());
|
|
||||||
}
|
|
||||||
for y in area.y()+1..area.y()+area.h()-1 {
|
|
||||||
to.blit(&self.0.w(), area.x(), y, self.0.style());
|
|
||||||
to.blit(&self.0.e(), area.x() + area.w() - 1, y, self.0.style());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub struct Bordered<S: BorderStyle, W: Render<Engine = Tui>>(pub S, pub W);
|
|
||||||
impl<S: BorderStyle, W: Render<Engine = Tui>> Content for Bordered<S, W> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Render<Engine = Tui> {
|
|
||||||
let content: &dyn Render<Engine = Tui> = &self.1;
|
|
||||||
lay! { content.inset_xy(1, 1), Border(self.0) }.fill_xy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub trait BorderStyle: Send + Sync + Copy {
|
|
||||||
const NW: &'static str = "";
|
|
||||||
const N: &'static str = "";
|
|
||||||
const NE: &'static str = "";
|
|
||||||
const E: &'static str = "";
|
|
||||||
const SE: &'static str = "";
|
|
||||||
const S: &'static str = "";
|
|
||||||
const SW: &'static str = "";
|
|
||||||
const W: &'static str = "";
|
|
||||||
fn n (&self) -> &str { Self::N }
|
|
||||||
fn s (&self) -> &str { Self::S }
|
|
||||||
fn e (&self) -> &str { Self::E }
|
|
||||||
fn w (&self) -> &str { Self::W }
|
|
||||||
fn nw (&self) -> &str { Self::NW }
|
|
||||||
fn ne (&self) -> &str { Self::NE }
|
|
||||||
fn sw (&self) -> &str { Self::SW }
|
|
||||||
fn se (&self) -> &str { Self::SE }
|
|
||||||
#[inline] fn draw <'a> (
|
|
||||||
&self, to: &mut TuiOutput
|
|
||||||
) -> Usually<()> {
|
|
||||||
self.draw_horizontal(to, None)?;
|
|
||||||
self.draw_vertical(to, None)?;
|
|
||||||
self.draw_corners(to, None)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[inline] fn draw_horizontal (
|
|
||||||
&self, to: &mut TuiOutput, style: Option<Style>
|
|
||||||
) -> Usually<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
let style = style.or_else(||self.style_horizontal());
|
|
||||||
let [x, x2, y, y2] = area.lrtb();
|
|
||||||
for x in x..x2.saturating_sub(1) {
|
|
||||||
self.draw_north(to, x, y, style);
|
|
||||||
self.draw_south(to, x, y2.saturating_sub(1), style);
|
|
||||||
}
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
#[inline] fn draw_north (
|
|
||||||
&self, to: &mut TuiOutput, x: u16, y: u16, style: Option<Style>
|
|
||||||
) -> () {
|
|
||||||
to.blit(&Self::N, x, y, style)
|
|
||||||
}
|
|
||||||
#[inline] fn draw_south (
|
|
||||||
&self, to: &mut TuiOutput, x: u16, y: u16, style: Option<Style>
|
|
||||||
) -> () {
|
|
||||||
to.blit(&Self::S, x, y, style)
|
|
||||||
}
|
|
||||||
#[inline] fn draw_vertical (
|
|
||||||
&self, to: &mut TuiOutput, style: Option<Style>
|
|
||||||
) -> Usually<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
let style = style.or_else(||self.style_vertical());
|
|
||||||
let [x, x2, y, y2] = area.lrtb();
|
|
||||||
for y in y..y2.saturating_sub(1) {
|
|
||||||
to.blit(&Self::W, x, y, style);
|
|
||||||
to.blit(&Self::E, x2.saturating_sub(1), y, style);
|
|
||||||
}
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
#[inline] fn draw_corners (
|
|
||||||
&self, to: &mut TuiOutput, style: Option<Style>
|
|
||||||
) -> Usually<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
let style = style.or_else(||self.style_corners());
|
|
||||||
let [x, y, width, height] = area.xywh();
|
|
||||||
if width > 0 && height > 0 {
|
|
||||||
to.blit(&Self::NW, x, y, style);
|
|
||||||
to.blit(&Self::NE, x + width - 1, y, style);
|
|
||||||
to.blit(&Self::SW, x, y + height - 1, style);
|
|
||||||
to.blit(&Self::SE, x + width - 1, y + height - 1, style);
|
|
||||||
}
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
#[inline] fn style (&self) -> Option<Style> { None }
|
|
||||||
#[inline] fn style_horizontal (&self) -> Option<Style> { self.style() }
|
|
||||||
#[inline] fn style_vertical (&self) -> Option<Style> { self.style() }
|
|
||||||
#[inline] fn style_corners (&self) -> Option<Style> { self.style() }
|
|
||||||
}
|
|
||||||
macro_rules! border {
|
|
||||||
($($T:ident {
|
|
||||||
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
|
|
||||||
$($x:tt)*
|
|
||||||
}),+) => {$(
|
|
||||||
impl BorderStyle for $T {
|
|
||||||
const NW: &'static str = $nw;
|
|
||||||
const N: &'static str = $n;
|
|
||||||
const NE: &'static str = $ne;
|
|
||||||
const W: &'static str = $w;
|
|
||||||
const E: &'static str = $e;
|
|
||||||
const SW: &'static str = $sw;
|
|
||||||
const S: &'static str = $s;
|
|
||||||
const SE: &'static str = $se;
|
|
||||||
$($x)*
|
|
||||||
}
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
pub struct $T(pub Style);
|
|
||||||
impl Render for $T {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(Some([0,0])) }
|
|
||||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> { self.draw(to) }
|
|
||||||
}
|
|
||||||
)+}
|
|
||||||
}
|
|
||||||
border! {
|
|
||||||
Square {
|
|
||||||
"┌" "─" "┐"
|
|
||||||
"│" "│"
|
|
||||||
"└" "─" "┘" fn style (&self) -> Option<Style> { Some(self.0) }
|
|
||||||
},
|
|
||||||
SquareBold {
|
|
||||||
"┏" "━" "┓"
|
|
||||||
"┃" "┃"
|
|
||||||
"┗" "━" "┛" fn style (&self) -> Option<Style> { Some(self.0) }
|
|
||||||
},
|
|
||||||
Tab {
|
|
||||||
"╭" "─" "╮"
|
|
||||||
"│" "│"
|
|
||||||
"│" " " "│" fn style (&self) -> Option<Style> { Some(self.0) }
|
|
||||||
},
|
|
||||||
Lozenge {
|
|
||||||
"╭" "─" "╮"
|
|
||||||
"│" "│"
|
|
||||||
"╰" "─" "╯" fn style (&self) -> Option<Style> { Some(self.0) }
|
|
||||||
},
|
|
||||||
Brace {
|
|
||||||
"╭" "" "╮"
|
|
||||||
"│" "│"
|
|
||||||
"╰" "" "╯" fn style (&self) -> Option<Style> { Some(self.0) }
|
|
||||||
},
|
|
||||||
LozengeDotted {
|
|
||||||
"╭" "┅" "╮"
|
|
||||||
"┇" "┇"
|
|
||||||
"╰" "┅" "╯" fn style (&self) -> Option<Style> { Some(self.0) }
|
|
||||||
},
|
|
||||||
Quarter {
|
|
||||||
"▎" "▔" "🮇"
|
|
||||||
"▎" "🮇"
|
|
||||||
"▎" "▁" "🮇" fn style (&self) -> Option<Style> { Some(self.0) }
|
|
||||||
},
|
|
||||||
QuarterV {
|
|
||||||
"▎" "" "🮇"
|
|
||||||
"▎" "🮇"
|
|
||||||
"▎" "" "🮇" fn style (&self) -> Option<Style> { Some(self.0) }
|
|
||||||
},
|
|
||||||
Chamfer {
|
|
||||||
"🭂" "▔" "🭍"
|
|
||||||
"▎" "🮇"
|
|
||||||
"🭓" "▁" "🭞" fn style (&self) -> Option<Style> { Some(self.0) }
|
|
||||||
},
|
|
||||||
Corners {
|
|
||||||
"🬆" "" "🬊" // 🬴 🬸
|
|
||||||
"" ""
|
|
||||||
"🬱" "" "🬵" fn style (&self) -> Option<Style> { Some(self.0) }
|
|
||||||
},
|
|
||||||
CornersTall {
|
|
||||||
"🭽" "" "🭾"
|
|
||||||
"" ""
|
|
||||||
"🭼" "" "🭿" fn style (&self) -> Option<Style> { Some(self.0) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub const CORNERS: CornersTall = CornersTall(Style {
|
|
||||||
fg: Some(Color::Rgb(96, 255, 32)),
|
|
||||||
bg: None,
|
|
||||||
underline_color: None,
|
|
||||||
add_modifier: Modifier::empty(),
|
|
||||||
sub_modifier: Modifier::DIM
|
|
||||||
});
|
|
||||||
/// Define a key
|
|
||||||
pub const fn key (code: KeyCode) -> KeyEvent {
|
|
||||||
let modifiers = KeyModifiers::NONE;
|
|
||||||
let kind = KeyEventKind::Press;
|
|
||||||
let state = KeyEventState::NONE;
|
|
||||||
KeyEvent { code, modifiers, kind, state }
|
|
||||||
}
|
|
||||||
/// Add Ctrl modifier to key
|
|
||||||
pub const fn ctrl (key: KeyEvent) -> KeyEvent {
|
|
||||||
KeyEvent { modifiers: key.modifiers.union(KeyModifiers::CONTROL), ..key }
|
|
||||||
}
|
|
||||||
/// Add Alt modifier to key
|
|
||||||
pub const fn alt (key: KeyEvent) -> KeyEvent {
|
|
||||||
KeyEvent { modifiers: key.modifiers.union(KeyModifiers::ALT), ..key }
|
|
||||||
}
|
|
||||||
/// Add Shift modifier to key
|
|
||||||
pub const fn shift (key: KeyEvent) -> KeyEvent {
|
|
||||||
KeyEvent { modifiers: key.modifiers.union(KeyModifiers::SHIFT), ..key }
|
|
||||||
}
|
|
||||||
/// Define a keymap
|
|
||||||
#[macro_export] macro_rules! keymap {
|
|
||||||
($T:ty { $([$k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr]),* $(,)? }) => {
|
|
||||||
&[
|
|
||||||
$((KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as KeyHandler<$T>)),*
|
|
||||||
] as &'static [KeyBinding<$T>]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Define a key in a keymap
|
|
||||||
#[macro_export] macro_rules! map_key {
|
|
||||||
($k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr) => {
|
|
||||||
(KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as &dyn Fn()->Usually<bool>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Shorthand for key match statement
|
|
||||||
#[macro_export] macro_rules! match_key {
|
|
||||||
($event:expr, {
|
|
||||||
$($key:pat=>$block:expr),* $(,)?
|
|
||||||
}) => {
|
|
||||||
match $event {
|
|
||||||
$(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
|
||||||
code: $key,
|
|
||||||
modifiers: crossterm::event::KeyModifiers::NONE,
|
|
||||||
kind: crossterm::event::KeyEventKind::Press,
|
|
||||||
state: crossterm::event::KeyEventState::NONE
|
|
||||||
}) => {
|
|
||||||
$block
|
|
||||||
})*
|
|
||||||
_ => Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Define key pattern in key match statement
|
|
||||||
#[macro_export] macro_rules! key {
|
|
||||||
($code:pat) => {
|
|
||||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
|
||||||
code: $code,
|
|
||||||
modifiers: crossterm::event::KeyModifiers::NONE,
|
|
||||||
kind: crossterm::event::KeyEventKind::Press,
|
|
||||||
state: crossterm::event::KeyEventState::NONE
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
(Ctrl-$code:pat) => {
|
|
||||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
|
||||||
code: $code,
|
|
||||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
|
||||||
kind: crossterm::event::KeyEventKind::Press,
|
|
||||||
state: crossterm::event::KeyEventState::NONE
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
(Alt-$code:pat) => {
|
|
||||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
|
||||||
code: $code,
|
|
||||||
modifiers: crossterm::event::KeyModifiers::ALT,
|
|
||||||
kind: crossterm::event::KeyEventKind::Press,
|
|
||||||
state: crossterm::event::KeyEventState::NONE
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
(Shift-$code:pat) => {
|
|
||||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
|
||||||
code: $code,
|
|
||||||
modifiers: crossterm::event::KeyModifiers::SHIFT,
|
|
||||||
kind: crossterm::event::KeyEventKind::Press,
|
|
||||||
state: crossterm::event::KeyEventState::NONE
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[macro_export] macro_rules! key_lit {
|
|
||||||
($code:expr) => {
|
|
||||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
|
||||||
code: $code,
|
|
||||||
modifiers: crossterm::event::KeyModifiers::NONE,
|
|
||||||
kind: crossterm::event::KeyEventKind::Press,
|
|
||||||
state: crossterm::event::KeyEventState::NONE
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
(Ctrl-$code:expr) => {
|
|
||||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
|
||||||
code: $code,
|
|
||||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
|
||||||
kind: crossterm::event::KeyEventKind::Press,
|
|
||||||
state: crossterm::event::KeyEventState::NONE
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
(Alt-$code:expr) => {
|
|
||||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
|
||||||
code: $code,
|
|
||||||
modifiers: crossterm::event::KeyModifiers::ALT,
|
|
||||||
kind: crossterm::event::KeyEventKind::Press,
|
|
||||||
state: crossterm::event::KeyEventState::NONE
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
(Shift-$code:expr) => {
|
|
||||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
|
||||||
code: $code,
|
|
||||||
modifiers: crossterm::event::KeyModifiers::SHIFT,
|
|
||||||
kind: crossterm::event::KeyEventKind::Press,
|
|
||||||
state: crossterm::event::KeyEventState::NONE
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -15,3 +15,5 @@ vst = "0.4.0"
|
||||||
#vst3 = "0.1.0"
|
#vst3 = "0.1.0"
|
||||||
wavers = "1.4.3"
|
wavers = "1.4.3"
|
||||||
winit = { version = "0.30.4", features = [ "x11" ] }
|
winit = { version = "0.30.4", features = [ "x11" ] }
|
||||||
|
crossterm = "0.27"
|
||||||
|
better-panic = "0.3.0"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
|
pub(crate) use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
pub(crate) use tek_core::midly::{num::u7, live::LiveEvent, MidiMessage};
|
pub(crate) use tek_core::midly::{num::u7, live::LiveEvent, MidiMessage};
|
||||||
pub(crate) use tek_core::{*, jack::*};
|
pub(crate) use tek_core::{*, jack::*};
|
||||||
pub(crate) use tek_api::*;
|
pub(crate) use tek_api::*;
|
||||||
|
|
@ -8,8 +8,11 @@ pub(crate) use std::sync::{Arc, Mutex, RwLock};
|
||||||
pub(crate) use std::path::PathBuf;
|
pub(crate) use std::path::PathBuf;
|
||||||
pub(crate) use std::ffi::OsString;
|
pub(crate) use std::ffi::OsString;
|
||||||
pub(crate) use std::fs::read_dir;
|
pub(crate) use std::fs::read_dir;
|
||||||
|
pub(crate) use better_panic::{Settings, Verbosity};
|
||||||
|
|
||||||
submod! {
|
submod! {
|
||||||
|
tui
|
||||||
|
|
||||||
tui_app_arranger
|
tui_app_arranger
|
||||||
tui_app_sequencer
|
tui_app_sequencer
|
||||||
tui_app_transport
|
tui_app_transport
|
||||||
|
|
@ -140,7 +143,7 @@ pub fn to_focus_command (input: &TuiInput) -> Option<FocusCommand> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait StatusBar: Render<Engine = Tui> {
|
pub trait StatusBar: Render<Engine = Tui> {
|
||||||
type State;
|
type State: Send + Sync;
|
||||||
fn hotkey_fg () -> Color where Self: Sized;
|
fn hotkey_fg () -> Color where Self: Sized;
|
||||||
fn update (&mut self, state: &Self::State) where Self: Sized;
|
fn update (&mut self, state: &Self::State) where Self: Sized;
|
||||||
fn command (commands: &[[impl Render<Engine = Tui>;3]])
|
fn command (commands: &[[impl Render<Engine = Tui>;3]])
|
||||||
|
|
@ -164,7 +167,10 @@ pub trait StatusBar: Render<Engine = Tui> {
|
||||||
fn with <'a> (state: &'a Self::State, content: impl Render<Engine=Tui>) -> impl Render<Engine=Tui>
|
fn with <'a> (state: &'a Self::State, content: impl Render<Engine=Tui>) -> impl Render<Engine=Tui>
|
||||||
where Self: Sized, &'a Self::State: Into<Self>
|
where Self: Sized, &'a Self::State: Into<Self>
|
||||||
{
|
{
|
||||||
Split::up(1, state.into(), content)
|
Stack::up(move |add|{
|
||||||
|
add(&state.into())?;
|
||||||
|
add(&content)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
693
crates/tek_tui/src/tui.rs
Normal file
693
crates/tek_tui/src/tui.rs
Normal file
|
|
@ -0,0 +1,693 @@
|
||||||
|
use crate::*;
|
||||||
|
pub(crate) use std::io::{stdout};
|
||||||
|
pub(crate) use std::thread::{spawn, JoinHandle};
|
||||||
|
pub(crate) use std::time::Duration;
|
||||||
|
pub(crate) use ratatui::buffer::Cell;
|
||||||
|
pub(crate) use crossterm::{ExecutableCommand};
|
||||||
|
pub use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState};
|
||||||
|
pub use ratatui::prelude::{Rect, Style, Color, Buffer};
|
||||||
|
pub use ratatui::style::{Stylize, Modifier};
|
||||||
|
use ratatui::backend::{Backend, CrosstermBackend, ClearType};
|
||||||
|
use std::io::Stdout;
|
||||||
|
use crossterm::terminal::{
|
||||||
|
EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
|
enable_raw_mode, disable_raw_mode
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Tui {
|
||||||
|
pub exited: Arc<AtomicBool>,
|
||||||
|
pub buffer: Buffer,
|
||||||
|
pub backend: CrosstermBackend<Stdout>,
|
||||||
|
pub area: [u16;4], // FIXME auto resize
|
||||||
|
}
|
||||||
|
impl 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.xywh(),
|
||||||
|
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!(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.xywh() };
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct TuiInput { event: TuiEvent, exited: Arc<AtomicBool>, }
|
||||||
|
impl Input<Tui> for TuiInput {
|
||||||
|
type Event = TuiEvent;
|
||||||
|
fn event (&self) -> &TuiEvent { &self.event }
|
||||||
|
fn is_done (&self) -> bool { self.exited.fetch_and(true, Ordering::Relaxed) }
|
||||||
|
fn done (&self) { self.exited.store(true, Ordering::Relaxed); }
|
||||||
|
}
|
||||||
|
impl TuiInput {
|
||||||
|
// TODO remove
|
||||||
|
pub fn handle_keymap <T> (&self, state: &mut T, keymap: &KeyMap<T>) -> Usually<bool> {
|
||||||
|
match self.event() {
|
||||||
|
TuiEvent::Input(crossterm::event::Event::Key(event)) => {
|
||||||
|
for (code, modifiers, _, _, command) in keymap.iter() {
|
||||||
|
if *code == event.code && modifiers.bits() == event.modifiers.bits() {
|
||||||
|
return command(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub type KeyHandler<T> = &'static dyn Fn(&mut T)->Usually<bool>;
|
||||||
|
pub type KeyBinding<T> = (KeyCode, KeyModifiers, &'static str, &'static str, KeyHandler<T>);
|
||||||
|
pub type KeyMap<T> = [KeyBinding<T>];
|
||||||
|
pub struct TuiOutput { pub buffer: Buffer, pub area: [u16;4] }
|
||||||
|
impl Output<Tui> for TuiOutput {
|
||||||
|
#[inline] fn area (&self) -> [u16;4] { self.area }
|
||||||
|
#[inline] fn area_mut (&mut self) -> &mut [u16;4] { &mut self.area }
|
||||||
|
#[inline] fn render_in (&mut self,
|
||||||
|
area: [u16;4],
|
||||||
|
widget: &dyn Render<Engine = Tui>
|
||||||
|
) -> Usually<()> {
|
||||||
|
let last = self.area();
|
||||||
|
*self.area_mut() = area;
|
||||||
|
widget.render(self)?;
|
||||||
|
*self.area_mut() = last;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TuiOutput {
|
||||||
|
pub fn buffer_update (&mut self,
|
||||||
|
area: [u16;4],
|
||||||
|
callback: &impl Fn(&mut Cell, u16, u16)
|
||||||
|
) {
|
||||||
|
buffer_update(&mut self.buffer, area, callback);
|
||||||
|
}
|
||||||
|
pub fn fill_bold (&mut self, area: [u16;4], on: bool) {
|
||||||
|
if on {
|
||||||
|
self.buffer_update(area, &|cell,_,_|cell.modifier.insert(Modifier::BOLD))
|
||||||
|
} else {
|
||||||
|
self.buffer_update(area, &|cell,_,_|cell.modifier.remove(Modifier::BOLD))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn fill_bg (&mut self, area: [u16;4], color: Color) {
|
||||||
|
self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);})
|
||||||
|
}
|
||||||
|
pub fn fill_fg (&mut self, area: [u16;4], color: Color) {
|
||||||
|
self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);})
|
||||||
|
}
|
||||||
|
pub fn fill_ul (&mut self, area: [u16;4], color: Color) {
|
||||||
|
self.buffer_update(area, &|cell,_,_|{
|
||||||
|
cell.modifier = ratatui::prelude::Modifier::UNDERLINED;
|
||||||
|
cell.underline_color = color;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn fill_char (&mut self, area: [u16;4], c: char) {
|
||||||
|
self.buffer_update(area, &|cell,_,_|{cell.set_char(c);})
|
||||||
|
}
|
||||||
|
pub fn make_dim (&mut self) {
|
||||||
|
for cell in self.buffer.content.iter_mut() {
|
||||||
|
cell.bg = ratatui::style::Color::Rgb(30,30,30);
|
||||||
|
cell.fg = ratatui::style::Color::Rgb(100,100,100);
|
||||||
|
cell.modifier = ratatui::style::Modifier::DIM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn blit (
|
||||||
|
&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>
|
||||||
|
) {
|
||||||
|
let text = text.as_ref();
|
||||||
|
let buf = &mut self.buffer;
|
||||||
|
if x < buf.area.width && y < buf.area.height {
|
||||||
|
buf.set_string(x, y, text, style.unwrap_or(Style::default()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn with_rect (&mut self, area: [u16;4]) -> &mut Self {
|
||||||
|
self.area = area;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TuiEvent {
|
||||||
|
/// Terminal input
|
||||||
|
Input(::crossterm::event::Event),
|
||||||
|
/// Update values but not the whole form.
|
||||||
|
Update,
|
||||||
|
/// Update the whole form.
|
||||||
|
Redraw,
|
||||||
|
/// Device gains focus
|
||||||
|
Focus,
|
||||||
|
/// Device loses focus
|
||||||
|
Blur,
|
||||||
|
// /// JACK notification
|
||||||
|
// Jack(JackEvent)
|
||||||
|
}
|
||||||
|
//impl Area<u16> for Rect {
|
||||||
|
//fn x (&self) -> u16 { self.x }
|
||||||
|
//fn y (&self) -> u16 { self.y }
|
||||||
|
//fn w (&self) -> u16 { self.width }
|
||||||
|
//fn h (&self) -> u16 { self.height }
|
||||||
|
//}
|
||||||
|
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
|
||||||
|
match (lower, upper) {
|
||||||
|
(true, true) => Some('█'),
|
||||||
|
(true, false) => Some('▄'),
|
||||||
|
(false, true) => Some('▀'),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct BigBuffer {
|
||||||
|
pub width: usize,
|
||||||
|
pub height: usize,
|
||||||
|
pub content: Vec<Cell>
|
||||||
|
}
|
||||||
|
impl BigBuffer {
|
||||||
|
pub fn new (width: usize, height: usize) -> Self {
|
||||||
|
Self { width, height, content: vec![Cell::default(); width*height] }
|
||||||
|
}
|
||||||
|
pub fn get (&self, x: usize, y: usize) -> Option<&Cell> {
|
||||||
|
let i = self.index_of(x, y);
|
||||||
|
self.content.get(i)
|
||||||
|
}
|
||||||
|
pub fn get_mut (&mut self, x: usize, y: usize) -> Option<&mut Cell> {
|
||||||
|
let i = self.index_of(x, y);
|
||||||
|
self.content.get_mut(i)
|
||||||
|
}
|
||||||
|
pub fn index_of (&self, x: usize, y: usize) -> usize {
|
||||||
|
y * self.width + x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
|
||||||
|
for row in 0..area.h() {
|
||||||
|
let y = area.y() + row;
|
||||||
|
for col in 0..area.w() {
|
||||||
|
let x = area.x() + col;
|
||||||
|
if x < buf.area.width && y < buf.area.height {
|
||||||
|
callback(buf.get_mut(x, y), col, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//impl Render for &str {
|
||||||
|
//type Engine = Tui;
|
||||||
|
//fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
|
||||||
|
//// TODO: line breaks
|
||||||
|
//Ok(Some([self.chars().count() as u16, 1]))
|
||||||
|
//}
|
||||||
|
//fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||||
|
//let [x, y, ..] = to.area();
|
||||||
|
////let [w, h] = self.min_size(to.area().wh())?.unwrap();
|
||||||
|
//Ok(to.blit(&self, x, y, None))
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//impl Render for String {
|
||||||
|
//type Engine = Tui;
|
||||||
|
//fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
|
||||||
|
//// TODO: line breaks
|
||||||
|
//Ok(Some([self.chars().count() as u16, 1]))
|
||||||
|
//}
|
||||||
|
//fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||||
|
//let [x, y, ..] = to.area();
|
||||||
|
////let [w, h] = self.min_size(to.area().wh())?.unwrap();
|
||||||
|
//Ok(to.blit(&self, x, y, None))
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//impl<T: Render<Engine = Tui>> Render for DebugOverlay<Tui, T> {
|
||||||
|
//type Engine = Tui;
|
||||||
|
//fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||||
|
//self.0.min_size(to)
|
||||||
|
//}
|
||||||
|
//fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||||
|
//let [x, y, w, h] = to.area();
|
||||||
|
//self.0.render(to)?;
|
||||||
|
//Ok(to.blit(&format!("{w}x{h}+{x}+{y}"), x, y, Some(Style::default().green())))
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
pub struct Styled<T: Render<Engine = Tui>>(pub Option<Style>, pub T);
|
||||||
|
impl Render for Styled<&str> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
|
||||||
|
Ok(Some([self.1.chars().count() as u16, 1]))
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||||
|
// FIXME
|
||||||
|
let [x, y, ..] = to.area();
|
||||||
|
//let [w, h] = self.min_size(to.area().wh())?.unwrap();
|
||||||
|
Ok(to.blit(&self.1, x, y, None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub trait TuiStyle: Render<Engine = Tui> + Sized {
|
||||||
|
fn fg (self, color: Color) -> impl Render<Engine = Tui> {
|
||||||
|
Layers::new(move |add|{ add(&Foreground(color))?; add(&self) })
|
||||||
|
}
|
||||||
|
fn bg (self, color: Color) -> impl Render<Engine = Tui> {
|
||||||
|
Layers::new(move |add|{ add(&Background(color))?; add(&self) })
|
||||||
|
}
|
||||||
|
fn bold (self, on: bool) -> impl Render<Engine = Tui> {
|
||||||
|
Layers::new(move |add|{ add(&Bold(on))?; add(&self) })
|
||||||
|
}
|
||||||
|
fn border (self, style: impl BorderStyle) -> impl Render<Engine = Tui> {
|
||||||
|
Bordered(style, self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<W: Render<Engine = Tui>> TuiStyle for W {}
|
||||||
|
pub struct Bold(pub bool);
|
||||||
|
impl Render for Bold {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(Some([0,0])) }
|
||||||
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> { Ok(to.fill_bold(to.area(), self.0)) }
|
||||||
|
}
|
||||||
|
pub struct Foreground(pub Color);
|
||||||
|
impl Render for Foreground {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(Some([0,0])) }
|
||||||
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> { Ok(to.fill_fg(to.area(), self.0)) }
|
||||||
|
}
|
||||||
|
pub struct Background(pub Color);
|
||||||
|
impl Render for Background {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(Some([0,0])) }
|
||||||
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> { Ok(to.fill_bg(to.area(), self.0)) }
|
||||||
|
}
|
||||||
|
pub struct Border<S: BorderStyle>(pub S);
|
||||||
|
impl<S: BorderStyle> Render for Border<S> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
|
||||||
|
Ok(Some([0, 0]))
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||||
|
let area = to.area();
|
||||||
|
if area.w() > 0 && area.y() > 0 {
|
||||||
|
to.blit(&self.0.nw(), area.x(), area.y(), self.0.style());
|
||||||
|
to.blit(&self.0.ne(), area.x() + area.w() - 1, area.y(), self.0.style());
|
||||||
|
to.blit(&self.0.sw(), area.x(), area.y() + area.h() - 1, self.0.style());
|
||||||
|
to.blit(&self.0.se(), area.x() + area.w() - 1, area.y() + area.h() - 1, self.0.style());
|
||||||
|
for x in area.x()+1..area.x()+area.w()-1 {
|
||||||
|
to.blit(&self.0.n(), x, area.y(), self.0.style());
|
||||||
|
to.blit(&self.0.s(), x, area.y() + area.h() - 1, self.0.style());
|
||||||
|
}
|
||||||
|
for y in area.y()+1..area.y()+area.h()-1 {
|
||||||
|
to.blit(&self.0.w(), area.x(), y, self.0.style());
|
||||||
|
to.blit(&self.0.e(), area.x() + area.w() - 1, y, self.0.style());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct Bordered<S: BorderStyle, W: Render<Engine = Tui>>(pub S, pub W);
|
||||||
|
impl<S: BorderStyle, W: Render<Engine = Tui>> Content for Bordered<S, W> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Render<Engine = Tui> {
|
||||||
|
let content: &dyn Render<Engine = Tui> = &self.1;
|
||||||
|
lay! { content.inset_xy(1, 1), Border(self.0) }.fill_xy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub trait BorderStyle: Send + Sync + Copy {
|
||||||
|
const NW: &'static str = "";
|
||||||
|
const N: &'static str = "";
|
||||||
|
const NE: &'static str = "";
|
||||||
|
const E: &'static str = "";
|
||||||
|
const SE: &'static str = "";
|
||||||
|
const S: &'static str = "";
|
||||||
|
const SW: &'static str = "";
|
||||||
|
const W: &'static str = "";
|
||||||
|
fn n (&self) -> &str { Self::N }
|
||||||
|
fn s (&self) -> &str { Self::S }
|
||||||
|
fn e (&self) -> &str { Self::E }
|
||||||
|
fn w (&self) -> &str { Self::W }
|
||||||
|
fn nw (&self) -> &str { Self::NW }
|
||||||
|
fn ne (&self) -> &str { Self::NE }
|
||||||
|
fn sw (&self) -> &str { Self::SW }
|
||||||
|
fn se (&self) -> &str { Self::SE }
|
||||||
|
#[inline] fn draw <'a> (
|
||||||
|
&self, to: &mut TuiOutput
|
||||||
|
) -> Usually<()> {
|
||||||
|
self.draw_horizontal(to, None)?;
|
||||||
|
self.draw_vertical(to, None)?;
|
||||||
|
self.draw_corners(to, None)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[inline] fn draw_horizontal (
|
||||||
|
&self, to: &mut TuiOutput, style: Option<Style>
|
||||||
|
) -> Usually<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
let style = style.or_else(||self.style_horizontal());
|
||||||
|
let [x, x2, y, y2] = area.lrtb();
|
||||||
|
for x in x..x2.saturating_sub(1) {
|
||||||
|
self.draw_north(to, x, y, style);
|
||||||
|
self.draw_south(to, x, y2.saturating_sub(1), style);
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
#[inline] fn draw_north (
|
||||||
|
&self, to: &mut TuiOutput, x: u16, y: u16, style: Option<Style>
|
||||||
|
) -> () {
|
||||||
|
to.blit(&Self::N, x, y, style)
|
||||||
|
}
|
||||||
|
#[inline] fn draw_south (
|
||||||
|
&self, to: &mut TuiOutput, x: u16, y: u16, style: Option<Style>
|
||||||
|
) -> () {
|
||||||
|
to.blit(&Self::S, x, y, style)
|
||||||
|
}
|
||||||
|
#[inline] fn draw_vertical (
|
||||||
|
&self, to: &mut TuiOutput, style: Option<Style>
|
||||||
|
) -> Usually<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
let style = style.or_else(||self.style_vertical());
|
||||||
|
let [x, x2, y, y2] = area.lrtb();
|
||||||
|
for y in y..y2.saturating_sub(1) {
|
||||||
|
to.blit(&Self::W, x, y, style);
|
||||||
|
to.blit(&Self::E, x2.saturating_sub(1), y, style);
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
#[inline] fn draw_corners (
|
||||||
|
&self, to: &mut TuiOutput, style: Option<Style>
|
||||||
|
) -> Usually<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
let style = style.or_else(||self.style_corners());
|
||||||
|
let [x, y, width, height] = area.xywh();
|
||||||
|
if width > 0 && height > 0 {
|
||||||
|
to.blit(&Self::NW, x, y, style);
|
||||||
|
to.blit(&Self::NE, x + width - 1, y, style);
|
||||||
|
to.blit(&Self::SW, x, y + height - 1, style);
|
||||||
|
to.blit(&Self::SE, x + width - 1, y + height - 1, style);
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
#[inline] fn style (&self) -> Option<Style> { None }
|
||||||
|
#[inline] fn style_horizontal (&self) -> Option<Style> { self.style() }
|
||||||
|
#[inline] fn style_vertical (&self) -> Option<Style> { self.style() }
|
||||||
|
#[inline] fn style_corners (&self) -> Option<Style> { self.style() }
|
||||||
|
}
|
||||||
|
macro_rules! border {
|
||||||
|
($($T:ident {
|
||||||
|
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
|
||||||
|
$($x:tt)*
|
||||||
|
}),+) => {$(
|
||||||
|
impl BorderStyle for $T {
|
||||||
|
const NW: &'static str = $nw;
|
||||||
|
const N: &'static str = $n;
|
||||||
|
const NE: &'static str = $ne;
|
||||||
|
const W: &'static str = $w;
|
||||||
|
const E: &'static str = $e;
|
||||||
|
const SW: &'static str = $sw;
|
||||||
|
const S: &'static str = $s;
|
||||||
|
const SE: &'static str = $se;
|
||||||
|
$($x)*
|
||||||
|
}
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct $T(pub Style);
|
||||||
|
impl Render for $T {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(Some([0,0])) }
|
||||||
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> { self.draw(to) }
|
||||||
|
}
|
||||||
|
)+}
|
||||||
|
}
|
||||||
|
border! {
|
||||||
|
Square {
|
||||||
|
"┌" "─" "┐"
|
||||||
|
"│" "│"
|
||||||
|
"└" "─" "┘" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||||
|
},
|
||||||
|
SquareBold {
|
||||||
|
"┏" "━" "┓"
|
||||||
|
"┃" "┃"
|
||||||
|
"┗" "━" "┛" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||||
|
},
|
||||||
|
Tab {
|
||||||
|
"╭" "─" "╮"
|
||||||
|
"│" "│"
|
||||||
|
"│" " " "│" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||||
|
},
|
||||||
|
Lozenge {
|
||||||
|
"╭" "─" "╮"
|
||||||
|
"│" "│"
|
||||||
|
"╰" "─" "╯" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||||
|
},
|
||||||
|
Brace {
|
||||||
|
"╭" "" "╮"
|
||||||
|
"│" "│"
|
||||||
|
"╰" "" "╯" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||||
|
},
|
||||||
|
LozengeDotted {
|
||||||
|
"╭" "┅" "╮"
|
||||||
|
"┇" "┇"
|
||||||
|
"╰" "┅" "╯" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||||
|
},
|
||||||
|
Quarter {
|
||||||
|
"▎" "▔" "🮇"
|
||||||
|
"▎" "🮇"
|
||||||
|
"▎" "▁" "🮇" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||||
|
},
|
||||||
|
QuarterV {
|
||||||
|
"▎" "" "🮇"
|
||||||
|
"▎" "🮇"
|
||||||
|
"▎" "" "🮇" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||||
|
},
|
||||||
|
Chamfer {
|
||||||
|
"🭂" "▔" "🭍"
|
||||||
|
"▎" "🮇"
|
||||||
|
"🭓" "▁" "🭞" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||||
|
},
|
||||||
|
Corners {
|
||||||
|
"🬆" "" "🬊" // 🬴 🬸
|
||||||
|
"" ""
|
||||||
|
"🬱" "" "🬵" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||||
|
},
|
||||||
|
CornersTall {
|
||||||
|
"🭽" "" "🭾"
|
||||||
|
"" ""
|
||||||
|
"🭼" "" "🭿" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub const CORNERS: CornersTall = CornersTall(Style {
|
||||||
|
fg: Some(Color::Rgb(96, 255, 32)),
|
||||||
|
bg: None,
|
||||||
|
underline_color: None,
|
||||||
|
add_modifier: Modifier::empty(),
|
||||||
|
sub_modifier: Modifier::DIM
|
||||||
|
});
|
||||||
|
/// Define a key
|
||||||
|
pub const fn key (code: KeyCode) -> KeyEvent {
|
||||||
|
let modifiers = KeyModifiers::NONE;
|
||||||
|
let kind = KeyEventKind::Press;
|
||||||
|
let state = KeyEventState::NONE;
|
||||||
|
KeyEvent { code, modifiers, kind, state }
|
||||||
|
}
|
||||||
|
/// Add Ctrl modifier to key
|
||||||
|
pub const fn ctrl (key: KeyEvent) -> KeyEvent {
|
||||||
|
KeyEvent { modifiers: key.modifiers.union(KeyModifiers::CONTROL), ..key }
|
||||||
|
}
|
||||||
|
/// Add Alt modifier to key
|
||||||
|
pub const fn alt (key: KeyEvent) -> KeyEvent {
|
||||||
|
KeyEvent { modifiers: key.modifiers.union(KeyModifiers::ALT), ..key }
|
||||||
|
}
|
||||||
|
/// Add Shift modifier to key
|
||||||
|
pub const fn shift (key: KeyEvent) -> KeyEvent {
|
||||||
|
KeyEvent { modifiers: key.modifiers.union(KeyModifiers::SHIFT), ..key }
|
||||||
|
}
|
||||||
|
/// Define a keymap
|
||||||
|
#[macro_export] macro_rules! keymap {
|
||||||
|
($T:ty { $([$k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr]),* $(,)? }) => {
|
||||||
|
&[
|
||||||
|
$((KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as KeyHandler<$T>)),*
|
||||||
|
] as &'static [KeyBinding<$T>]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Define a key in a keymap
|
||||||
|
#[macro_export] macro_rules! map_key {
|
||||||
|
($k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr) => {
|
||||||
|
(KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as &dyn Fn()->Usually<bool>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Shorthand for key match statement
|
||||||
|
#[macro_export] macro_rules! match_key {
|
||||||
|
($event:expr, {
|
||||||
|
$($key:pat=>$block:expr),* $(,)?
|
||||||
|
}) => {
|
||||||
|
match $event {
|
||||||
|
$(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||||
|
code: $key,
|
||||||
|
modifiers: crossterm::event::KeyModifiers::NONE,
|
||||||
|
kind: crossterm::event::KeyEventKind::Press,
|
||||||
|
state: crossterm::event::KeyEventState::NONE
|
||||||
|
}) => {
|
||||||
|
$block
|
||||||
|
})*
|
||||||
|
_ => Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Define key pattern in key match statement
|
||||||
|
#[macro_export] macro_rules! key {
|
||||||
|
($code:pat) => {
|
||||||
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||||
|
code: $code,
|
||||||
|
modifiers: crossterm::event::KeyModifiers::NONE,
|
||||||
|
kind: crossterm::event::KeyEventKind::Press,
|
||||||
|
state: crossterm::event::KeyEventState::NONE
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
(Ctrl-$code:pat) => {
|
||||||
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||||
|
code: $code,
|
||||||
|
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||||
|
kind: crossterm::event::KeyEventKind::Press,
|
||||||
|
state: crossterm::event::KeyEventState::NONE
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
(Alt-$code:pat) => {
|
||||||
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||||
|
code: $code,
|
||||||
|
modifiers: crossterm::event::KeyModifiers::ALT,
|
||||||
|
kind: crossterm::event::KeyEventKind::Press,
|
||||||
|
state: crossterm::event::KeyEventState::NONE
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
(Shift-$code:pat) => {
|
||||||
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||||
|
code: $code,
|
||||||
|
modifiers: crossterm::event::KeyModifiers::SHIFT,
|
||||||
|
kind: crossterm::event::KeyEventKind::Press,
|
||||||
|
state: crossterm::event::KeyEventState::NONE
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[macro_export] macro_rules! key_lit {
|
||||||
|
($code:expr) => {
|
||||||
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||||
|
code: $code,
|
||||||
|
modifiers: crossterm::event::KeyModifiers::NONE,
|
||||||
|
kind: crossterm::event::KeyEventKind::Press,
|
||||||
|
state: crossterm::event::KeyEventState::NONE
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
(Ctrl-$code:expr) => {
|
||||||
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||||
|
code: $code,
|
||||||
|
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||||
|
kind: crossterm::event::KeyEventKind::Press,
|
||||||
|
state: crossterm::event::KeyEventState::NONE
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
(Alt-$code:expr) => {
|
||||||
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||||
|
code: $code,
|
||||||
|
modifiers: crossterm::event::KeyModifiers::ALT,
|
||||||
|
kind: crossterm::event::KeyEventKind::Press,
|
||||||
|
state: crossterm::event::KeyEventState::NONE
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
(Shift-$code:expr) => {
|
||||||
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||||
|
code: $code,
|
||||||
|
modifiers: crossterm::event::KeyModifiers::SHIFT,
|
||||||
|
kind: crossterm::event::KeyEventKind::Press,
|
||||||
|
state: crossterm::event::KeyEventState::NONE
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -115,11 +115,12 @@ impl_focus!(SequencerTui SequencerFocus [
|
||||||
/// Status bar for sequencer app
|
/// Status bar for sequencer app
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SequencerStatusBar {
|
pub struct SequencerStatusBar {
|
||||||
pub(crate) cpu: Option<String>,
|
pub(crate) cpu: Option<String>,
|
||||||
pub(crate) size: String,
|
pub(crate) width: usize,
|
||||||
pub(crate) res: String,
|
pub(crate) size: String,
|
||||||
pub(crate) mode: &'static str,
|
pub(crate) res: String,
|
||||||
pub(crate) help: &'static [(&'static str, &'static str, &'static str)]
|
pub(crate) mode: &'static str,
|
||||||
|
pub(crate) help: &'static [(&'static str, &'static str, &'static str)]
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusBar for SequencerStatusBar {
|
impl StatusBar for SequencerStatusBar {
|
||||||
|
|
@ -139,10 +140,12 @@ impl From<&SequencerTui> for SequencerStatusBar {
|
||||||
let samples = state.clock.chunk.load(Ordering::Relaxed);
|
let samples = state.clock.chunk.load(Ordering::Relaxed);
|
||||||
let rate = state.clock.timebase.sr.get() as f64;
|
let rate = state.clock.timebase.sr.get() as f64;
|
||||||
let buffer = samples as f64 / rate;
|
let buffer = samples as f64 / rate;
|
||||||
|
let width = state.size.w();
|
||||||
let default_help = &[("", "⏎", " enter"), ("", "✣", " navigate")];
|
let default_help = &[("", "⏎", " enter"), ("", "✣", " navigate")];
|
||||||
Self {
|
Self {
|
||||||
|
width,
|
||||||
cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")),
|
cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")),
|
||||||
size: format!("{}x{}│", state.size.w(), state.size.h()),
|
size: format!("{}x{}│", width, state.size.h()),
|
||||||
res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.),
|
res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.),
|
||||||
mode: match state.focused() {
|
mode: match state.focused() {
|
||||||
Transport(PlayPause) => " PLAY/PAUSE ",
|
Transport(PlayPause) => " PLAY/PAUSE ",
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ pub struct PhrasePlayerModel {
|
||||||
/// State of clock and playhead
|
/// State of clock and playhead
|
||||||
pub(crate) clock: ClockModel,
|
pub(crate) clock: ClockModel,
|
||||||
/// Start time and phrase being played
|
/// Start time and phrase being played
|
||||||
pub(crate) play_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
|
pub(crate) play_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
||||||
/// Start time and next phrase
|
/// Start time and next phrase
|
||||||
pub(crate) next_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
|
pub(crate) next_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
||||||
/// Play input through output.
|
/// Play input through output.
|
||||||
pub(crate) monitoring: bool,
|
pub(crate) monitoring: bool,
|
||||||
/// Write input to sequence.
|
/// Write input to sequence.
|
||||||
|
|
@ -70,16 +70,16 @@ impl HasPlayPhrase for PhrasePlayerModel {
|
||||||
fn reset_mut (&mut self) -> &mut bool {
|
fn reset_mut (&mut self) -> &mut bool {
|
||||||
&mut self.reset
|
&mut self.reset
|
||||||
}
|
}
|
||||||
fn play_phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
|
fn play_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<Phrase>>>)> {
|
||||||
&self.play_phrase
|
&self.play_phrase
|
||||||
}
|
}
|
||||||
fn play_phrase_mut (&mut self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
|
fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)> {
|
||||||
&mut self.play_phrase
|
&mut self.play_phrase
|
||||||
}
|
}
|
||||||
fn next_phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
|
fn next_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<Phrase>>>)> {
|
||||||
&self.next_phrase
|
&self.next_phrase
|
||||||
}
|
}
|
||||||
fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
|
fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)> {
|
||||||
&mut self.next_phrase
|
&mut self.next_phrase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::*;
|
||||||
|
|
||||||
pub struct PhraseSelector<'a> {
|
pub struct PhraseSelector<'a> {
|
||||||
pub(crate) title: &'static str,
|
pub(crate) title: &'static str,
|
||||||
pub(crate) phrase: &'a Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
|
pub(crate) phrase: &'a Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
||||||
pub(crate) focused: bool,
|
pub(crate) focused: bool,
|
||||||
pub(crate) entered: bool,
|
pub(crate) entered: bool,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,35 @@
|
||||||
|
//]))
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
impl Content for SequencerTui {
|
impl Content for SequencerTui {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Render<Engine = Tui> {
|
fn content (&self) -> impl Render<Engine = Tui> {
|
||||||
lay!(self.size, SequencerStatusBar::with(self, col!(
|
let play = PhraseSelector::play_phrase(
|
||||||
TransportView::from(self),
|
&self.player,
|
||||||
Split::right(20,
|
self.focused() == SequencerFocus::PhrasePlay,
|
||||||
col_up!(
|
self.entered()
|
||||||
PhraseSelector::play_phrase(
|
);
|
||||||
&self.player,
|
let next = PhraseSelector::next_phrase(
|
||||||
self.focused() == SequencerFocus::PhrasePlay,
|
&self.player,
|
||||||
self.entered()
|
self.focused() == SequencerFocus::PhraseNext,
|
||||||
).fixed_y(4),
|
self.entered()
|
||||||
PhraseSelector::next_phrase(
|
);
|
||||||
&self.player,
|
Stack::Up(
|
||||||
self.focused() == SequencerFocus::PhraseNext,
|
SequencerStatusBar::from(self),
|
||||||
self.entered()
|
Stack::Down(
|
||||||
).fixed_y(4),
|
TransportView::from(self),
|
||||||
PhraseListView::from(self),
|
Min::Y(20, Split::Right(20,
|
||||||
),
|
Stack::Down(
|
||||||
PhraseView::from(self),
|
play.fixed_y(4),
|
||||||
).min_y(20)
|
Stack::Down(
|
||||||
)))
|
next.fixed_y(4),
|
||||||
|
PhraseListView::from(self)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
PhraseView::from(self),
|
||||||
|
))
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,18 +39,99 @@ impl Content for SequencerStatusBar {
|
||||||
let orange = Color::Rgb(255,128,0);
|
let orange = Color::Rgb(255,128,0);
|
||||||
let yellow = Color::Rgb(255,255,0);
|
let yellow = Color::Rgb(255,255,0);
|
||||||
let black = Color::Rgb(0,0,0);
|
let black = Color::Rgb(0,0,0);
|
||||||
lay!(
|
let modeline =
|
||||||
row!(
|
//Stack::Right(
|
||||||
widget(&self.mode).bg(orange).fg(black).bold(true),
|
widget(&self.mode).bg(orange).fg(black).bold(true)
|
||||||
row!((prefix, hotkey, suffix) in self.help => {
|
//row!((prefix, hotkey, suffix) in self.help => {
|
||||||
row!(" ", *prefix, TuiStyle::fg(*hotkey, yellow), *suffix)
|
//row!(" ", *prefix, TuiStyle::fg(*hotkey, yellow), *suffix)
|
||||||
})
|
//})
|
||||||
),
|
//)
|
||||||
row!(
|
.bg(Color::Rgb(100,100,100));
|
||||||
|
let statusbar =
|
||||||
|
Stack::Right(
|
||||||
widget(&self.cpu).fg(orange),
|
widget(&self.cpu).fg(orange),
|
||||||
widget(&self.res).fg(orange),
|
Fill::X(Align::SE(Both(
|
||||||
widget(&self.size).fg(orange),
|
Background(Color::Rgb(50,50,50)),
|
||||||
).align_se().fill_x()
|
Stack::Right(
|
||||||
)
|
widget(&self.res).fg(orange),
|
||||||
|
widget(&self.size).fg(orange)
|
||||||
|
)))));
|
||||||
|
if self.width > 60 {
|
||||||
|
return Stack::Right(modeline, statusbar);
|
||||||
|
}
|
||||||
|
if self.width > 0 {
|
||||||
|
return Stack::Down(modeline, statusbar);
|
||||||
|
}
|
||||||
|
return Stack::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//struct Either<A: Render<Engine = Tui>, B: Render<Engine = Tui>>(bool, A, B);
|
||||||
|
|
||||||
|
//impl<A: Render<Engine = Tui>, B: Render<Engine = Tui>> Content for Either<A, B> {
|
||||||
|
//type Engine = Tui;
|
||||||
|
//fn content (&self) -> impl Render<Engine = Tui> {
|
||||||
|
//if self.0 { self.2.content() } else { self.1.content() }
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
struct Both<A: Render<Engine = Tui>, B: Render<Engine = Tui>>(A, B);
|
||||||
|
|
||||||
|
impl<A: Render<Engine = Tui>, B: Render<Engine = Tui>> Render for Both<A, B> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||||
|
self.0.render(to)?;
|
||||||
|
self.1.render(to)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Layers<'a, const N: usize>(Collect<'a, Tui, N>);
|
||||||
|
|
||||||
|
impl<'a, const N: usize> Render for Layers<'a, N> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||||
|
for item in self.0.iter() {
|
||||||
|
item.render(to)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//impl<'a> Content for Layers<'a> {
|
||||||
|
//type Engine = Tui;
|
||||||
|
//fn content (&self) -> impl Render<Engine = Tui> {
|
||||||
|
//self
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
enum Stack<A: Render<Engine = Tui>, B: Render<Engine = Tui>> {
|
||||||
|
None,
|
||||||
|
Up(A, B),
|
||||||
|
Down(A, B),
|
||||||
|
Left(A, B),
|
||||||
|
Right(A, B),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: Render<Engine = Tui>, B: Render<Engine = Tui>> Content for Stack<A, B> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Render<Engine = Tui> {
|
||||||
|
todo!();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Split<A: Render<Engine = Tui>, B: Render<Engine = Tui>> {
|
||||||
|
Up(usize, A, B),
|
||||||
|
Down(usize, A, B),
|
||||||
|
Left(usize, A, B),
|
||||||
|
Right(usize, A, B),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: Render<Engine = Tui>, B: Render<Engine = Tui>> Content for Split< A, B> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Render<Engine = Tui> {
|
||||||
|
todo!();
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,32 +25,70 @@ impl Content for TransportView {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Render<Engine = Tui> {
|
fn content (&self) -> impl Render<Engine = Tui> {
|
||||||
let Self { state, selected, focused, bpm, sync, quant, beat, msu, } = self;
|
let Self { state, selected, focused, bpm, sync, quant, beat, msu, } = self;
|
||||||
row!(
|
Stack::down(move|add|{
|
||||||
selected.wrap(TransportFocus::PlayPause, &Styled(
|
add(&row!("│World ", row!(
|
||||||
None,
|
format!("│0 (0)"), //sample(chunk)
|
||||||
match *state {
|
format!("│00m00s000u"), //msu
|
||||||
Some(TransportState::Rolling) => "▶ PLAYING",
|
format!("│00B 0b 00/00"), //bbt
|
||||||
Some(TransportState::Starting) => "READY ...",
|
)))?;
|
||||||
Some(TransportState::Stopped) => "⏹ STOPPED",
|
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(&"")?;
|
||||||
}
|
}
|
||||||
).min_xy(11, 2).push_x(1)),
|
}
|
||||||
selected.wrap(TransportFocus::Bpm, &Outset::X(1u16, {
|
Ok(())
|
||||||
row! {
|
}).fill_x().bg(Color::Rgb(40, 50, 30))
|
||||||
"BPM ",
|
//row!(
|
||||||
format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0)
|
////selected.wrap(TransportFocus::PlayPause, &play_pause.fixed_xy(10, 3)),
|
||||||
}
|
//row!(
|
||||||
})),
|
//col!(
|
||||||
selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! {
|
//Field("SR ", format!("192000")),
|
||||||
"SYNC ", pulses_to_name(*sync as usize)
|
//Field("BUF ", format!("1024")),
|
||||||
})),
|
//Field("LEN ", format!("21300")),
|
||||||
selected.wrap(TransportFocus::Quant, &Outset::X(1u16, row! {
|
//Field("CPU ", format!("00.0%"))
|
||||||
"QUANT ", pulses_to_name(*quant as usize)
|
//),
|
||||||
})),
|
//col!(
|
||||||
selected.wrap(TransportFocus::Clock, &{
|
//Field("PUL ", format!("000000000")),
|
||||||
row!("B" , beat.as_str(), " T", msu.as_str()).outset_x(1)
|
//Field("PPQ ", format!("96")),
|
||||||
}).align_e().fill_x(),
|
//Field("BBT ", format!("00B0b00p"))
|
||||||
).fill_x().bg(Color::Rgb(40, 50, 30))
|
//),
|
||||||
|
//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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,3 +131,16 @@ impl From<&ArrangerTui> for Option<TransportFocus> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Field(&'static str, String);
|
||||||
|
|
||||||
|
impl Content for Field {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Render<Engine = Tui> {
|
||||||
|
row!(
|
||||||
|
"│",
|
||||||
|
TuiStyle::bold(self.0, true),
|
||||||
|
TuiStyle::bg(self.1.as_str(), Color::Rgb(0, 0, 0)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue