mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
separate tui model and view layers
This commit is contained in:
parent
1060afa4f3
commit
416acd9f7b
19 changed files with 1124 additions and 1095 deletions
|
|
@ -14,7 +14,6 @@ use std::fmt::Debug;
|
|||
submod! {
|
||||
tui_apps
|
||||
tui_command
|
||||
tui_content
|
||||
tui_control
|
||||
tui_debug
|
||||
tui_focus
|
||||
|
|
@ -24,10 +23,23 @@ submod! {
|
|||
tui_impls
|
||||
tui_jack
|
||||
tui_menu
|
||||
tui_model
|
||||
tui_select
|
||||
tui_status
|
||||
tui_theme
|
||||
tui_view
|
||||
tui_widget
|
||||
|
||||
tui_model_arranger
|
||||
tui_model_clock
|
||||
tui_model_file_browser
|
||||
tui_model_phrase_editor
|
||||
tui_model_phrase_length
|
||||
tui_model_phrase_list
|
||||
tui_model_phrase_player
|
||||
|
||||
tui_view_arranger
|
||||
tui_view_file_browser
|
||||
tui_view_phrase_editor
|
||||
tui_view_phrase_length
|
||||
tui_view_phrase_list
|
||||
tui_view_sequencer
|
||||
tui_view_transport
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,370 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
impl Content for TransportView {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let Self { state, selected, focused, bpm, sync, quant, beat, msu, } = self;
|
||||
row!(
|
||||
selected.wrap(TransportFocus::PlayPause, &Styled(
|
||||
None,
|
||||
match *state {
|
||||
Some(TransportState::Rolling) => "▶ PLAYING",
|
||||
Some(TransportState::Starting) => "READY ...",
|
||||
Some(TransportState::Stopped) => "⏹ STOPPED",
|
||||
_ => "???",
|
||||
}
|
||||
).min_xy(11, 2).push_x(1)),
|
||||
selected.wrap(TransportFocus::Bpm, &Outset::X(1u16, {
|
||||
row! {
|
||||
"BPM ",
|
||||
format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0)
|
||||
}
|
||||
})),
|
||||
selected.wrap(TransportFocus::Quant, &Outset::X(1u16, row! {
|
||||
"QUANT ", pulses_to_name(*quant as usize)
|
||||
})),
|
||||
selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! {
|
||||
"SYNC ", pulses_to_name(*sync 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))
|
||||
}
|
||||
}
|
||||
|
||||
impl Content for SequencerTui {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
lay!(
|
||||
col!(
|
||||
TransportView::from(self),
|
||||
Split::right(20,
|
||||
widget(&PhrasesView(self)),
|
||||
widget(&PhraseView(self)),
|
||||
).min_y(20)
|
||||
),
|
||||
self.perf.percentage()
|
||||
.map(|cpu|format!("{cpu:.03}%"))
|
||||
.fg(Color::Rgb(255,128,0))
|
||||
.align_sw(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Display mode of arranger
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum ArrangerMode {
|
||||
/// Tracks are rows
|
||||
Horizontal,
|
||||
/// Tracks are columns
|
||||
Vertical(usize),
|
||||
}
|
||||
|
||||
/// Arranger display mode can be cycled
|
||||
impl ArrangerMode {
|
||||
/// Cycle arranger display mode
|
||||
pub fn to_next (&mut self) {
|
||||
*self = match self {
|
||||
Self::Horizontal => Self::Vertical(1),
|
||||
Self::Vertical(1) => Self::Vertical(2),
|
||||
Self::Vertical(2) => Self::Vertical(2),
|
||||
Self::Vertical(0) => Self::Horizontal,
|
||||
Self::Vertical(_) => Self::Vertical(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout for standalone arranger app.
|
||||
impl Content for ArrangerTui {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let arranger_focused = self.arranger_focused();
|
||||
Split::down(
|
||||
1,
|
||||
TransportView::from(self),
|
||||
Split::down(
|
||||
self.splits[0],
|
||||
lay!(
|
||||
Layers::new(move |add|{
|
||||
match self.mode {
|
||||
ArrangerMode::Horizontal =>
|
||||
add(&arranger_content_horizontal(self))?,
|
||||
ArrangerMode::Vertical(factor) =>
|
||||
add(&arranger_content_vertical(self, factor))?
|
||||
};
|
||||
add(&self.size)
|
||||
})
|
||||
.grow_y(1)
|
||||
.border(Lozenge(Style::default()
|
||||
.bg(TuiTheme::border_bg())
|
||||
.fg(TuiTheme::border_fg(arranger_focused)))),
|
||||
widget(&self.size),
|
||||
widget(&format!("[{}] Arranger", if self.entered {
|
||||
"■"
|
||||
} else {
|
||||
" "
|
||||
}))
|
||||
.fg(TuiTheme::title_fg(arranger_focused))
|
||||
.push_x(1),
|
||||
),
|
||||
Split::right(
|
||||
self.splits[1],
|
||||
PhrasesView(self),
|
||||
PhraseView(self),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Display phrases always in order of appearance
|
||||
impl<'a, T: PhrasesViewState> Content for PhrasesView<'a, T> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let focused = self.0.phrases_focused();
|
||||
let entered = self.0.phrases_entered();
|
||||
let mode = self.0.phrases_mode();
|
||||
let content = Stack::down(move|add|match mode {
|
||||
Some(PhrasesMode::Import(_, ref browser)) => {
|
||||
add(browser)
|
||||
},
|
||||
Some(PhrasesMode::Export(_, ref browser)) => {
|
||||
add(browser)
|
||||
},
|
||||
_ => {
|
||||
let phrases = self.0.phrases();
|
||||
let selected = self.0.phrase_index();
|
||||
for (i, phrase) in phrases.iter().enumerate() {
|
||||
add(&Layers::new(|add|{
|
||||
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
|
||||
let mut length = PhraseLength::new(length, None);
|
||||
if let Some(PhrasesMode::Length(phrase, new_length, focus)) = mode {
|
||||
if focused && i == *phrase {
|
||||
length.pulses = *new_length;
|
||||
length.focus = Some(*focus);
|
||||
}
|
||||
}
|
||||
let length = length.align_e().fill_x();
|
||||
let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x();
|
||||
let mut row2 = format!(" {name}");
|
||||
if let Some(PhrasesMode::Rename(phrase, _)) = mode {
|
||||
if focused && i == *phrase {
|
||||
row2 = format!("{row2}▄");
|
||||
}
|
||||
};
|
||||
let row2 = TuiStyle::bold(row2, true);
|
||||
add(&col!(row1, row2).fill_x().bg(color.base.rgb))?;
|
||||
if focused && i == selected {
|
||||
add(&CORNERS)?;
|
||||
}
|
||||
Ok(())
|
||||
}))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
let border_color = if focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)};
|
||||
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
|
||||
let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border);
|
||||
let title_color = if focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)};
|
||||
let upper_left = format!("[{}] Phrases", if entered {"■"} else {" "});
|
||||
let upper_right = format!("({})", self.0.phrases().len());
|
||||
lay!(
|
||||
content,
|
||||
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
|
||||
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Content for FileBrowser {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
Stack::down(|add|{
|
||||
let mut i = 0;
|
||||
for (_, name) in self.dirs.iter() {
|
||||
if i >= self.scroll {
|
||||
add(&TuiStyle::bold(name.as_str(), i == self.index))?;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
for (_, name) in self.files.iter() {
|
||||
if i >= self.scroll {
|
||||
add(&TuiStyle::bold(name.as_str(), i == self.index))?;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
add(&format!("{}/{i}", self.index))?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: PhraseViewState> Content for PhraseView<'a, T> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let phrase = self.0.phrase_editing();
|
||||
let size = self.0.size();
|
||||
let focused = self.0.phrase_editor_focused();
|
||||
let entered = self.0.phrase_editor_entered();
|
||||
let keys = self.0.keys();
|
||||
let buffer = self.0.buffer();
|
||||
let note_len = self.0.note_len();
|
||||
let note_axis = self.0.note_axis();
|
||||
let time_axis = self.0.time_axis();
|
||||
let FixedAxis { start: note_start, point: note_point, clamp: note_clamp }
|
||||
= *note_axis.read().unwrap();
|
||||
let ScaledAxis { start: time_start, point: time_point, clamp: time_clamp, scale: time_scale }
|
||||
= *time_axis.read().unwrap();
|
||||
//let color = Color::Rgb(0,255,0);
|
||||
//let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color);
|
||||
let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{
|
||||
Ok(if to.area().h() >= 2 {
|
||||
to.buffer_update(to.area().set_w(5), &|cell, x, y|{
|
||||
let y = y + (note_start / 2) as u16;
|
||||
if x < keys.area.width && y < keys.area.height {
|
||||
*cell = keys.get(x, y).clone()
|
||||
}
|
||||
});
|
||||
})
|
||||
}).fill_y();
|
||||
let notes_bg_null = Color::Rgb(28, 35, 25);
|
||||
let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
|
||||
let area = to.area();
|
||||
let h = area.h() as usize;
|
||||
size.set_wh(area.w(), h);
|
||||
let mut axis = note_axis.write().unwrap();
|
||||
if let Some(point) = axis.point {
|
||||
if point.saturating_sub(axis.start) > (h * 2).saturating_sub(1) {
|
||||
axis.start += 2;
|
||||
}
|
||||
}
|
||||
Ok(if to.area().h() >= 2 {
|
||||
let area = to.area();
|
||||
to.buffer_update(area, &move |cell, x, y|{
|
||||
cell.set_bg(notes_bg_null);
|
||||
let src_x = (x as usize + time_start) * time_scale;
|
||||
let src_y = y as usize + note_start / 2;
|
||||
if src_x < buffer.width && src_y < buffer.height - 1 {
|
||||
buffer.get(src_x, buffer.height - src_y - 2).map(|src|{
|
||||
cell.set_symbol(src.symbol());
|
||||
cell.set_fg(src.fg);
|
||||
cell.set_bg(src.bg);
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
}).fill_x();
|
||||
let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
|
||||
Ok(if focused && entered {
|
||||
let area = to.area();
|
||||
if let (Some(time), Some(note)) = (time_point, note_point) {
|
||||
let x1 = area.x() + (time / time_scale) as u16;
|
||||
let x2 = x1 + (note_len / time_scale) as u16;
|
||||
let y = area.y() + note.saturating_sub(note_start) as u16 / 2;
|
||||
let c = if note % 2 == 0 { "▀" } else { "▄" };
|
||||
for x in x1..x2 {
|
||||
to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0))));
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30));
|
||||
let playhead_active = playhead_inactive.clone().yellow().bold().not_dim();
|
||||
let playhead = CustomWidget::new(
|
||||
|to:[u16;2]|Ok(Some(to.clip_h(1))),
|
||||
move|to: &mut TuiOutput|{
|
||||
if let Some(_) = phrase {
|
||||
let now = self.0.now().get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length;
|
||||
let time_clamp = time_clamp
|
||||
.expect("time_axis of sequencer expected to be clamped");
|
||||
for x in 0..(time_clamp/time_scale).saturating_sub(time_start) {
|
||||
let this_step = time_start + (x + 0) * time_scale;
|
||||
let next_step = time_start + (x + 1) * time_scale;
|
||||
let x = to.area().x() + x as u16;
|
||||
let active = this_step <= now && now < next_step;
|
||||
let character = if active { "|" } else { "·" };
|
||||
let style = if active { playhead_active } else { playhead_inactive };
|
||||
to.blit(&character, x, to.area.y(), Some(style));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
).push_x(6).align_sw();
|
||||
let border_color = if focused{Color::Rgb(100, 110, 40)}else{Color::Rgb(70, 80, 50)};
|
||||
let title_color = if focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)};
|
||||
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
|
||||
let note_area = lay!(notes, cursor).fill_x();
|
||||
let piano_roll = row!(keys, note_area).fill_x();
|
||||
let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border);
|
||||
let content = lay!(content, playhead);
|
||||
let mut upper_left = format!("[{}] Sequencer", if entered {"■"} else {" "});
|
||||
if let Some(phrase) = phrase {
|
||||
upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name);
|
||||
}
|
||||
let mut lower_right = format!("┤{}├", size.format());
|
||||
lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale));
|
||||
//lower_right = format!("Zoom: {} (+{}:{}*{}|{})",
|
||||
//pulses_to_name(time_scale),
|
||||
//time_start, time_point.unwrap_or(0),
|
||||
//time_scale, time_clamp.unwrap_or(0),
|
||||
//);
|
||||
if focused && entered {
|
||||
lower_right = format!("┤Note: {} {}├─{lower_right}",
|
||||
note_axis.read().unwrap().point.unwrap(),
|
||||
pulses_to_name(note_len));
|
||||
//lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}",
|
||||
//pulses_to_name(*note_len),
|
||||
//note_start,
|
||||
//note_point.unwrap_or(0),
|
||||
//note_clamp.unwrap_or(0),
|
||||
//);
|
||||
}
|
||||
let upper_right = if let Some(phrase) = phrase {
|
||||
format!("┤Length: {}├", phrase.read().unwrap().length)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
lay!(
|
||||
content,
|
||||
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
|
||||
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
|
||||
TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Content for PhraseLength {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
Layers::new(move|add|{
|
||||
match self.focus {
|
||||
None => add(&row!(
|
||||
" ", self.bars_string(),
|
||||
".", self.beats_string(),
|
||||
".", self.ticks_string(),
|
||||
" "
|
||||
)),
|
||||
Some(PhraseLengthFocus::Bar) => add(&row!(
|
||||
"[", self.bars_string(),
|
||||
"]", self.beats_string(),
|
||||
".", self.ticks_string(),
|
||||
" "
|
||||
)),
|
||||
Some(PhraseLengthFocus::Beat) => add(&row!(
|
||||
" ", self.bars_string(),
|
||||
"[", self.beats_string(),
|
||||
"]", self.ticks_string(),
|
||||
" "
|
||||
)),
|
||||
Some(PhraseLengthFocus::Tick) => add(&row!(
|
||||
" ", self.bars_string(),
|
||||
".", self.beats_string(),
|
||||
"[", self.ticks_string(),
|
||||
"]"
|
||||
)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -174,68 +174,6 @@ macro_rules! impl_phrase_editor_control {
|
|||
}
|
||||
}
|
||||
}
|
||||
macro_rules! impl_phrases_view_state {
|
||||
($Struct:ident $(:: $field:ident)* [$self1:ident: $focus:expr] [$self2:ident: $enter:expr]) => {
|
||||
impl PhrasesViewState for $Struct {
|
||||
fn phrases_focused (&$self1) -> bool {
|
||||
$focus
|
||||
}
|
||||
fn phrases_entered (&$self2) -> bool {
|
||||
$enter
|
||||
}
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
||||
&self$(.$field)*.phrases
|
||||
}
|
||||
fn phrase_index (&self) -> usize {
|
||||
self$(.$field)*.phrase.load(Ordering::Relaxed)
|
||||
}
|
||||
fn phrases_mode (&self) -> &Option<PhrasesMode> {
|
||||
&self$(.$field)*.mode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
macro_rules! impl_phrase_view_state {
|
||||
($Struct:ident $(:: $field:ident)* [$self1:ident : $focused:expr] [$self2:ident : $entered:expr]) => {
|
||||
impl PhraseViewState for $Struct {
|
||||
fn phrase_editing (&self) -> &Option<Arc<RwLock<Phrase>>> {
|
||||
&self$(.$field)*.phrase
|
||||
}
|
||||
fn phrase_editor_focused (&$self1) -> bool {
|
||||
$focused
|
||||
//self$(.$field)*.focus.is_focused()
|
||||
}
|
||||
fn phrase_editor_entered (&$self2) -> bool {
|
||||
$entered
|
||||
//self$(.$field)*.focus.is_entered()
|
||||
}
|
||||
fn phrase_editor_size (&self) -> &Measure<Tui> {
|
||||
todo!()
|
||||
}
|
||||
fn keys (&self) -> &Buffer {
|
||||
&self$(.$field)*.keys
|
||||
}
|
||||
fn buffer (&self) -> &BigBuffer {
|
||||
&self$(.$field)*.buffer
|
||||
}
|
||||
fn note_len (&self) -> usize {
|
||||
self$(.$field)*.note_len
|
||||
}
|
||||
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
|
||||
&self$(.$field)*.note_axis
|
||||
}
|
||||
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
|
||||
&self$(.$field)*.time_axis
|
||||
}
|
||||
fn now (&self) -> &Arc<Pulse> {
|
||||
&self$(.$field)*.now
|
||||
}
|
||||
fn size (&self) -> &Measure<Tui> {
|
||||
&self$(.$field)*.size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_jack_api!(TransportTui::jack);
|
||||
impl_jack_api!(SequencerTui::jack);
|
||||
|
|
@ -268,28 +206,3 @@ impl_phrase_editor_control!(ArrangerTui
|
|||
[self: todo!()]
|
||||
[self, phrase: todo!()]
|
||||
);
|
||||
|
||||
impl_phrases_view_state!(PhrasesModel
|
||||
[self: false]
|
||||
[self: false]
|
||||
);
|
||||
impl_phrases_view_state!(SequencerTui::phrases
|
||||
[self: self.focused() == AppFocus::Content(SequencerFocus::Phrases)]
|
||||
[self: self.focused() == AppFocus::Content(SequencerFocus::Phrases)]
|
||||
);
|
||||
impl_phrases_view_state!(ArrangerTui::phrases
|
||||
[self: self.focused() == AppFocus::Content(ArrangerFocus::Phrases)]
|
||||
[self: self.focused() == AppFocus::Content(ArrangerFocus::Phrases)]
|
||||
);
|
||||
impl_phrase_view_state!(PhraseEditorModel
|
||||
[self: true]
|
||||
[self: true]
|
||||
);
|
||||
impl_phrase_view_state!(SequencerTui::editor
|
||||
[self: self.focused() == AppFocus::Content(SequencerFocus::PhraseEditor)]
|
||||
[self: self.entered() && self.focused() == AppFocus::Content(SequencerFocus::PhraseEditor)]
|
||||
);
|
||||
impl_phrase_view_state!(ArrangerTui::editor
|
||||
[self: self.focused() == AppFocus::Content(ArrangerFocus::PhraseEditor)]
|
||||
[self: self.entered() && self.focused() == AppFocus::Content(ArrangerFocus::PhraseEditor)])
|
||||
;
|
||||
|
|
|
|||
|
|
@ -1,379 +1 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClockModel {
|
||||
/// JACK transport handle.
|
||||
pub(crate) transport: Arc<Transport>,
|
||||
/// Playback state
|
||||
pub(crate) playing: Arc<RwLock<Option<TransportState>>>,
|
||||
/// Global sample and usec at which playback started
|
||||
pub(crate) started: Arc<RwLock<Option<(usize, usize)>>>,
|
||||
/// Current moment in time
|
||||
pub(crate) current: Arc<Instant>,
|
||||
/// Note quantization factor
|
||||
pub(crate) quant: Arc<Quantize>,
|
||||
/// Launch quantization factor
|
||||
pub(crate) sync: Arc<LaunchSync>,
|
||||
/// TODO: Enable metronome?
|
||||
pub(crate) metronome: bool,
|
||||
}
|
||||
|
||||
impl From<&Arc<Transport>> for ClockModel {
|
||||
fn from (transport: &Arc<Transport>) -> Self {
|
||||
Self {
|
||||
current: Instant::default().into(),
|
||||
playing: RwLock::new(None).into(),
|
||||
quant: Quantize::default().into(),
|
||||
started: RwLock::new(None).into(),
|
||||
sync: LaunchSync::default().into(),
|
||||
transport: transport.clone(),
|
||||
metronome: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains state for playing a phrase
|
||||
pub struct PhrasePlayerModel {
|
||||
/// State of clock and playhead
|
||||
pub(crate) clock: ClockModel,
|
||||
/// Start time and phrase being played
|
||||
pub(crate) play_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
|
||||
/// Start time and next phrase
|
||||
pub(crate) next_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
|
||||
/// Play input through output.
|
||||
pub(crate) monitoring: bool,
|
||||
/// Write input to sequence.
|
||||
pub(crate) recording: bool,
|
||||
/// Overdub input to sequence.
|
||||
pub(crate) overdub: bool,
|
||||
/// Send all notes off
|
||||
pub(crate) reset: bool, // TODO?: after Some(nframes)
|
||||
/// Record from MIDI ports to current sequence.
|
||||
pub midi_ins: Vec<Port<MidiIn>>,
|
||||
/// Play from current sequence to MIDI ports
|
||||
pub midi_outs: Vec<Port<MidiOut>>,
|
||||
/// Notes currently held at input
|
||||
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
|
||||
/// Notes currently held at output
|
||||
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
|
||||
/// MIDI output buffer
|
||||
pub note_buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl From<&ClockModel> for PhrasePlayerModel {
|
||||
fn from (clock: &ClockModel) -> Self {
|
||||
Self {
|
||||
clock: clock.clone(),
|
||||
midi_ins: vec![],
|
||||
midi_outs: vec![],
|
||||
note_buf: vec![0;8],
|
||||
reset: true,
|
||||
recording: false,
|
||||
monitoring: false,
|
||||
overdub: false,
|
||||
play_phrase: None,
|
||||
next_phrase: None,
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains state for viewing and editing a phrase
|
||||
pub struct PhraseEditorModel {
|
||||
/// Phrase being played
|
||||
pub(crate) phrase: Option<Arc<RwLock<Phrase>>>,
|
||||
/// Length of note that will be inserted, in pulses
|
||||
pub(crate) note_len: usize,
|
||||
/// The full piano keys are rendered to this buffer
|
||||
pub(crate) keys: Buffer,
|
||||
/// The full piano roll is rendered to this buffer
|
||||
pub(crate) buffer: BigBuffer,
|
||||
/// Cursor/scroll/zoom in pitch axis
|
||||
pub(crate) note_axis: RwLock<FixedAxis<usize>>,
|
||||
/// Cursor/scroll/zoom in time axis
|
||||
pub(crate) time_axis: RwLock<ScaledAxis<usize>>,
|
||||
/// Display mode
|
||||
pub(crate) mode: bool,
|
||||
/// Notes currently held at input
|
||||
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
|
||||
/// Notes currently held at output
|
||||
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
|
||||
/// Current position of global playhead
|
||||
pub(crate) now: Arc<Pulse>,
|
||||
/// Width and height of notes area at last render
|
||||
pub(crate) size: Measure<Tui>
|
||||
}
|
||||
|
||||
impl Default for PhraseEditorModel {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
phrase: None,
|
||||
note_len: 24,
|
||||
keys: keys_vert(),
|
||||
buffer: Default::default(),
|
||||
mode: false,
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
now: Pulse::default().into(),
|
||||
size: Measure::new(),
|
||||
note_axis: RwLock::new(FixedAxis {
|
||||
start: 12,
|
||||
point: Some(36),
|
||||
clamp: Some(127)
|
||||
}),
|
||||
time_axis: RwLock::new(ScaledAxis {
|
||||
start: 00,
|
||||
point: Some(00),
|
||||
clamp: Some(000),
|
||||
scale: 24
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PhrasesModel {
|
||||
/// Collection of phrases
|
||||
pub(crate) phrases: Vec<Arc<RwLock<Phrase>>>,
|
||||
/// Selected phrase
|
||||
pub(crate) phrase: AtomicUsize,
|
||||
/// Scroll offset
|
||||
pub(crate) scroll: usize,
|
||||
/// Mode switch
|
||||
pub(crate) mode: Option<PhrasesMode>,
|
||||
}
|
||||
|
||||
impl Default for PhrasesModel {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
phrases: vec![RwLock::new(Phrase::default()).into()],
|
||||
phrase: 0.into(),
|
||||
scroll: 0,
|
||||
mode: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Modes for phrase pool
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PhrasesMode {
|
||||
/// Renaming a pattern
|
||||
Rename(usize, String),
|
||||
/// Editing the length of a pattern
|
||||
Length(usize, usize, PhraseLengthFocus),
|
||||
/// Load phrase from disk
|
||||
Import(usize, FileBrowser),
|
||||
/// Save phrase to disk
|
||||
Export(usize, FileBrowser),
|
||||
}
|
||||
|
||||
/// Browses for phrase to import/export
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileBrowser {
|
||||
pub cwd: PathBuf,
|
||||
pub dirs: Vec<(OsString, String)>,
|
||||
pub files: Vec<(OsString, String)>,
|
||||
pub filter: String,
|
||||
pub index: usize,
|
||||
pub scroll: usize,
|
||||
pub size: Measure<Tui>
|
||||
}
|
||||
|
||||
impl FileBrowser {
|
||||
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
|
||||
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
|
||||
let mut dirs = vec![];
|
||||
let mut files = vec![];
|
||||
for entry in std::fs::read_dir(&cwd)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name();
|
||||
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
|
||||
let meta = entry.metadata()?;
|
||||
if meta.is_dir() {
|
||||
dirs.push((name, format!("📁 {decoded}")));
|
||||
} else if meta.is_file() {
|
||||
files.push((name, format!("📄 {decoded}")));
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
cwd,
|
||||
dirs,
|
||||
files,
|
||||
filter: "".to_string(),
|
||||
index: 0,
|
||||
scroll: 0,
|
||||
size: Measure::new(),
|
||||
})
|
||||
}
|
||||
pub fn len (&self) -> usize {
|
||||
self.dirs.len() + self.files.len()
|
||||
}
|
||||
pub fn is_dir (&self) -> bool {
|
||||
self.index < self.dirs.len()
|
||||
}
|
||||
pub fn is_file (&self) -> bool {
|
||||
self.index >= self.dirs.len()
|
||||
}
|
||||
pub fn path (&self) -> PathBuf {
|
||||
self.cwd.join(if self.is_dir() {
|
||||
&self.dirs[self.index].0
|
||||
} else if self.is_file() {
|
||||
&self.files[self.index - self.dirs.len()].0
|
||||
} else {
|
||||
unreachable!()
|
||||
})
|
||||
}
|
||||
pub fn chdir (&self) -> Usually<Self> {
|
||||
Self::new(Some(self.path()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays and edits phrase length.
|
||||
pub struct PhraseLength {
|
||||
/// Pulses per beat (quaver)
|
||||
pub ppq: usize,
|
||||
/// Beats per bar
|
||||
pub bpb: usize,
|
||||
/// Length of phrase in pulses
|
||||
pub pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<PhraseLengthFocus>,
|
||||
}
|
||||
|
||||
impl PhraseLength {
|
||||
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
|
||||
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
||||
}
|
||||
pub fn bars (&self) -> usize {
|
||||
self.pulses / (self.bpb * self.ppq)
|
||||
}
|
||||
pub fn beats (&self) -> usize {
|
||||
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
||||
}
|
||||
pub fn ticks (&self) -> usize {
|
||||
self.pulses % self.ppq
|
||||
}
|
||||
pub fn bars_string (&self) -> String {
|
||||
format!("{}", self.bars())
|
||||
}
|
||||
pub fn beats_string (&self) -> String {
|
||||
format!("{}", self.beats())
|
||||
}
|
||||
pub fn ticks_string (&self) -> String {
|
||||
format!("{:>02}", self.ticks())
|
||||
}
|
||||
}
|
||||
|
||||
impl HasScenes<ArrangerScene> for ArrangerTui {
|
||||
fn scenes (&self) -> &Vec<ArrangerScene> {
|
||||
&self.scenes
|
||||
}
|
||||
fn scenes_mut (&mut self) -> &mut Vec<ArrangerScene> {
|
||||
&mut self.scenes
|
||||
}
|
||||
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
|
||||
-> Usually<&mut ArrangerScene>
|
||||
{
|
||||
let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string());
|
||||
let scene = ArrangerScene {
|
||||
name: Arc::new(name.into()),
|
||||
clips: vec![None;self.tracks().len()],
|
||||
color: color.unwrap_or_else(||ItemColor::random()),
|
||||
};
|
||||
self.scenes_mut().push(scene);
|
||||
let index = self.scenes().len() - 1;
|
||||
Ok(&mut self.scenes_mut()[index])
|
||||
}
|
||||
fn selected_scene (&self) -> Option<&ArrangerScene> {
|
||||
self.selected.scene().map(|s|self.scenes().get(s)).flatten()
|
||||
}
|
||||
fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
|
||||
self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct ArrangerScene {
|
||||
/// Name of scene
|
||||
pub(crate) name: Arc<RwLock<String>>,
|
||||
/// Clips in scene, one per track
|
||||
pub(crate) clips: Vec<Option<Arc<RwLock<Phrase>>>>,
|
||||
/// Identifying color of scene
|
||||
pub(crate) color: ItemColor,
|
||||
}
|
||||
|
||||
impl ArrangerSceneApi for ArrangerScene {
|
||||
fn name (&self) -> &Arc<RwLock<String>> {
|
||||
&self.name
|
||||
}
|
||||
fn clips (&self) -> &Vec<Option<Arc<RwLock<Phrase>>>> {
|
||||
&self.clips
|
||||
}
|
||||
fn color (&self) -> ItemColor {
|
||||
self.color
|
||||
}
|
||||
}
|
||||
|
||||
impl HasTracks<ArrangerTrack> for ArrangerTui {
|
||||
fn tracks (&self) -> &Vec<ArrangerTrack> {
|
||||
&self.tracks
|
||||
}
|
||||
fn tracks_mut (&mut self) -> &mut Vec<ArrangerTrack> {
|
||||
&mut self.tracks
|
||||
}
|
||||
}
|
||||
|
||||
impl ArrangerTracksApi<ArrangerTrack> for ArrangerTui {
|
||||
fn track_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
|
||||
-> Usually<&mut ArrangerTrack>
|
||||
{
|
||||
let name = name.map_or_else(||self.track_default_name(), |x|x.to_string());
|
||||
let track = ArrangerTrack {
|
||||
width: name.len() + 2,
|
||||
name: Arc::new(name.into()),
|
||||
color: color.unwrap_or_else(||ItemColor::random()),
|
||||
player: PhrasePlayerModel::from(&self.clock),
|
||||
};
|
||||
self.tracks_mut().push(track);
|
||||
let index = self.tracks().len() - 1;
|
||||
Ok(&mut self.tracks_mut()[index])
|
||||
}
|
||||
fn track_del (&mut self, index: usize) {
|
||||
self.tracks_mut().remove(index);
|
||||
for scene in self.scenes_mut().iter_mut() {
|
||||
scene.clips.remove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArrangerTrack {
|
||||
/// Name of track
|
||||
pub(crate) name: Arc<RwLock<String>>,
|
||||
/// Preferred width of track column
|
||||
pub(crate) width: usize,
|
||||
/// Identifying color of track
|
||||
pub(crate) color: ItemColor,
|
||||
/// MIDI player state
|
||||
pub(crate) player: PhrasePlayerModel,
|
||||
}
|
||||
|
||||
impl ArrangerTrackApi for ArrangerTrack {
|
||||
/// Name of track
|
||||
fn name (&self) -> &Arc<RwLock<String>> {
|
||||
&self.name
|
||||
}
|
||||
/// Preferred width of track column
|
||||
fn width (&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
/// Preferred width of track column
|
||||
fn width_mut (&mut self) -> &mut usize {
|
||||
&mut self.width
|
||||
}
|
||||
/// Identifying color of track
|
||||
fn color (&self) -> ItemColor {
|
||||
self.color
|
||||
}
|
||||
}
|
||||
|
|
|
|||
114
crates/tek_tui/src/tui_model_arranger.rs
Normal file
114
crates/tek_tui/src/tui_model_arranger.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
use crate::*;
|
||||
|
||||
impl HasScenes<ArrangerScene> for ArrangerTui {
|
||||
fn scenes (&self) -> &Vec<ArrangerScene> {
|
||||
&self.scenes
|
||||
}
|
||||
fn scenes_mut (&mut self) -> &mut Vec<ArrangerScene> {
|
||||
&mut self.scenes
|
||||
}
|
||||
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
|
||||
-> Usually<&mut ArrangerScene>
|
||||
{
|
||||
let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string());
|
||||
let scene = ArrangerScene {
|
||||
name: Arc::new(name.into()),
|
||||
clips: vec![None;self.tracks().len()],
|
||||
color: color.unwrap_or_else(||ItemColor::random()),
|
||||
};
|
||||
self.scenes_mut().push(scene);
|
||||
let index = self.scenes().len() - 1;
|
||||
Ok(&mut self.scenes_mut()[index])
|
||||
}
|
||||
fn selected_scene (&self) -> Option<&ArrangerScene> {
|
||||
self.selected.scene().map(|s|self.scenes().get(s)).flatten()
|
||||
}
|
||||
fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
|
||||
self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct ArrangerScene {
|
||||
/// Name of scene
|
||||
pub(crate) name: Arc<RwLock<String>>,
|
||||
/// Clips in scene, one per track
|
||||
pub(crate) clips: Vec<Option<Arc<RwLock<Phrase>>>>,
|
||||
/// Identifying color of scene
|
||||
pub(crate) color: ItemColor,
|
||||
}
|
||||
|
||||
impl ArrangerSceneApi for ArrangerScene {
|
||||
fn name (&self) -> &Arc<RwLock<String>> {
|
||||
&self.name
|
||||
}
|
||||
fn clips (&self) -> &Vec<Option<Arc<RwLock<Phrase>>>> {
|
||||
&self.clips
|
||||
}
|
||||
fn color (&self) -> ItemColor {
|
||||
self.color
|
||||
}
|
||||
}
|
||||
|
||||
impl HasTracks<ArrangerTrack> for ArrangerTui {
|
||||
fn tracks (&self) -> &Vec<ArrangerTrack> {
|
||||
&self.tracks
|
||||
}
|
||||
fn tracks_mut (&mut self) -> &mut Vec<ArrangerTrack> {
|
||||
&mut self.tracks
|
||||
}
|
||||
}
|
||||
|
||||
impl ArrangerTracksApi<ArrangerTrack> for ArrangerTui {
|
||||
fn track_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
|
||||
-> Usually<&mut ArrangerTrack>
|
||||
{
|
||||
let name = name.map_or_else(||self.track_default_name(), |x|x.to_string());
|
||||
let track = ArrangerTrack {
|
||||
width: name.len() + 2,
|
||||
name: Arc::new(name.into()),
|
||||
color: color.unwrap_or_else(||ItemColor::random()),
|
||||
player: PhrasePlayerModel::from(&self.clock),
|
||||
};
|
||||
self.tracks_mut().push(track);
|
||||
let index = self.tracks().len() - 1;
|
||||
Ok(&mut self.tracks_mut()[index])
|
||||
}
|
||||
fn track_del (&mut self, index: usize) {
|
||||
self.tracks_mut().remove(index);
|
||||
for scene in self.scenes_mut().iter_mut() {
|
||||
scene.clips.remove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArrangerTrack {
|
||||
/// Name of track
|
||||
pub(crate) name: Arc<RwLock<String>>,
|
||||
/// Preferred width of track column
|
||||
pub(crate) width: usize,
|
||||
/// Identifying color of track
|
||||
pub(crate) color: ItemColor,
|
||||
/// MIDI player state
|
||||
pub(crate) player: PhrasePlayerModel,
|
||||
}
|
||||
|
||||
impl ArrangerTrackApi for ArrangerTrack {
|
||||
/// Name of track
|
||||
fn name (&self) -> &Arc<RwLock<String>> {
|
||||
&self.name
|
||||
}
|
||||
/// Preferred width of track column
|
||||
fn width (&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
/// Preferred width of track column
|
||||
fn width_mut (&mut self) -> &mut usize {
|
||||
&mut self.width
|
||||
}
|
||||
/// Identifying color of track
|
||||
fn color (&self) -> ItemColor {
|
||||
self.color
|
||||
}
|
||||
}
|
||||
33
crates/tek_tui/src/tui_model_clock.rs
Normal file
33
crates/tek_tui/src/tui_model_clock.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClockModel {
|
||||
/// JACK transport handle.
|
||||
pub(crate) transport: Arc<Transport>,
|
||||
/// Playback state
|
||||
pub(crate) playing: Arc<RwLock<Option<TransportState>>>,
|
||||
/// Global sample and usec at which playback started
|
||||
pub(crate) started: Arc<RwLock<Option<(usize, usize)>>>,
|
||||
/// Current moment in time
|
||||
pub(crate) current: Arc<Instant>,
|
||||
/// Note quantization factor
|
||||
pub(crate) quant: Arc<Quantize>,
|
||||
/// Launch quantization factor
|
||||
pub(crate) sync: Arc<LaunchSync>,
|
||||
/// TODO: Enable metronome?
|
||||
pub(crate) metronome: bool,
|
||||
}
|
||||
|
||||
impl From<&Arc<Transport>> for ClockModel {
|
||||
fn from (transport: &Arc<Transport>) -> Self {
|
||||
Self {
|
||||
current: Instant::default().into(),
|
||||
playing: RwLock::new(None).into(),
|
||||
quant: Quantize::default().into(),
|
||||
started: RwLock::new(None).into(),
|
||||
sync: LaunchSync::default().into(),
|
||||
transport: transport.clone(),
|
||||
metronome: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
62
crates/tek_tui/src/tui_model_file_browser.rs
Normal file
62
crates/tek_tui/src/tui_model_file_browser.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
use crate::*;
|
||||
|
||||
/// Browses for phrase to import/export
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileBrowser {
|
||||
pub cwd: PathBuf,
|
||||
pub dirs: Vec<(OsString, String)>,
|
||||
pub files: Vec<(OsString, String)>,
|
||||
pub filter: String,
|
||||
pub index: usize,
|
||||
pub scroll: usize,
|
||||
pub size: Measure<Tui>
|
||||
}
|
||||
|
||||
impl FileBrowser {
|
||||
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
|
||||
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
|
||||
let mut dirs = vec![];
|
||||
let mut files = vec![];
|
||||
for entry in std::fs::read_dir(&cwd)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name();
|
||||
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
|
||||
let meta = entry.metadata()?;
|
||||
if meta.is_dir() {
|
||||
dirs.push((name, format!("📁 {decoded}")));
|
||||
} else if meta.is_file() {
|
||||
files.push((name, format!("📄 {decoded}")));
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
cwd,
|
||||
dirs,
|
||||
files,
|
||||
filter: "".to_string(),
|
||||
index: 0,
|
||||
scroll: 0,
|
||||
size: Measure::new(),
|
||||
})
|
||||
}
|
||||
pub fn len (&self) -> usize {
|
||||
self.dirs.len() + self.files.len()
|
||||
}
|
||||
pub fn is_dir (&self) -> bool {
|
||||
self.index < self.dirs.len()
|
||||
}
|
||||
pub fn is_file (&self) -> bool {
|
||||
self.index >= self.dirs.len()
|
||||
}
|
||||
pub fn path (&self) -> PathBuf {
|
||||
self.cwd.join(if self.is_dir() {
|
||||
&self.dirs[self.index].0
|
||||
} else if self.is_file() {
|
||||
&self.files[self.index - self.dirs.len()].0
|
||||
} else {
|
||||
unreachable!()
|
||||
})
|
||||
}
|
||||
pub fn chdir (&self) -> Usually<Self> {
|
||||
Self::new(Some(self.path()))
|
||||
}
|
||||
}
|
||||
54
crates/tek_tui/src/tui_model_phrase_editor.rs
Normal file
54
crates/tek_tui/src/tui_model_phrase_editor.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use crate::*;
|
||||
|
||||
/// Contains state for viewing and editing a phrase
|
||||
pub struct PhraseEditorModel {
|
||||
/// Phrase being played
|
||||
pub(crate) phrase: Option<Arc<RwLock<Phrase>>>,
|
||||
/// Length of note that will be inserted, in pulses
|
||||
pub(crate) note_len: usize,
|
||||
/// The full piano keys are rendered to this buffer
|
||||
pub(crate) keys: Buffer,
|
||||
/// The full piano roll is rendered to this buffer
|
||||
pub(crate) buffer: BigBuffer,
|
||||
/// Cursor/scroll/zoom in pitch axis
|
||||
pub(crate) note_axis: RwLock<FixedAxis<usize>>,
|
||||
/// Cursor/scroll/zoom in time axis
|
||||
pub(crate) time_axis: RwLock<ScaledAxis<usize>>,
|
||||
/// Display mode
|
||||
pub(crate) mode: bool,
|
||||
/// Notes currently held at input
|
||||
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
|
||||
/// Notes currently held at output
|
||||
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
|
||||
/// Current position of global playhead
|
||||
pub(crate) now: Arc<Pulse>,
|
||||
/// Width and height of notes area at last render
|
||||
pub(crate) size: Measure<Tui>
|
||||
}
|
||||
|
||||
impl Default for PhraseEditorModel {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
phrase: None,
|
||||
note_len: 24,
|
||||
keys: keys_vert(),
|
||||
buffer: Default::default(),
|
||||
mode: false,
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
now: Pulse::default().into(),
|
||||
size: Measure::new(),
|
||||
note_axis: RwLock::new(FixedAxis {
|
||||
start: 12,
|
||||
point: Some(36),
|
||||
clamp: Some(127)
|
||||
}),
|
||||
time_axis: RwLock::new(ScaledAxis {
|
||||
start: 00,
|
||||
point: Some(00),
|
||||
clamp: Some(000),
|
||||
scale: 24
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
37
crates/tek_tui/src/tui_model_phrase_length.rs
Normal file
37
crates/tek_tui/src/tui_model_phrase_length.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use crate::*;
|
||||
|
||||
/// Displays and edits phrase length.
|
||||
pub struct PhraseLength {
|
||||
/// Pulses per beat (quaver)
|
||||
pub ppq: usize,
|
||||
/// Beats per bar
|
||||
pub bpb: usize,
|
||||
/// Length of phrase in pulses
|
||||
pub pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<PhraseLengthFocus>,
|
||||
}
|
||||
|
||||
impl PhraseLength {
|
||||
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
|
||||
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
||||
}
|
||||
pub fn bars (&self) -> usize {
|
||||
self.pulses / (self.bpb * self.ppq)
|
||||
}
|
||||
pub fn beats (&self) -> usize {
|
||||
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
||||
}
|
||||
pub fn ticks (&self) -> usize {
|
||||
self.pulses % self.ppq
|
||||
}
|
||||
pub fn bars_string (&self) -> String {
|
||||
format!("{}", self.bars())
|
||||
}
|
||||
pub fn beats_string (&self) -> String {
|
||||
format!("{}", self.beats())
|
||||
}
|
||||
pub fn ticks_string (&self) -> String {
|
||||
format!("{:>02}", self.ticks())
|
||||
}
|
||||
}
|
||||
37
crates/tek_tui/src/tui_model_phrase_list.rs
Normal file
37
crates/tek_tui/src/tui_model_phrase_list.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PhrasesModel {
|
||||
/// Collection of phrases
|
||||
pub(crate) phrases: Vec<Arc<RwLock<Phrase>>>,
|
||||
/// Selected phrase
|
||||
pub(crate) phrase: AtomicUsize,
|
||||
/// Scroll offset
|
||||
pub(crate) scroll: usize,
|
||||
/// Mode switch
|
||||
pub(crate) mode: Option<PhrasesMode>,
|
||||
}
|
||||
|
||||
impl Default for PhrasesModel {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
phrases: vec![RwLock::new(Phrase::default()).into()],
|
||||
phrase: 0.into(),
|
||||
scroll: 0,
|
||||
mode: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Modes for phrase pool
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PhrasesMode {
|
||||
/// Renaming a pattern
|
||||
Rename(usize, String),
|
||||
/// Editing the length of a pattern
|
||||
Length(usize, usize, PhraseLengthFocus),
|
||||
/// Load phrase from disk
|
||||
Import(usize, FileBrowser),
|
||||
/// Save phrase to disk
|
||||
Export(usize, FileBrowser),
|
||||
}
|
||||
48
crates/tek_tui/src/tui_model_phrase_player.rs
Normal file
48
crates/tek_tui/src/tui_model_phrase_player.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use crate::*;
|
||||
|
||||
/// Contains state for playing a phrase
|
||||
pub struct PhrasePlayerModel {
|
||||
/// State of clock and playhead
|
||||
pub(crate) clock: ClockModel,
|
||||
/// Start time and phrase being played
|
||||
pub(crate) play_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
|
||||
/// Start time and next phrase
|
||||
pub(crate) next_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
|
||||
/// Play input through output.
|
||||
pub(crate) monitoring: bool,
|
||||
/// Write input to sequence.
|
||||
pub(crate) recording: bool,
|
||||
/// Overdub input to sequence.
|
||||
pub(crate) overdub: bool,
|
||||
/// Send all notes off
|
||||
pub(crate) reset: bool, // TODO?: after Some(nframes)
|
||||
/// Record from MIDI ports to current sequence.
|
||||
pub midi_ins: Vec<Port<MidiIn>>,
|
||||
/// Play from current sequence to MIDI ports
|
||||
pub midi_outs: Vec<Port<MidiOut>>,
|
||||
/// Notes currently held at input
|
||||
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
|
||||
/// Notes currently held at output
|
||||
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
|
||||
/// MIDI output buffer
|
||||
pub note_buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl From<&ClockModel> for PhrasePlayerModel {
|
||||
fn from (clock: &ClockModel) -> Self {
|
||||
Self {
|
||||
clock: clock.clone(),
|
||||
midi_ins: vec![],
|
||||
midi_outs: vec![],
|
||||
note_buf: vec![0;8],
|
||||
reset: true,
|
||||
recording: false,
|
||||
monitoring: false,
|
||||
overdub: false,
|
||||
play_phrase: None,
|
||||
next_phrase: None,
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +1,70 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct TransportView {
|
||||
pub(crate) state: Option<TransportState>,
|
||||
pub(crate) selected: Option<TransportFocus>,
|
||||
pub(crate) focused: bool,
|
||||
pub(crate) bpm: f64,
|
||||
pub(crate) sync: f64,
|
||||
pub(crate) quant: f64,
|
||||
pub(crate) beat: String,
|
||||
pub(crate) msu: String,
|
||||
}
|
||||
impl<'a, T> From<&'a T> for TransportView
|
||||
where
|
||||
T: ClockApi,
|
||||
Option<TransportFocus>: From<&'a T>
|
||||
{
|
||||
fn from (state: &'a T) -> Self {
|
||||
let selected = state.into();
|
||||
Self {
|
||||
selected,
|
||||
focused: selected.is_some(),
|
||||
state: state.transport_state().read().unwrap().clone(),
|
||||
bpm: state.bpm().get(),
|
||||
sync: state.sync().get(),
|
||||
quant: state.quant().get(),
|
||||
beat: state.current().format_beat(),
|
||||
msu: state.current().usec.format_msu(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<&TransportTui> for Option<TransportFocus> {
|
||||
fn from (state: &TransportTui) -> Self {
|
||||
match state.focus.inner() {
|
||||
AppFocus::Content(focus) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<&SequencerTui> for Option<TransportFocus> {
|
||||
fn from (state: &SequencerTui) -> Self {
|
||||
match state.focus.inner() {
|
||||
AppFocus::Content(SequencerFocus::Transport(focus)) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<&ArrangerTui> for Option<TransportFocus> {
|
||||
fn from (state: &ArrangerTui) -> Self {
|
||||
match state.focus.inner() {
|
||||
AppFocus::Content(ArrangerFocus::Transport(focus)) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
/// Layout for standalone arranger app.
|
||||
impl Content for ArrangerTui {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let arranger_focused = self.arranger_focused();
|
||||
Split::down(
|
||||
1,
|
||||
TransportView::from(self),
|
||||
Split::down(
|
||||
self.splits[0],
|
||||
lay!(
|
||||
Layers::new(move |add|{
|
||||
match self.mode {
|
||||
ArrangerMode::Horizontal =>
|
||||
add(&arranger_content_horizontal(self))?,
|
||||
ArrangerMode::Vertical(factor) =>
|
||||
add(&arranger_content_vertical(self, factor))?
|
||||
};
|
||||
add(&self.size)
|
||||
})
|
||||
.grow_y(1)
|
||||
.border(Lozenge(Style::default()
|
||||
.bg(TuiTheme::border_bg())
|
||||
.fg(TuiTheme::border_fg(arranger_focused)))),
|
||||
widget(&self.size),
|
||||
widget(&format!("[{}] Arranger", if self.entered {
|
||||
"■"
|
||||
} else {
|
||||
" "
|
||||
}))
|
||||
.fg(TuiTheme::title_fg(arranger_focused))
|
||||
.push_x(1),
|
||||
),
|
||||
Split::right(
|
||||
self.splits[1],
|
||||
PhrasesView(self),
|
||||
PhraseView(self),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PhrasesView<'a, T: PhrasesViewState>(pub &'a T);
|
||||
/// Display mode of arranger
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum ArrangerMode {
|
||||
/// Tracks are rows
|
||||
Horizontal,
|
||||
/// Tracks are columns
|
||||
Vertical(usize),
|
||||
}
|
||||
|
||||
pub struct PhraseView<'a, T: PhraseViewState>(pub &'a T);
|
||||
/// Arranger display mode can be cycled
|
||||
impl ArrangerMode {
|
||||
/// Cycle arranger display mode
|
||||
pub fn to_next (&mut self) {
|
||||
*self = match self {
|
||||
Self::Horizontal => Self::Vertical(1),
|
||||
Self::Vertical(1) => Self::Vertical(2),
|
||||
Self::Vertical(2) => Self::Vertical(2),
|
||||
Self::Vertical(0) => Self::Horizontal,
|
||||
Self::Vertical(_) => Self::Vertical(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ArrangerViewState {
|
||||
fn arranger_focused (&self) -> bool;
|
||||
|
|
@ -67,28 +75,6 @@ impl ArrangerViewState for ArrangerTui {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait PhrasesViewState: Send + Sync {
|
||||
fn phrases_focused (&self) -> bool;
|
||||
fn phrases_entered (&self) -> bool;
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>>;
|
||||
fn phrase_index (&self) -> usize;
|
||||
fn phrases_mode (&self) -> &Option<PhrasesMode>;
|
||||
}
|
||||
|
||||
pub trait PhraseViewState: Send + Sync {
|
||||
fn phrase_editing (&self) -> &Option<Arc<RwLock<Phrase>>>;
|
||||
fn phrase_editor_focused (&self) -> bool;
|
||||
fn phrase_editor_size (&self) -> &Measure<Tui>;
|
||||
fn phrase_editor_entered (&self) -> bool;
|
||||
fn keys (&self) -> &Buffer;
|
||||
fn buffer (&self) -> &BigBuffer;
|
||||
fn note_len (&self) -> usize;
|
||||
fn note_axis (&self) -> &RwLock<FixedAxis<usize>>;
|
||||
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>>;
|
||||
fn now (&self) -> &Arc<Pulse>;
|
||||
fn size (&self) -> &Measure<Tui>;
|
||||
}
|
||||
|
||||
fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> {
|
||||
let mut widths = vec![];
|
||||
let mut total = 0;
|
||||
|
|
@ -480,153 +466,3 @@ pub fn arranger_content_horizontal (
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// Colors of piano keys
|
||||
const KEY_COLORS: [(Color, Color);6] = [
|
||||
(Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)),
|
||||
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
||||
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
||||
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
||||
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
||||
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
||||
];
|
||||
|
||||
pub(crate) fn keys_vert () -> Buffer {
|
||||
let area = [0, 0, 5, 64];
|
||||
let mut buffer = Buffer::empty(Rect {
|
||||
x: area.x(), y: area.y(), width: area.w(), height: area.h()
|
||||
});
|
||||
buffer_update(&mut buffer, area, &|cell, x, y| {
|
||||
let y = 63 - y;
|
||||
match x {
|
||||
0 => {
|
||||
cell.set_char('▀');
|
||||
let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize];
|
||||
cell.set_fg(fg);
|
||||
cell.set_bg(bg);
|
||||
},
|
||||
1 => {
|
||||
cell.set_char('▀');
|
||||
cell.set_fg(Color::White);
|
||||
cell.set_bg(Color::White);
|
||||
},
|
||||
2 => if y % 6 == 0 {
|
||||
cell.set_char('C');
|
||||
},
|
||||
3 => if y % 6 == 0 {
|
||||
cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]);
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
buffer
|
||||
}
|
||||
|
||||
const NTH_OCTAVE: [&'static str; 11] = [
|
||||
"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8",
|
||||
];
|
||||
|
||||
impl PhraseEditorModel {
|
||||
pub fn put (&mut self) {
|
||||
if let (Some(phrase), Some(time), Some(note)) = (
|
||||
&self.phrase,
|
||||
self.time_axis.read().unwrap().point,
|
||||
self.note_axis.read().unwrap().point,
|
||||
) {
|
||||
let mut phrase = phrase.write().unwrap();
|
||||
let key: u7 = u7::from((127 - note) as u8);
|
||||
let vel: u7 = 100.into();
|
||||
let start = time;
|
||||
let end = (start + self.note_len) % phrase.length;
|
||||
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
|
||||
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
|
||||
self.buffer = Self::redraw(&phrase);
|
||||
}
|
||||
}
|
||||
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
|
||||
pub fn show (&mut self, phrase: Option<Arc<RwLock<Phrase>>>) {
|
||||
if let Some(phrase) = phrase {
|
||||
self.phrase = Some(phrase.clone());
|
||||
self.time_axis.write().unwrap().clamp = Some(phrase.read().unwrap().length);
|
||||
self.buffer = Self::redraw(&*phrase.read().unwrap());
|
||||
} else {
|
||||
self.phrase = None;
|
||||
self.time_axis.write().unwrap().clamp = Some(0);
|
||||
self.buffer = Default::default();
|
||||
}
|
||||
}
|
||||
fn redraw (phrase: &Phrase) -> BigBuffer {
|
||||
let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65);
|
||||
Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq);
|
||||
Self::fill_seq_fg(&mut buf, &phrase);
|
||||
buf
|
||||
}
|
||||
fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) {
|
||||
for x in 0..buf.width {
|
||||
// Only fill as far as phrase length
|
||||
if x as usize >= length { break }
|
||||
// Fill each row with background characters
|
||||
for y in 0 .. buf.height {
|
||||
buf.get_mut(x, y).map(|cell|{
|
||||
cell.set_char(if ppq == 0 {
|
||||
'·'
|
||||
} else if x % (4 * ppq) == 0 {
|
||||
'│'
|
||||
} else if x % ppq == 0 {
|
||||
'╎'
|
||||
} else {
|
||||
'·'
|
||||
});
|
||||
cell.set_fg(Color::Rgb(48, 64, 56));
|
||||
cell.modifier = Modifier::DIM;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) {
|
||||
let mut notes_on = [false;128];
|
||||
for x in 0..buf.width {
|
||||
if x as usize >= phrase.length {
|
||||
break
|
||||
}
|
||||
if let Some(notes) = phrase.notes.get(x as usize) {
|
||||
if phrase.percussive {
|
||||
for note in notes {
|
||||
match note {
|
||||
MidiMessage::NoteOn { key, .. } =>
|
||||
notes_on[key.as_int() as usize] = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for note in notes {
|
||||
match note {
|
||||
MidiMessage::NoteOn { key, .. } =>
|
||||
notes_on[key.as_int() as usize] = true,
|
||||
MidiMessage::NoteOff { key, .. } =>
|
||||
notes_on[key.as_int() as usize] = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
for y in 0..buf.height {
|
||||
if y >= 64 {
|
||||
break
|
||||
}
|
||||
if let Some(block) = half_block(
|
||||
notes_on[y as usize * 2],
|
||||
notes_on[y as usize * 2 + 1],
|
||||
) {
|
||||
buf.get_mut(x, y).map(|cell|{
|
||||
cell.set_char(block);
|
||||
cell.set_fg(Color::White);
|
||||
});
|
||||
}
|
||||
}
|
||||
if phrase.percussive {
|
||||
notes_on.fill(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
crates/tek_tui/src/tui_view_file_browser.rs
Normal file
24
crates/tek_tui/src/tui_view_file_browser.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use crate::*;
|
||||
|
||||
impl Content for FileBrowser {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
Stack::down(|add|{
|
||||
let mut i = 0;
|
||||
for (_, name) in self.dirs.iter() {
|
||||
if i >= self.scroll {
|
||||
add(&TuiStyle::bold(name.as_str(), i == self.index))?;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
for (_, name) in self.files.iter() {
|
||||
if i >= self.scroll {
|
||||
add(&TuiStyle::bold(name.as_str(), i == self.index))?;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
add(&format!("{}/{i}", self.index))?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
365
crates/tek_tui/src/tui_view_phrase_editor.rs
Normal file
365
crates/tek_tui/src/tui_view_phrase_editor.rs
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
use crate::*;
|
||||
|
||||
impl Widget for PhraseEditorModel {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
PhraseView(self).layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
PhraseView(self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PhraseView<'a, T: PhraseViewState>(pub &'a T);
|
||||
|
||||
impl<'a, T: PhraseViewState> Content for PhraseView<'a, T> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let phrase = self.0.phrase_editing();
|
||||
let size = self.0.size();
|
||||
let focused = self.0.phrase_editor_focused();
|
||||
let entered = self.0.phrase_editor_entered();
|
||||
let keys = self.0.keys();
|
||||
let buffer = self.0.buffer();
|
||||
let note_len = self.0.note_len();
|
||||
let note_axis = self.0.note_axis();
|
||||
let time_axis = self.0.time_axis();
|
||||
let FixedAxis { start: note_start, point: note_point, clamp: note_clamp }
|
||||
= *note_axis.read().unwrap();
|
||||
let ScaledAxis { start: time_start, point: time_point, clamp: time_clamp, scale: time_scale }
|
||||
= *time_axis.read().unwrap();
|
||||
//let color = Color::Rgb(0,255,0);
|
||||
//let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color);
|
||||
let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{
|
||||
Ok(if to.area().h() >= 2 {
|
||||
to.buffer_update(to.area().set_w(5), &|cell, x, y|{
|
||||
let y = y + (note_start / 2) as u16;
|
||||
if x < keys.area.width && y < keys.area.height {
|
||||
*cell = keys.get(x, y).clone()
|
||||
}
|
||||
});
|
||||
})
|
||||
}).fill_y();
|
||||
let notes_bg_null = Color::Rgb(28, 35, 25);
|
||||
let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
|
||||
let area = to.area();
|
||||
let h = area.h() as usize;
|
||||
size.set_wh(area.w(), h);
|
||||
let mut axis = note_axis.write().unwrap();
|
||||
if let Some(point) = axis.point {
|
||||
if point.saturating_sub(axis.start) > (h * 2).saturating_sub(1) {
|
||||
axis.start += 2;
|
||||
}
|
||||
}
|
||||
Ok(if to.area().h() >= 2 {
|
||||
let area = to.area();
|
||||
to.buffer_update(area, &move |cell, x, y|{
|
||||
cell.set_bg(notes_bg_null);
|
||||
let src_x = (x as usize + time_start) * time_scale;
|
||||
let src_y = y as usize + note_start / 2;
|
||||
if src_x < buffer.width && src_y < buffer.height - 1 {
|
||||
buffer.get(src_x, buffer.height - src_y - 2).map(|src|{
|
||||
cell.set_symbol(src.symbol());
|
||||
cell.set_fg(src.fg);
|
||||
cell.set_bg(src.bg);
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
}).fill_x();
|
||||
let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
|
||||
Ok(if focused && entered {
|
||||
let area = to.area();
|
||||
if let (Some(time), Some(note)) = (time_point, note_point) {
|
||||
let x1 = area.x() + (time / time_scale) as u16;
|
||||
let x2 = x1 + (note_len / time_scale) as u16;
|
||||
let y = area.y() + note.saturating_sub(note_start) as u16 / 2;
|
||||
let c = if note % 2 == 0 { "▀" } else { "▄" };
|
||||
for x in x1..x2 {
|
||||
to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0))));
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30));
|
||||
let playhead_active = playhead_inactive.clone().yellow().bold().not_dim();
|
||||
let playhead = CustomWidget::new(
|
||||
|to:[u16;2]|Ok(Some(to.clip_h(1))),
|
||||
move|to: &mut TuiOutput|{
|
||||
if let Some(_) = phrase {
|
||||
let now = self.0.now().get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length;
|
||||
let time_clamp = time_clamp
|
||||
.expect("time_axis of sequencer expected to be clamped");
|
||||
for x in 0..(time_clamp/time_scale).saturating_sub(time_start) {
|
||||
let this_step = time_start + (x + 0) * time_scale;
|
||||
let next_step = time_start + (x + 1) * time_scale;
|
||||
let x = to.area().x() + x as u16;
|
||||
let active = this_step <= now && now < next_step;
|
||||
let character = if active { "|" } else { "·" };
|
||||
let style = if active { playhead_active } else { playhead_inactive };
|
||||
to.blit(&character, x, to.area.y(), Some(style));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
).push_x(6).align_sw();
|
||||
let border_color = if focused{Color::Rgb(100, 110, 40)}else{Color::Rgb(70, 80, 50)};
|
||||
let title_color = if focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)};
|
||||
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
|
||||
let note_area = lay!(notes, cursor).fill_x();
|
||||
let piano_roll = row!(keys, note_area).fill_x();
|
||||
let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border);
|
||||
let content = lay!(content, playhead);
|
||||
let mut upper_left = format!("[{}] Sequencer", if entered {"■"} else {" "});
|
||||
if let Some(phrase) = phrase {
|
||||
upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name);
|
||||
}
|
||||
let mut lower_right = format!("┤{}├", size.format());
|
||||
lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale));
|
||||
//lower_right = format!("Zoom: {} (+{}:{}*{}|{})",
|
||||
//pulses_to_name(time_scale),
|
||||
//time_start, time_point.unwrap_or(0),
|
||||
//time_scale, time_clamp.unwrap_or(0),
|
||||
//);
|
||||
if focused && entered {
|
||||
lower_right = format!("┤Note: {} {}├─{lower_right}",
|
||||
note_axis.read().unwrap().point.unwrap(),
|
||||
pulses_to_name(note_len));
|
||||
//lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}",
|
||||
//pulses_to_name(*note_len),
|
||||
//note_start,
|
||||
//note_point.unwrap_or(0),
|
||||
//note_clamp.unwrap_or(0),
|
||||
//);
|
||||
}
|
||||
let upper_right = if let Some(phrase) = phrase {
|
||||
format!("┤Length: {}├", phrase.read().unwrap().length)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
lay!(
|
||||
content,
|
||||
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
|
||||
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
|
||||
TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PhraseViewState: Send + Sync {
|
||||
fn phrase_editing (&self) -> &Option<Arc<RwLock<Phrase>>>;
|
||||
fn phrase_editor_focused (&self) -> bool;
|
||||
fn phrase_editor_size (&self) -> &Measure<Tui>;
|
||||
fn phrase_editor_entered (&self) -> bool;
|
||||
fn keys (&self) -> &Buffer;
|
||||
fn buffer (&self) -> &BigBuffer;
|
||||
fn note_len (&self) -> usize;
|
||||
fn note_axis (&self) -> &RwLock<FixedAxis<usize>>;
|
||||
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>>;
|
||||
fn now (&self) -> &Arc<Pulse>;
|
||||
fn size (&self) -> &Measure<Tui>;
|
||||
}
|
||||
|
||||
macro_rules! impl_phrase_view_state {
|
||||
($Struct:ident $(:: $field:ident)* [$self1:ident : $focused:expr] [$self2:ident : $entered:expr]) => {
|
||||
impl PhraseViewState for $Struct {
|
||||
fn phrase_editing (&self) -> &Option<Arc<RwLock<Phrase>>> {
|
||||
&self$(.$field)*.phrase
|
||||
}
|
||||
fn phrase_editor_focused (&$self1) -> bool {
|
||||
$focused
|
||||
//self$(.$field)*.focus.is_focused()
|
||||
}
|
||||
fn phrase_editor_entered (&$self2) -> bool {
|
||||
$entered
|
||||
//self$(.$field)*.focus.is_entered()
|
||||
}
|
||||
fn phrase_editor_size (&self) -> &Measure<Tui> {
|
||||
todo!()
|
||||
}
|
||||
fn keys (&self) -> &Buffer {
|
||||
&self$(.$field)*.keys
|
||||
}
|
||||
fn buffer (&self) -> &BigBuffer {
|
||||
&self$(.$field)*.buffer
|
||||
}
|
||||
fn note_len (&self) -> usize {
|
||||
self$(.$field)*.note_len
|
||||
}
|
||||
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
|
||||
&self$(.$field)*.note_axis
|
||||
}
|
||||
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
|
||||
&self$(.$field)*.time_axis
|
||||
}
|
||||
fn now (&self) -> &Arc<Pulse> {
|
||||
&self$(.$field)*.now
|
||||
}
|
||||
fn size (&self) -> &Measure<Tui> {
|
||||
&self$(.$field)*.size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl_phrase_view_state!(PhraseEditorModel
|
||||
[self: true]
|
||||
[self: true]
|
||||
);
|
||||
impl_phrase_view_state!(SequencerTui::editor
|
||||
[self: self.focused() == AppFocus::Content(SequencerFocus::PhraseEditor)]
|
||||
[self: self.entered() && self.focused() == AppFocus::Content(SequencerFocus::PhraseEditor)]
|
||||
);
|
||||
impl_phrase_view_state!(ArrangerTui::editor
|
||||
[self: self.focused() == AppFocus::Content(ArrangerFocus::PhraseEditor)]
|
||||
[self: self.entered() && self.focused() == AppFocus::Content(ArrangerFocus::PhraseEditor)])
|
||||
;
|
||||
|
||||
/// Colors of piano keys
|
||||
const KEY_COLORS: [(Color, Color);6] = [
|
||||
(Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)),
|
||||
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
||||
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
||||
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
||||
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
||||
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
||||
];
|
||||
|
||||
pub(crate) fn keys_vert () -> Buffer {
|
||||
let area = [0, 0, 5, 64];
|
||||
let mut buffer = Buffer::empty(Rect {
|
||||
x: area.x(), y: area.y(), width: area.w(), height: area.h()
|
||||
});
|
||||
buffer_update(&mut buffer, area, &|cell, x, y| {
|
||||
let y = 63 - y;
|
||||
match x {
|
||||
0 => {
|
||||
cell.set_char('▀');
|
||||
let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize];
|
||||
cell.set_fg(fg);
|
||||
cell.set_bg(bg);
|
||||
},
|
||||
1 => {
|
||||
cell.set_char('▀');
|
||||
cell.set_fg(Color::White);
|
||||
cell.set_bg(Color::White);
|
||||
},
|
||||
2 => if y % 6 == 0 {
|
||||
cell.set_char('C');
|
||||
},
|
||||
3 => if y % 6 == 0 {
|
||||
cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]);
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
buffer
|
||||
}
|
||||
|
||||
const NTH_OCTAVE: [&'static str; 11] = [
|
||||
"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8",
|
||||
];
|
||||
|
||||
impl PhraseEditorModel {
|
||||
pub fn put (&mut self) {
|
||||
if let (Some(phrase), Some(time), Some(note)) = (
|
||||
&self.phrase,
|
||||
self.time_axis.read().unwrap().point,
|
||||
self.note_axis.read().unwrap().point,
|
||||
) {
|
||||
let mut phrase = phrase.write().unwrap();
|
||||
let key: u7 = u7::from((127 - note) as u8);
|
||||
let vel: u7 = 100.into();
|
||||
let start = time;
|
||||
let end = (start + self.note_len) % phrase.length;
|
||||
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
|
||||
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
|
||||
self.buffer = Self::redraw(&phrase);
|
||||
}
|
||||
}
|
||||
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
|
||||
pub fn show (&mut self, phrase: Option<Arc<RwLock<Phrase>>>) {
|
||||
if let Some(phrase) = phrase {
|
||||
self.phrase = Some(phrase.clone());
|
||||
self.time_axis.write().unwrap().clamp = Some(phrase.read().unwrap().length);
|
||||
self.buffer = Self::redraw(&*phrase.read().unwrap());
|
||||
} else {
|
||||
self.phrase = None;
|
||||
self.time_axis.write().unwrap().clamp = Some(0);
|
||||
self.buffer = Default::default();
|
||||
}
|
||||
}
|
||||
fn redraw (phrase: &Phrase) -> BigBuffer {
|
||||
let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65);
|
||||
Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq);
|
||||
Self::fill_seq_fg(&mut buf, &phrase);
|
||||
buf
|
||||
}
|
||||
fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) {
|
||||
for x in 0..buf.width {
|
||||
// Only fill as far as phrase length
|
||||
if x as usize >= length { break }
|
||||
// Fill each row with background characters
|
||||
for y in 0 .. buf.height {
|
||||
buf.get_mut(x, y).map(|cell|{
|
||||
cell.set_char(if ppq == 0 {
|
||||
'·'
|
||||
} else if x % (4 * ppq) == 0 {
|
||||
'│'
|
||||
} else if x % ppq == 0 {
|
||||
'╎'
|
||||
} else {
|
||||
'·'
|
||||
});
|
||||
cell.set_fg(Color::Rgb(48, 64, 56));
|
||||
cell.modifier = Modifier::DIM;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) {
|
||||
let mut notes_on = [false;128];
|
||||
for x in 0..buf.width {
|
||||
if x as usize >= phrase.length {
|
||||
break
|
||||
}
|
||||
if let Some(notes) = phrase.notes.get(x as usize) {
|
||||
if phrase.percussive {
|
||||
for note in notes {
|
||||
match note {
|
||||
MidiMessage::NoteOn { key, .. } =>
|
||||
notes_on[key.as_int() as usize] = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for note in notes {
|
||||
match note {
|
||||
MidiMessage::NoteOn { key, .. } =>
|
||||
notes_on[key.as_int() as usize] = true,
|
||||
MidiMessage::NoteOff { key, .. } =>
|
||||
notes_on[key.as_int() as usize] = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
for y in 0..buf.height {
|
||||
if y >= 64 {
|
||||
break
|
||||
}
|
||||
if let Some(block) = half_block(
|
||||
notes_on[y as usize * 2],
|
||||
notes_on[y as usize * 2 + 1],
|
||||
) {
|
||||
buf.get_mut(x, y).map(|cell|{
|
||||
cell.set_char(block);
|
||||
cell.set_fg(Color::White);
|
||||
});
|
||||
}
|
||||
}
|
||||
if phrase.percussive {
|
||||
notes_on.fill(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
crates/tek_tui/src/tui_view_phrase_length.rs
Normal file
35
crates/tek_tui/src/tui_view_phrase_length.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use crate::*;
|
||||
|
||||
impl Content for PhraseLength {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
Layers::new(move|add|{
|
||||
match self.focus {
|
||||
None => add(&row!(
|
||||
" ", self.bars_string(),
|
||||
".", self.beats_string(),
|
||||
".", self.ticks_string(),
|
||||
" "
|
||||
)),
|
||||
Some(PhraseLengthFocus::Bar) => add(&row!(
|
||||
"[", self.bars_string(),
|
||||
"]", self.beats_string(),
|
||||
".", self.ticks_string(),
|
||||
" "
|
||||
)),
|
||||
Some(PhraseLengthFocus::Beat) => add(&row!(
|
||||
" ", self.bars_string(),
|
||||
"[", self.beats_string(),
|
||||
"]", self.ticks_string(),
|
||||
" "
|
||||
)),
|
||||
Some(PhraseLengthFocus::Tick) => add(&row!(
|
||||
" ", self.bars_string(),
|
||||
".", self.beats_string(),
|
||||
"[", self.ticks_string(),
|
||||
"]"
|
||||
)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
116
crates/tek_tui/src/tui_view_phrase_list.rs
Normal file
116
crates/tek_tui/src/tui_view_phrase_list.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
use crate::*;
|
||||
|
||||
impl Widget for PhrasesModel {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
PhrasesView(self).layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
PhrasesView(self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PhrasesView<'a, T: PhrasesViewState>(pub &'a T);
|
||||
|
||||
// TODO: Display phrases always in order of appearance
|
||||
impl<'a, T: PhrasesViewState> Content for PhrasesView<'a, T> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let focused = self.0.phrases_focused();
|
||||
let entered = self.0.phrases_entered();
|
||||
let mode = self.0.phrases_mode();
|
||||
let content = Stack::down(move|add|match mode {
|
||||
Some(PhrasesMode::Import(_, ref browser)) => {
|
||||
add(browser)
|
||||
},
|
||||
Some(PhrasesMode::Export(_, ref browser)) => {
|
||||
add(browser)
|
||||
},
|
||||
_ => {
|
||||
let phrases = self.0.phrases();
|
||||
let selected = self.0.phrase_index();
|
||||
for (i, phrase) in phrases.iter().enumerate() {
|
||||
add(&Layers::new(|add|{
|
||||
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
|
||||
let mut length = PhraseLength::new(length, None);
|
||||
if let Some(PhrasesMode::Length(phrase, new_length, focus)) = mode {
|
||||
if focused && i == *phrase {
|
||||
length.pulses = *new_length;
|
||||
length.focus = Some(*focus);
|
||||
}
|
||||
}
|
||||
let length = length.align_e().fill_x();
|
||||
let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x();
|
||||
let mut row2 = format!(" {name}");
|
||||
if let Some(PhrasesMode::Rename(phrase, _)) = mode {
|
||||
if focused && i == *phrase {
|
||||
row2 = format!("{row2}▄");
|
||||
}
|
||||
};
|
||||
let row2 = TuiStyle::bold(row2, true);
|
||||
add(&col!(row1, row2).fill_x().bg(color.base.rgb))?;
|
||||
if focused && i == selected {
|
||||
add(&CORNERS)?;
|
||||
}
|
||||
Ok(())
|
||||
}))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
let border_color = if focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)};
|
||||
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
|
||||
let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border);
|
||||
let title_color = if focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)};
|
||||
let upper_left = format!("[{}] Phrases", if entered {"■"} else {" "});
|
||||
let upper_right = format!("({})", self.0.phrases().len());
|
||||
lay!(
|
||||
content,
|
||||
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
|
||||
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PhrasesViewState: Send + Sync {
|
||||
fn phrases_focused (&self) -> bool;
|
||||
fn phrases_entered (&self) -> bool;
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>>;
|
||||
fn phrase_index (&self) -> usize;
|
||||
fn phrases_mode (&self) -> &Option<PhrasesMode>;
|
||||
}
|
||||
|
||||
macro_rules! impl_phrases_view_state {
|
||||
($Struct:ident $(:: $field:ident)* [$self1:ident: $focus:expr] [$self2:ident: $enter:expr]) => {
|
||||
impl PhrasesViewState for $Struct {
|
||||
fn phrases_focused (&$self1) -> bool {
|
||||
$focus
|
||||
}
|
||||
fn phrases_entered (&$self2) -> bool {
|
||||
$enter
|
||||
}
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
||||
&self$(.$field)*.phrases
|
||||
}
|
||||
fn phrase_index (&self) -> usize {
|
||||
self$(.$field)*.phrase.load(Ordering::Relaxed)
|
||||
}
|
||||
fn phrases_mode (&self) -> &Option<PhrasesMode> {
|
||||
&self$(.$field)*.mode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_phrases_view_state!(PhrasesModel
|
||||
[self: false]
|
||||
[self: false]
|
||||
);
|
||||
impl_phrases_view_state!(SequencerTui::phrases
|
||||
[self: self.focused() == AppFocus::Content(SequencerFocus::Phrases)]
|
||||
[self: self.focused() == AppFocus::Content(SequencerFocus::Phrases)]
|
||||
);
|
||||
impl_phrases_view_state!(ArrangerTui::phrases
|
||||
[self: self.focused() == AppFocus::Content(ArrangerFocus::Phrases)]
|
||||
[self: self.focused() == AppFocus::Content(ArrangerFocus::Phrases)]
|
||||
);
|
||||
20
crates/tek_tui/src/tui_view_sequencer.rs
Normal file
20
crates/tek_tui/src/tui_view_sequencer.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use crate::*;
|
||||
|
||||
impl Content for SequencerTui {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
lay!(
|
||||
col!(
|
||||
TransportView::from(self),
|
||||
Split::right(20,
|
||||
widget(&PhrasesView(self)),
|
||||
widget(&PhraseView(self)),
|
||||
).min_y(20)
|
||||
),
|
||||
self.perf.percentage()
|
||||
.map(|cpu|format!("{cpu:.03}%"))
|
||||
.fg(Color::Rgb(255,128,0))
|
||||
.align_sw(),
|
||||
)
|
||||
}
|
||||
}
|
||||
102
crates/tek_tui/src/tui_view_transport.rs
Normal file
102
crates/tek_tui/src/tui_view_transport.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
use crate::*;
|
||||
|
||||
impl Widget for TransportTui {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
TransportView::from(self).layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
TransportView::from(self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TransportView {
|
||||
pub(crate) state: Option<TransportState>,
|
||||
pub(crate) selected: Option<TransportFocus>,
|
||||
pub(crate) focused: bool,
|
||||
pub(crate) bpm: f64,
|
||||
pub(crate) sync: f64,
|
||||
pub(crate) quant: f64,
|
||||
pub(crate) beat: String,
|
||||
pub(crate) msu: String,
|
||||
}
|
||||
|
||||
impl Content for TransportView {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let Self { state, selected, focused, bpm, sync, quant, beat, msu, } = self;
|
||||
row!(
|
||||
selected.wrap(TransportFocus::PlayPause, &Styled(
|
||||
None,
|
||||
match *state {
|
||||
Some(TransportState::Rolling) => "▶ PLAYING",
|
||||
Some(TransportState::Starting) => "READY ...",
|
||||
Some(TransportState::Stopped) => "⏹ STOPPED",
|
||||
_ => "???",
|
||||
}
|
||||
).min_xy(11, 2).push_x(1)),
|
||||
selected.wrap(TransportFocus::Bpm, &Outset::X(1u16, {
|
||||
row! {
|
||||
"BPM ",
|
||||
format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0)
|
||||
}
|
||||
})),
|
||||
selected.wrap(TransportFocus::Quant, &Outset::X(1u16, row! {
|
||||
"QUANT ", pulses_to_name(*quant as usize)
|
||||
})),
|
||||
selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! {
|
||||
"SYNC ", pulses_to_name(*sync 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))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<&'a T> for TransportView
|
||||
where
|
||||
T: ClockApi,
|
||||
Option<TransportFocus>: From<&'a T>
|
||||
{
|
||||
fn from (state: &'a T) -> Self {
|
||||
let selected = state.into();
|
||||
Self {
|
||||
selected,
|
||||
focused: selected.is_some(),
|
||||
state: state.transport_state().read().unwrap().clone(),
|
||||
bpm: state.bpm().get(),
|
||||
sync: state.sync().get(),
|
||||
quant: state.quant().get(),
|
||||
beat: state.current().format_beat(),
|
||||
msu: state.current().usec.format_msu(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TransportTui> for Option<TransportFocus> {
|
||||
fn from (state: &TransportTui) -> Self {
|
||||
match state.focus.inner() {
|
||||
AppFocus::Content(focus) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&SequencerTui> for Option<TransportFocus> {
|
||||
fn from (state: &SequencerTui) -> Self {
|
||||
match state.focus.inner() {
|
||||
AppFocus::Content(SequencerFocus::Transport(focus)) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ArrangerTui> for Option<TransportFocus> {
|
||||
fn from (state: &ArrangerTui) -> Self {
|
||||
match state.focus.inner() {
|
||||
AppFocus::Content(ArrangerFocus::Transport(focus)) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
impl Widget for TransportTui {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
TransportView::from(self).layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
TransportView::from(self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for PhrasesModel {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
PhrasesView(self).layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
PhrasesView(self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for PhraseEditorModel {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
PhraseView(self).layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
PhraseView(self).render(to)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue