tek/crates/tek_sequencer/src/arranger_tui.rs

488 lines
21 KiB
Rust

use crate::*;
/// Layout for standalone arranger app.
impl Content for Arranger<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Split::down(
1,
widget(&self.transport),
Split::up(
1,
widget(&self.status),
Split::down(
self.arrangement_split,
widget(&self.arrangement),
Split::right(
self.phrases_split,
self.phrases.clone(),
widget(&self.editor),
)
)
)
)
}
}
impl Content for ArrangerStatusBar {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let label = match self {
Self::Transport => "TRANSPORT",
Self::ArrangementMix => "PROJECT",
Self::ArrangementTrack => "TRACK",
Self::ArrangementScene => "SCENE",
Self::ArrangementClip => "CLIP",
Self::PhrasePool => "SEQ LIST",
Self::PhraseView => "VIEW SEQ",
Self::PhraseEdit => "EDIT SEQ",
};
let mode = TuiStyle::bg(format!(" {label} "), Color::Rgb(150, 160, 90))
.fg(Color::Rgb(0, 0, 0))
.bold(true);
let commands = match self {
Self::ArrangementMix => command(&[
["", "c", "olor"],
["", ",.", "scale rows"],
["", "<>", "resize view"],
]),
Self::ArrangementClip => command(&[
["", "g", "et"],
["", "s", "et"],
["", "a", "dd"],
["", "i", "ns"],
["", "d", "up"],
["", "e", "dit"],
["", "c", "olor"],
["re", "n", "ame"],
["", ",.", "select"],
]),
Self::ArrangementTrack => command(&[
["", ",.", "resize"],
["", "<>", "move"],
["", "i", "nput"],
["", "o", "utput"],
["", "m", "ute"],
["", "s", "olo"],
]),
Self::PhrasePool => command(&[
["", "a", "ppend"],
["", "i", "nsert"],
["", "d", "uplicate"],
["", "c", "olor"],
["re", "n", "ame"],
["leng", "t", "h"],
["", ",.", "move"],
["", "<>", "resize view"],
]),
Self::PhraseView => command(&[
["", "enter", " edit"],
["", "arrows/pgup/pgdn", " scroll"],
["", ",.", "zoom"],
]),
Self::PhraseEdit => command(&[
["", "esc", " exit"],
["", "a", "ppend"],
["", "s", "et"],
["", ",.", "length"],
["", "<>", "zoom"],
]),
_ => command(&[])
};
//let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}"));
row!(mode, commands)
}
}
fn command (
commands: &[[impl Widget<Engine = Tui>;3]]
) -> impl Widget<Engine = Tui> + '_ {
Stack::right(|add|{
for [a, b, c] in commands.iter() {
add(&row!(" ", widget(a), widget(b).bold(true).fg(Color::Rgb(255,255,0)), widget(c)))?;
}
Ok(())
})
}
impl Content for Arrangement<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Layers::new(move |add|{
match self.mode {
ArrangementViewMode::Horizontal => add(&HorizontalArranger(&self)),
ArrangementViewMode::Vertical(factor) => add(&VerticalArranger(&self, factor)),
}?;
let color = if self.focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)};
add(&TuiStyle::fg("Session", color).push_x(1))
})
}
}
impl<'a> Content for VerticalArranger<'a, Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let Self(state, factor) = self;
let cols = state.track_widths();
let rows = Scene::ppqs(state.scenes.as_slice(), *factor);
let tracks = state.tracks.as_ref() as &[ArrangementTrack<Tui>];
let scenes = state.scenes.as_ref();
//panic!("{scenes:#?} {rows:#?}");
let offset = 3 + Scene::longest_name(scenes) as u16; // x of 1st track
let bg = state.color;
let clip_bg = Color::Rgb(40, 50, 30);
let border_bg = Color::Rgb(40, 50, 30);
let border_hi = Color::Rgb(100, 110, 40);
let border_lo = Color::Rgb(70, 80, 50);
let border_fg = if self.0.focused { border_hi } else { border_lo };
let border = Lozenge(Style::default().bg(border_bg).fg(border_fg));
Layers::new(move |add|{
let rows: &[(usize, usize)] = rows.as_ref();
let cols: &[(usize, usize)] = cols.as_ref();
// column separators
add(&CustomWidget::new(|_|Ok(Some([0,0])), move|to: &mut TuiOutput|{
let area = to.area();
let style = Some(Style::default().fg(COLOR_SEPARATOR));
for x in cols.iter().map(|col|col.1) {
let x = offset + area.x() + x as u16 - 1;
for y in area.y()..area.y2() { to.blit(&"", x, y, style); }
}
Ok(())
}))?;
// row separators
add(&CustomWidget::new(|_|Ok(Some([0,0])), move|to: &mut TuiOutput|{
let area = to.area();
for y in rows.iter().map(|row|row.1) {
let y = area.y() + (y / PPQ) as u16 + 1;
if y >= to.buffer.area.height { break }
for x in area.x()..area.x2().saturating_sub(2) {
if x < to.buffer.area.x && y < to.buffer.area.y {
let cell = to.buffer.get_mut(x, y);
cell.modifier = Modifier::UNDERLINED;
cell.underline_color = COLOR_SEPARATOR;
}
}
}
Ok(())
}))?;
// track titles
let track_titles = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{
let name = track.name.read().unwrap();
let max_w = w.saturating_sub(1).min(name.len()).max(2);
(&format!("{}", &name[0..max_w]).as_str())
.min_xy(w as u16, 2)
.bg(track.color)
.push_x(offset - 1)
});
// scene titles
let scene_name = |scene, playing: bool, height|row!(
if playing { "" } else { " " },
(scene as &Scene).name.read().unwrap().as_str(),
).fixed_xy(offset.saturating_sub(1), height);
// scene clips
let scene_clip = |scene, track: usize, w: u16, h: u16|Layers::new(move |add|{
let mut color = clip_bg;
match (tracks.get(track), (scene as &Scene).clips.get(track)) {
(Some(_), Some(Some(phrase))) => {
let name = &(phrase as &Arc<RwLock<Phrase>>).read().unwrap().name;
let name = format!("{}", name);
let max_w = name.len().min((w as usize).saturating_sub(2));
add(&name.as_str()[0..max_w].push_x(1).fixed_x(w))?;
color = (phrase as &Arc<RwLock<Phrase>>).read().unwrap().color;
},
_ => {}
};
add(&Background(color))
}).fixed_xy(w, h);
add(&col!(track_titles, col!(
// scenes:
(scene, pulses) in scenes.iter().zip(rows.iter().map(|row|row.0)) => {
let height = 1.max((pulses / PPQ) as u16);
let playing = scene.is_playing(tracks);
Stack::right(move |add| {
// scene title:
add(&scene_name(scene, playing, height).bg(scene.color))?;
// clip per track:
for (track, w) in cols.iter().map(|col|col.0).enumerate() {
add(&scene_clip(scene, track, w as u16, height))?;
}
Ok(())
}).fixed_y(height)
}
)))?;
// cursor
add(&CustomWidget::new(|_|Ok(Some([0,0])), move|to: &mut TuiOutput|{
let area = to.area();
let focused = state.focused;
let selected = state.selected;
let get_track_area = |t: usize| [
offset + area.x() + cols[t].1 as u16 - 1, area.y(),
cols[t].0 as u16, area.h(),
];
let get_scene_area = |s: usize| [
area.x(), 2 + area.y() + (rows[s].1 / PPQ) as u16,
area.w(), (rows[s].0 / PPQ) as u16
];
let get_clip_area = |t: usize, s: usize| [
offset+area.x()+cols[t].1 as u16 - 1, 2+area.y()+(rows[s].1/PPQ) as u16,
cols[t].0 as u16, (rows[s].0 / PPQ) as u16
];
let mut track_area: Option<[u16;4]> = None;
let mut scene_area: Option<[u16;4]> = None;
let mut clip_area: Option<[u16;4]> = None;
let area = match selected {
ArrangementFocus::Mix => {
//if focused { to.fill_bg(area, Color::Rgb(40, 50, 30)); }
area
},
ArrangementFocus::Track(t) => { track_area = Some(get_track_area(t)); area },
ArrangementFocus::Scene(s) => { scene_area = Some(get_scene_area(s)); area },
ArrangementFocus::Clip(t, s) => {
track_area = Some(get_track_area(t));
scene_area = Some(get_scene_area(s));
clip_area = Some(get_clip_area(t, s));
area
},
};
if let Some([x, y, width, height]) = track_area {
to.fill_fg([x, y, 1, height], Color::Rgb(70, 80, 50));
to.fill_fg([x + width, y, 1, height], Color::Rgb(70, 80, 50));
}
if let Some([_, y, _, height]) = scene_area {
to.fill_ul([area.x(), y - 1, area.w(), 1], Color::Rgb(70, 80, 50));
to.fill_ul([area.x(), y + height - 1, area.w(), 1], Color::Rgb(70, 80, 50));
}
if focused {
if let Some(clip_area) = clip_area {
to.render_in(clip_area, &CORNERS)?;
//to.fill_bg(clip_area, Color::Rgb(40, 50, 30));
} else if let Some(track_area) = track_area {
to.render_in(track_area.clip_h(2u16), &CORNERS)?;
//to.fill_bg(track_area, Color::Rgb(40, 50, 30));
} else if let Some(scene_area) = scene_area {
to.render_in(scene_area.clip_w(offset-1), &CORNERS)?;
//to.fill_bg(scene_area, Color::Rgb(40, 50, 30));
} else {
to.render_in(area.clip_w(offset-1).clip_h(2u16), &CORNERS)?;
}
}
Ok(())
}))?;
Ok(())
}).bg(bg).grow_y(1).border(border)
}
}
impl<'a> Content for HorizontalArranger<'a, Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let Arrangement { tracks, focused, .. } = self.0;
let _tracks = tracks.as_slice();
lay!(
focused.then_some(Background(Color::Rgb(40, 50, 30))),
row!(
// name
CustomWidget::new(|_|{
todo!()
}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks, selected) = self;
//let yellow = Some(Style::default().yellow().bold().not_dim());
//let white = Some(Style::default().white().bold().not_dim());
//let area = to.area();
//let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()];
//let offset = 0; // track scroll offset
//for y in 0..area.h() {
//if y == 0 {
//to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2 + offset;
//if let Some(track) = tracks.get(index) {
//let selected = selected.track() == Some(index);
//let style = if selected { yellow } else { white };
//to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?;
//to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?;
//}
//}
//}
//Ok(Some(area))
}),
// monitor
CustomWidget::new(|_|{
todo!()
}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks) = self;
//let mut area = to.area();
//let on = Some(Style::default().not_dim().green().bold());
//let off = Some(DIM);
//area.x += 1;
//for y in 0..area.h() {
//if y == 0 {
////" MON ".blit(to.buffer, area.x, area.y + y, style2)?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2;
//if let Some(track) = tracks.get(index) {
//let style = if track.monitoring { on } else { off };
//to.blit(&" MON ", area.x(), area.y() + y, style)?;
//} else {
//area.height = y;
//break
//}
//}
//}
//area.width = 4;
//Ok(Some(area))
}),
// record
CustomWidget::new(|_|{
todo!()
}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks) = self;
//let mut area = to.area();
//let on = Some(Style::default().not_dim().red().bold());
//let off = Some(Style::default().dim());
//area.x += 1;
//for y in 0..area.h() {
//if y == 0 {
////" REC ".blit(to.buffer, area.x, area.y + y, style2)?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2;
//if let Some(track) = tracks.get(index) {
//let style = if track.recording { on } else { off };
//to.blit(&" REC ", area.x(), area.y() + y, style)?;
//} else {
//area.height = y;
//break
//}
//}
//}
//area.width = 4;
//Ok(Some(area))
}),
// overdub
CustomWidget::new(|_|{
todo!()
}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks) = self;
//let mut area = to.area();
//let on = Some(Style::default().not_dim().yellow().bold());
//let off = Some(Style::default().dim());
//area.x = area.x + 1;
//for y in 0..area.h() {
//if y == 0 {
////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2;
//if let Some(track) = tracks.get(index) {
//to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub {
//on
//} else {
//off
//})?;
//} else {
//area.height = y;
//break
//}
//}
//}
//area.width = 4;
//Ok(Some(area))
}),
// erase
CustomWidget::new(|_|{
todo!()
}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks) = self;
//let mut area = to.area();
//let off = Some(Style::default().dim());
//area.x = area.x + 1;
//for y in 0..area.h() {
//if y == 0 {
////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2;
//if let Some(_) = tracks.get(index) {
//to.blit(&" DEL ", area.x(), area.y() + y, off)?;
//} else {
//area.height = y;
//break
//}
//}
//}
//area.width = 4;
//Ok(Some(area))
}),
// gain
CustomWidget::new(|_|{
todo!()
}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks) = self;
//let mut area = to.area();
//let off = Some(Style::default().dim());
//area.x = area.x() + 1;
//for y in 0..area.h() {
//if y == 0 {
////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2;
//if let Some(_) = tracks.get(index) {
//to.blit(&" +0.0 ", area.x(), area.y() + y, off)?;
//} else {
//area.height = y;
//break
//}
//}
//}
//area.width = 7;
//Ok(Some(area))
}),
// scenes
CustomWidget::new(|_|{
todo!()
}, |to: &mut TuiOutput|{
let Arrangement { scenes, selected, .. } = self.0;
let area = to.area();
let mut x2 = 0;
let [x, y, _, height] = area;
for (scene_index, scene) in scenes.iter().enumerate() {
let active_scene = selected.scene() == Some(scene_index);
let sep = Some(if active_scene {
Style::default().yellow().not_dim()
} else {
Style::default().dim()
});
for y in y+1..y+height {
to.blit(&"", x + x2, y, sep);
}
let name = scene.name.read().unwrap();
let mut x3 = name.len() as u16;
to.blit(&*name, x + x2, y, sep);
for (i, clip) in scene.clips.iter().enumerate() {
let active_track = selected.track() == Some(i);
if let Some(clip) = clip {
let y2 = y + 2 + i as u16 * 2;
let label = format!("{}", clip.read().unwrap().name);
to.blit(&label, x + x2, y2, Some(if active_track && active_scene {
Style::default().not_dim().yellow().bold()
} else {
Style::default().not_dim()
}));
x3 = x3.max(label.len() as u16)
}
}
x2 = x2 + x3 + 1;
}
//Ok(Some([x, y, x2, height]))
Ok(())
}),
)
)
}
}