mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
wip: p.54, e=85
This commit is contained in:
parent
76af9d9bac
commit
54fb5b6ece
9 changed files with 629 additions and 571 deletions
|
|
@ -14,6 +14,7 @@ use std::fmt::Debug;
|
|||
submod! {
|
||||
tui_apis
|
||||
tui_command
|
||||
tui_content
|
||||
tui_control
|
||||
tui_focus
|
||||
tui_handle
|
||||
|
|
@ -26,35 +27,40 @@ submod! {
|
|||
tui_status
|
||||
tui_theme
|
||||
tui_view
|
||||
tui_widget
|
||||
}
|
||||
|
||||
fn content_with_menu_and_status <'a, S: Send + Sync + 'a, C: Command<S>> (
|
||||
content: &'a impl Widget<Engine = Tui>,
|
||||
menu_bar: Option<&'a MenuBar<Tui, S, C>>,
|
||||
status_bar: Option<impl Widget<Engine = Tui>>
|
||||
) -> impl Widget<Engine = Tui> + 'a {
|
||||
fn content_with_menu_and_status <'a, A, S, C> (
|
||||
content: &'a A,
|
||||
menu_bar: &'a Option<MenuBar<Tui, S, C>>,
|
||||
status_bar: &'a Option<impl StatusBar>
|
||||
) -> impl Widget<Engine = Tui> + 'a
|
||||
where
|
||||
A: Widget<Engine = Tui>,
|
||||
S: Send + Sync + 'a,
|
||||
C: Command<S>
|
||||
{
|
||||
let menus = menu_bar.as_ref().map_or_else(
|
||||
||&[] as &[Menu<_, _, _>],
|
||||
|m|m.menus.as_slice()
|
||||
);
|
||||
let content = Either(
|
||||
status_bar.is_none(),
|
||||
widget(&content),
|
||||
Split::up(
|
||||
1,
|
||||
widget(status_bar.as_ref().unwrap()),
|
||||
widget(&content)
|
||||
),
|
||||
);
|
||||
Either(
|
||||
menu_bar.is_none(),
|
||||
widget(&content),
|
||||
Either(
|
||||
status_bar.is_none(),
|
||||
widget(content),
|
||||
Split::up(
|
||||
1,
|
||||
widget(status_bar.as_ref().unwrap()),
|
||||
widget(content)
|
||||
),
|
||||
),
|
||||
Split::down(
|
||||
1,
|
||||
row!(menu in menus.iter() => {
|
||||
row!(" ", menu.title.as_str(), " ")
|
||||
}),
|
||||
widget(&content)
|
||||
widget(content)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ pub enum PhraseCommand {
|
|||
impl<T: TransportControl> Command<T> for TransportCommand {
|
||||
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||
use TransportCommand::{Focus, Clock, Playhead};
|
||||
use FocusCommand::{Next, Prev};
|
||||
use ClockCommand::{SetBpm, SetQuant, SetSync};
|
||||
Ok(Some(match self {
|
||||
Focus(Next) => { todo!() }
|
||||
|
|
@ -133,6 +134,24 @@ impl<T: ArrangerControl + FocusGrid> Command<T> for ArrangerCommand {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> Command<T> for ArrangerSceneCommand {
|
||||
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Command<T> for ArrangerTrackCommand {
|
||||
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Command<T> for ArrangerClipCommand {
|
||||
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<PhrasesTui> for PhrasesCommand {
|
||||
fn execute (self, view: &mut PhrasesTui) -> Perhaps<Self> {
|
||||
use PhraseRenameCommand as Rename;
|
||||
|
|
|
|||
311
crates/tek_tui/src/tui_content.rs
Normal file
311
crates/tek_tui/src/tui_content.rs
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
use crate::*;
|
||||
|
||||
impl<'a, T: TransportViewState> Content for TransportView<'a, T> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let state = self.0;
|
||||
lay!(
|
||||
state.focus().wrap(state.is_focused(), TransportFocus::PlayPause, &Styled(
|
||||
None,
|
||||
match state.transport_state() {
|
||||
Some(TransportState::Rolling) => "▶ PLAYING",
|
||||
Some(TransportState::Starting) => "READY ...",
|
||||
Some(TransportState::Stopped) => "⏹ STOPPED",
|
||||
_ => unreachable!(),
|
||||
}
|
||||
).min_xy(11, 2).push_x(1)).align_x().fill_x(),
|
||||
|
||||
row!(
|
||||
state.focus().wrap(state.is_focused(), TransportFocus::Bpm, &Outset::X(1u16, {
|
||||
let bpm = state.bpm_value();
|
||||
row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) }
|
||||
})),
|
||||
//let quant = state.focus().wrap(state.focused(), TransportFocus::Quant, &Outset::X(1u16, row! {
|
||||
//"QUANT ", ppq_to_name(state.0.quant as usize)
|
||||
//})),
|
||||
state.focus().wrap(state.is_focused(), TransportFocus::Sync, &Outset::X(1u16, row! {
|
||||
"SYNC ", pulses_to_name(state.sync_value() as usize)
|
||||
}))
|
||||
).align_w().fill_x(),
|
||||
|
||||
state.focus().wrap(state.is_focused(), TransportFocus::Clock, &{
|
||||
let time1 = state.format_beat();
|
||||
let time2 = state.format_msu();
|
||||
row!("B" ,time1.as_str(), " T", time2.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> {
|
||||
col!(
|
||||
widget(&TransportView(self)),
|
||||
Split::right(20,
|
||||
widget(&PhrasesView(self)),
|
||||
widget(&PhraseView(self)),
|
||||
).min_y(20)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout for standalone arranger app.
|
||||
impl Content for ArrangerTui {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
Split::up(
|
||||
1,
|
||||
widget(&TransportView(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(ArrangerViewState::focused(self))))),
|
||||
widget(&self.size),
|
||||
widget(&format!("[{}] Arranger", if self.entered {
|
||||
"■"
|
||||
} else {
|
||||
" "
|
||||
}))
|
||||
.fg(TuiTheme::title_fg(ArrangerViewState::focused(self)))
|
||||
.push_x(1),
|
||||
),
|
||||
Split::right(
|
||||
self.splits[1],
|
||||
widget(&PhrasesView(self)),
|
||||
widget(&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.focused();
|
||||
let entered = self.0.entered();
|
||||
let phrases = self.0.phrases();
|
||||
let selected_phrase = self.0.phrase();
|
||||
let mode = self.0.mode();
|
||||
let content = col!(
|
||||
(i, phrase) in phrases.iter().enumerate() => 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_phrase {
|
||||
add(&CORNERS)?;
|
||||
}
|
||||
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<'a, T: PhraseViewState> Content for PhraseView<'a, T> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let phrase = self.0.phrase();
|
||||
let size = self.0.size();
|
||||
let focused = self.0.focused();
|
||||
let entered = self.0.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(),
|
||||
"]"
|
||||
)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -20,25 +20,37 @@ pub trait TransportControl {
|
|||
|
||||
impl TransportControl for TransportTui {
|
||||
fn bpm (&self) -> &BeatsPerMinute {
|
||||
self.bpm()
|
||||
&self.current.timebase.bpm
|
||||
}
|
||||
fn quant (&self) -> &Quantize {
|
||||
self.quant()
|
||||
&self.quant
|
||||
}
|
||||
fn sync (&self) -> &LaunchSync {
|
||||
self.sync()
|
||||
&self.sync
|
||||
}
|
||||
}
|
||||
|
||||
impl TransportControl for SequencerTui {
|
||||
fn bpm (&self) -> &BeatsPerMinute {
|
||||
self.bpm()
|
||||
&self.current.timebase.bpm
|
||||
}
|
||||
fn quant (&self) -> &Quantize {
|
||||
self.quant()
|
||||
&self.quant
|
||||
}
|
||||
fn sync (&self) -> &LaunchSync {
|
||||
self.sync()
|
||||
&self.sync
|
||||
}
|
||||
}
|
||||
|
||||
impl TransportControl for ArrangerTui {
|
||||
fn bpm (&self) -> &BeatsPerMinute {
|
||||
&self.current.timebase.bpm
|
||||
}
|
||||
fn quant (&self) -> &Quantize {
|
||||
&self.quant
|
||||
}
|
||||
fn sync (&self) -> &LaunchSync {
|
||||
&self.sync
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,22 +72,22 @@ impl TransportFocus {
|
|||
//type Item = TransportFocus;
|
||||
//}
|
||||
|
||||
impl FocusEnter for TransportTui {
|
||||
type Item = TransportFocus;
|
||||
fn focus_enter (&mut self) {
|
||||
self.entered = true;
|
||||
}
|
||||
fn focus_exit (&mut self) {
|
||||
self.entered = false;
|
||||
}
|
||||
fn focus_entered (&self) -> Option<Self::Item> {
|
||||
if self.entered {
|
||||
Some(self.focused())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
//impl FocusEnter for TransportTui {
|
||||
//type Item = TransportFocus;
|
||||
//fn focus_enter (&mut self) {
|
||||
//self.entered = true;
|
||||
//}
|
||||
//fn focus_exit (&mut self) {
|
||||
//self.entered = false;
|
||||
//}
|
||||
//fn focus_entered (&self) -> Option<Self::Item> {
|
||||
//if self.entered {
|
||||
//Some(self.focused())
|
||||
//} else {
|
||||
//None
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
|
||||
impl FocusGrid for TransportTui {
|
||||
type Item = TransportFocus;
|
||||
|
|
|
|||
|
|
@ -61,8 +61,17 @@ impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
|
|||
key!(KeyCode::Left) => Some(Self::Focus(Left)),
|
||||
key!(KeyCode::Right) => Some(Self::Focus(Right)),
|
||||
_ => Some(Self::App(match state.focused() {
|
||||
SequencerFocus::Transport =>
|
||||
TransportCommand::input_to_command(&state, input).map(Transport),
|
||||
SequencerFocus::Transport => {
|
||||
use TransportCommand::{Clock, Playhead};
|
||||
match TransportCommand::input_to_command(view, input)? {
|
||||
Clock(command) => {
|
||||
todo!()
|
||||
},
|
||||
Playhead(command) => {
|
||||
todo!()
|
||||
},
|
||||
}
|
||||
},
|
||||
SequencerFocus::Phrases =>
|
||||
PhrasesCommand::input_to_command(&state.phrases, input).map(Phrases),
|
||||
SequencerFocus::PhraseEditor =>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ impl Audio for ArrangerTui {
|
|||
let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t));
|
||||
if let Some(Some(Some(phrase))) = phrase {
|
||||
if let Some(track) = self.tracks().get(t) {
|
||||
if let Some((ref started_at, Some(ref playing))) = track.player.phrase {
|
||||
if let Some((ref started_at, Some(ref playing))) = track.play_phrase {
|
||||
let phrase = phrase.read().unwrap();
|
||||
if *playing.read().unwrap() == *phrase {
|
||||
let pulse = self.current().pulse.get();
|
||||
|
|
|
|||
|
|
@ -1,15 +1,5 @@
|
|||
use crate::*;
|
||||
|
||||
impl Widget for TransportTui {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
TransportView(&self, Default::default()).layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
TransportView(&self, Default::default()).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TransportView<'a, T: TransportViewState>(pub &'a T);
|
||||
|
||||
pub trait TransportViewState: Send + Sync {
|
||||
|
|
@ -27,7 +17,7 @@ impl TransportViewState for TransportTui {
|
|||
self.focus
|
||||
}
|
||||
fn is_focused (&self) -> bool {
|
||||
self.focused
|
||||
true
|
||||
}
|
||||
fn transport_state (&self) -> Option<TransportState> {
|
||||
*self.playing().read().unwrap()
|
||||
|
|
@ -46,63 +36,12 @@ impl TransportViewState for TransportTui {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, T: TransportViewState> Content for TransportView<'a, T> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let state = self.0;
|
||||
lay!(
|
||||
state.focus().wrap(state.is_focused(), TransportFocus::PlayPause, &Styled(
|
||||
None,
|
||||
match state.transport_state() {
|
||||
Some(TransportState::Rolling) => "▶ PLAYING",
|
||||
Some(TransportState::Starting) => "READY ...",
|
||||
Some(TransportState::Stopped) => "⏹ STOPPED",
|
||||
_ => unreachable!(),
|
||||
}
|
||||
).min_xy(11, 2).push_x(1)).align_x().fill_x(),
|
||||
|
||||
row!(
|
||||
state.focus().wrap(state.is_focused(), TransportFocus::Bpm, &Outset::X(1u16, {
|
||||
let bpm = state.bpm_value();
|
||||
row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) }
|
||||
})),
|
||||
//let quant = state.focus().wrap(state.focused(), TransportFocus::Quant, &Outset::X(1u16, row! {
|
||||
//"QUANT ", ppq_to_name(state.0.quant as usize)
|
||||
//})),
|
||||
state.focus().wrap(state.is_focused(), TransportFocus::Sync, &Outset::X(1u16, row! {
|
||||
"SYNC ", pulses_to_name(state.sync_value() as usize)
|
||||
}))
|
||||
).align_w().fill_x(),
|
||||
|
||||
state.focus().wrap(state.is_focused(), TransportFocus::Clock, &{
|
||||
let time1 = state.format_beat();
|
||||
let time2 = state.format_msu();
|
||||
row!("B" ,time1.as_str(), " T", time2.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> {
|
||||
col!(
|
||||
widget(&TransportView(self)),
|
||||
Split::right(20,
|
||||
widget(&PhrasesView(self)),
|
||||
widget(&PhraseView(self)),
|
||||
).min_y(20)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TransportViewState for SequencerTui {
|
||||
fn focus (&self) -> TransportFocus {
|
||||
self.focus
|
||||
}
|
||||
fn is_focused (&self) -> bool {
|
||||
self.focused
|
||||
self.focused() == SequencerFocus::Transport
|
||||
}
|
||||
fn transport_state (&self) -> Option<TransportState> {
|
||||
*self.playing().read().unwrap()
|
||||
|
|
@ -121,6 +60,68 @@ impl TransportViewState for SequencerTui {
|
|||
}
|
||||
}
|
||||
|
||||
impl TransportViewState for ArrangerTui {
|
||||
fn focus (&self) -> TransportFocus {
|
||||
self.focus
|
||||
}
|
||||
fn is_focused (&self) -> bool {
|
||||
self.focused() == ArrangerFocus::Transport
|
||||
}
|
||||
fn transport_state (&self) -> Option<TransportState> {
|
||||
*self.playing().read().unwrap()
|
||||
}
|
||||
fn bpm_value (&self) -> f64 {
|
||||
self.bpm().get()
|
||||
}
|
||||
fn sync_value (&self) -> f64 {
|
||||
self.sync().get()
|
||||
}
|
||||
fn format_beat (&self) -> String {
|
||||
self.current().format_beat()
|
||||
}
|
||||
fn format_msu (&self) -> String {
|
||||
self.current().usec.format_msu()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ArrangerViewState {
|
||||
fn is_focused (&self) -> bool;
|
||||
}
|
||||
|
||||
impl ArrangerViewState for ArrangerTui {
|
||||
fn is_focused (&self) -> bool {
|
||||
self.focused() == ArrangerFocus::Arranger
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PhrasesView<'a, T: PhrasesViewState>(pub &'a T);
|
||||
|
||||
pub trait PhrasesViewState: Send + Sync {
|
||||
fn focused (&self) -> bool;
|
||||
fn entered (&self) -> bool;
|
||||
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>>;
|
||||
fn phrase (&self) -> usize;
|
||||
fn mode (&self) -> &Option<PhrasesMode>;
|
||||
}
|
||||
|
||||
impl PhrasesViewState for PhrasesTui {
|
||||
fn focused (&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
fn entered (&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>> {
|
||||
todo!()
|
||||
}
|
||||
fn phrase (&self) -> usize {
|
||||
todo!()
|
||||
}
|
||||
fn mode (&self) -> &Option<PhrasesMode> {
|
||||
&self.mode
|
||||
}
|
||||
}
|
||||
|
||||
impl PhrasesViewState for SequencerTui {
|
||||
fn focused (&self) -> bool {
|
||||
todo!()
|
||||
|
|
@ -134,11 +135,77 @@ impl PhrasesViewState for SequencerTui {
|
|||
fn phrase (&self) -> usize {
|
||||
todo!()
|
||||
}
|
||||
fn mode (&self) -> Option<&PhrasesMode> {
|
||||
fn mode (&self) -> &Option<PhrasesMode> {
|
||||
&self.mode
|
||||
}
|
||||
}
|
||||
|
||||
impl PhrasesViewState for ArrangerTui {
|
||||
fn focused (&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
fn entered (&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>> {
|
||||
todo!()
|
||||
}
|
||||
fn phrase (&self) -> usize {
|
||||
todo!()
|
||||
}
|
||||
fn mode (&self) -> &Option<PhrasesMode> {
|
||||
&self.mode
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PhraseView<'a, T: PhraseViewState>(pub &'a T);
|
||||
|
||||
pub trait PhraseViewState: Send + Sync {
|
||||
fn focused (&self) -> bool;
|
||||
fn entered (&self) -> bool;
|
||||
fn keys (&self) -> &Buffer;
|
||||
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>>;
|
||||
fn buffer (&self) -> &BigBuffer;
|
||||
fn note_len (&self) -> usize;
|
||||
fn note_axis (&self) -> &RwLock<FixedAxis<usize>>;
|
||||
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>>;
|
||||
fn size (&self) -> &Measure<Tui>;
|
||||
fn now (&self) -> &Arc<Pulse>;
|
||||
}
|
||||
|
||||
impl PhraseViewState for PhraseTui {
|
||||
fn focused (&self) -> bool {
|
||||
self.focused
|
||||
}
|
||||
fn entered (&self) -> bool {
|
||||
self.entered
|
||||
}
|
||||
fn keys (&self) -> &Buffer {
|
||||
&self.keys
|
||||
}
|
||||
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>> {
|
||||
&self.phrase
|
||||
}
|
||||
fn buffer (&self) -> &BigBuffer {
|
||||
&self.buffer
|
||||
}
|
||||
fn note_len (&self) -> usize {
|
||||
self.note_len
|
||||
}
|
||||
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
|
||||
&self.note_axis
|
||||
}
|
||||
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
|
||||
&self.time_axis
|
||||
}
|
||||
fn size (&self) -> &Measure<Tui> {
|
||||
&self.size
|
||||
}
|
||||
fn now (&self) -> &Arc<Pulse> {
|
||||
&self.now
|
||||
}
|
||||
}
|
||||
|
||||
impl PhraseViewState for SequencerTui {
|
||||
fn focused (&self) -> bool {
|
||||
todo!()
|
||||
|
|
@ -172,6 +239,75 @@ impl PhraseViewState for SequencerTui {
|
|||
}
|
||||
}
|
||||
|
||||
impl PhraseViewState for ArrangerTui {
|
||||
fn focused (&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
fn entered (&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
fn keys (&self) -> &Buffer {
|
||||
todo!()
|
||||
}
|
||||
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>> {
|
||||
todo!()
|
||||
}
|
||||
fn buffer (&self) -> &BigBuffer {
|
||||
todo!()
|
||||
}
|
||||
fn note_len (&self) -> usize {
|
||||
todo!()
|
||||
}
|
||||
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
|
||||
todo!()
|
||||
}
|
||||
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
|
||||
todo!()
|
||||
}
|
||||
fn size (&self) -> &Measure<Tui> {
|
||||
todo!()
|
||||
}
|
||||
fn now (&self) -> &Arc<Pulse> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
}
|
||||
|
||||
/// Display mode of arranger
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum ArrangerMode {
|
||||
|
|
@ -195,48 +331,6 @@ impl ArrangerMode {
|
|||
}
|
||||
}
|
||||
|
||||
/// Layout for standalone arranger app.
|
||||
impl Content for ArrangerTui {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
Split::up(
|
||||
1,
|
||||
widget(&TransportView(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(self.focused() == ArrangerFocus::Arranger)))),
|
||||
widget(&self.size),
|
||||
widget(&format!("[{}] Arranger", if self.entered {
|
||||
"■"
|
||||
} else {
|
||||
" "
|
||||
}))
|
||||
.fg(TuiTheme::title_fg(self.focused() == ArrangerFocus::Arranger))
|
||||
.push_x(1),
|
||||
),
|
||||
Split::right(
|
||||
self.splits[1],
|
||||
widget(&PhrasesView(self)),
|
||||
widget(&PhraseView(self)),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> {
|
||||
let mut widths = vec![];
|
||||
let mut total = 0;
|
||||
|
|
@ -629,350 +723,6 @@ pub fn arranger_content_horizontal (
|
|||
)
|
||||
}
|
||||
|
||||
impl TransportViewState for ArrangerTui {
|
||||
fn focus (&self) -> TransportFocus {
|
||||
self.focus
|
||||
}
|
||||
fn is_focused (&self) -> bool {
|
||||
self.focused
|
||||
}
|
||||
fn transport_state (&self) -> Option<TransportState> {
|
||||
*self.playing().read().unwrap()
|
||||
}
|
||||
fn bpm_value (&self) -> f64 {
|
||||
self.bpm().get()
|
||||
}
|
||||
fn sync_value (&self) -> f64 {
|
||||
self.sync().get()
|
||||
}
|
||||
fn format_beat (&self) -> String {
|
||||
self.current().format_beat()
|
||||
}
|
||||
fn format_msu (&self) -> String {
|
||||
self.current().usec.format_msu()
|
||||
}
|
||||
}
|
||||
|
||||
impl PhrasesViewState for ArrangerTui {
|
||||
fn focused (&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
fn entered (&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>> {
|
||||
todo!()
|
||||
}
|
||||
fn phrase (&self) -> usize {
|
||||
todo!()
|
||||
}
|
||||
fn mode (&self) -> Option<&PhrasesMode> {
|
||||
&self.mode
|
||||
}
|
||||
}
|
||||
|
||||
impl PhraseViewState for ArrangerTui {
|
||||
fn focused (&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
fn entered (&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
fn keys (&self) -> &Buffer {
|
||||
todo!()
|
||||
}
|
||||
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>> {
|
||||
todo!()
|
||||
}
|
||||
fn buffer (&self) -> &BigBuffer {
|
||||
todo!()
|
||||
}
|
||||
fn note_len (&self) -> usize {
|
||||
todo!()
|
||||
}
|
||||
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
|
||||
todo!()
|
||||
}
|
||||
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
|
||||
todo!()
|
||||
}
|
||||
fn size (&self) -> &Measure<Tui> {
|
||||
todo!()
|
||||
}
|
||||
fn now (&self) -> &Arc<Pulse> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for PhrasesTui {
|
||||
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 trait PhrasesViewState {
|
||||
fn focused (&self) -> bool;
|
||||
fn entered (&self) -> bool;
|
||||
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>>;
|
||||
fn phrase (&self) -> usize;
|
||||
fn mode (&self) -> Option<&PhrasesMode>;
|
||||
}
|
||||
|
||||
impl PhrasesViewState for PhrasesTui {
|
||||
fn focused (&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
fn entered (&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>> {
|
||||
todo!()
|
||||
}
|
||||
fn phrase (&self) -> usize {
|
||||
todo!()
|
||||
}
|
||||
fn mode (&self) -> Option<&PhrasesMode> {
|
||||
&self.mode
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PhrasesView<'a, T: PhrasesViewState>(&'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.focused();
|
||||
let entered = self.0.entered();
|
||||
let phrases = self.0.phrases();
|
||||
let selected_phrase = self.0.phrase();
|
||||
let mode = self.0.mode();
|
||||
let content = col!(
|
||||
(i, phrase) in phrases.iter().enumerate() => 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_phrase {
|
||||
add(&CORNERS)?;
|
||||
}
|
||||
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 struct PhraseView<'a, T: PhraseViewState>(pub &'a T);
|
||||
|
||||
pub trait PhraseViewState: Send + Sync {
|
||||
fn focused (&self) -> bool;
|
||||
fn entered (&self) -> bool;
|
||||
fn keys (&self) -> &Buffer;
|
||||
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>>;
|
||||
fn buffer (&self) -> &BigBuffer;
|
||||
fn note_len (&self) -> usize;
|
||||
fn note_axis (&self) -> &RwLock<FixedAxis<usize>>;
|
||||
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>>;
|
||||
fn size (&self) -> &Measure<Tui>;
|
||||
fn now (&self) -> &Arc<Pulse>;
|
||||
}
|
||||
|
||||
impl PhraseViewState for PhraseTui {
|
||||
fn focused (&self) -> bool {
|
||||
self.focused
|
||||
}
|
||||
fn entered (&self) -> bool {
|
||||
self.entered
|
||||
}
|
||||
fn keys (&self) -> &Buffer {
|
||||
&self.keys
|
||||
}
|
||||
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>> {
|
||||
&self.phrase
|
||||
}
|
||||
fn buffer (&self) -> &BigBuffer {
|
||||
&self.buffer
|
||||
}
|
||||
fn note_len (&self) -> usize {
|
||||
self.note_len
|
||||
}
|
||||
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
|
||||
&self.note_axis
|
||||
}
|
||||
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
|
||||
&self.time_axis
|
||||
}
|
||||
fn size (&self) -> &Measure<Tui> {
|
||||
&self.size
|
||||
}
|
||||
fn now (&self) -> &Arc<Pulse> {
|
||||
&self.now
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: PhraseViewState> Content for PhraseView<'a, T> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let phrase = self.0.phrase();
|
||||
let size = self.0.size();
|
||||
let focused = self.0.focused();
|
||||
let entered = self.0.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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Colors of piano keys
|
||||
const KEY_COLORS: [(Color, Color);6] = [
|
||||
(Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)),
|
||||
|
|
@ -1017,83 +767,3 @@ pub(crate) fn keys_vert () -> Buffer {
|
|||
const NTH_OCTAVE: [&'static str; 11] = [
|
||||
"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8",
|
||||
];
|
||||
|
||||
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(),
|
||||
"]"
|
||||
)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 Widget for PhraseTui {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
PhraseView(&self, Default::default()).layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
PhraseView(&self, Default::default()).render(to)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
crates/tek_tui/src/tui_widget.rs
Normal file
31
crates/tek_tui/src/tui_widget.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use crate::*;
|
||||
|
||||
impl Widget for TransportTui {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
TransportView(self).layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
TransportView(self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for PhrasesTui {
|
||||
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 PhraseTui {
|
||||
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