mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
break down arranger_tui
This commit is contained in:
parent
0b1193722d
commit
88e5aed2cd
7 changed files with 693 additions and 665 deletions
|
|
@ -39,100 +39,7 @@ impl Content for Arranger<Tui> {
|
|||
)
|
||||
}
|
||||
}
|
||||
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 status_bar_bg = Arranger::<Tui>::status_bar_bg();
|
||||
let mode_bg = Arranger::<Tui>::mode_bg();
|
||||
let mode_fg = Arranger::<Tui>::mode_fg();
|
||||
let mode = TuiStyle::bold(format!(" {label} "), true).bg(mode_bg).fg(mode_fg);
|
||||
let commands = match self {
|
||||
Self::ArrangementMix => command(&[
|
||||
["", "c", "olor"],
|
||||
["", "<>", "resize"],
|
||||
["", "+-", "zoom"],
|
||||
["", "n", "ame/number"],
|
||||
["", "Enter", " stop all"],
|
||||
]),
|
||||
Self::ArrangementClip => command(&[
|
||||
["", "g", "et"],
|
||||
["", "s", "et"],
|
||||
["", "a", "dd"],
|
||||
["", "i", "ns"],
|
||||
["", "d", "up"],
|
||||
["", "e", "dit"],
|
||||
["", "c", "olor"],
|
||||
["re", "n", "ame"],
|
||||
["", ",.", "select"],
|
||||
["", "Enter", " launch"],
|
||||
]),
|
||||
Self::ArrangementTrack => command(&[
|
||||
["re", "n", "ame"],
|
||||
["", ",.", "resize"],
|
||||
["", "<>", "move"],
|
||||
["", "i", "nput"],
|
||||
["", "o", "utput"],
|
||||
["", "m", "ute"],
|
||||
["", "s", "olo"],
|
||||
["", "Del", "ete"],
|
||||
["", "Enter", " stop"],
|
||||
]),
|
||||
Self::ArrangementScene => command(&[
|
||||
["re", "n", "ame"],
|
||||
["", "Del", "ete"],
|
||||
["", "Enter", " launch"],
|
||||
]),
|
||||
Self::PhrasePool => command(&[
|
||||
["", "a", "ppend"],
|
||||
["", "i", "nsert"],
|
||||
["", "d", "uplicate"],
|
||||
["", "Del", "ete"],
|
||||
["", "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).fill_x().bg(status_bar_bg)
|
||||
}
|
||||
}
|
||||
fn command (commands: &[[impl Widget<Engine = Tui>;3]]) -> impl Widget<Engine = Tui> + '_ {
|
||||
Stack::right(|add|{
|
||||
Ok(for [a, b, c] in commands.iter() {
|
||||
add(&row!(
|
||||
" ",
|
||||
widget(a),
|
||||
widget(b).bold(true).fg(Arranger::<Tui>::hotkey_fg()),
|
||||
widget(c),
|
||||
))?;
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
impl Content for Arrangement<Tui> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
|
|
@ -145,571 +52,3 @@ impl Content for Arrangement<Tui> {
|
|||
})
|
||||
}
|
||||
}
|
||||
impl<'a> Content for VerticalArranger<'a, Tui> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let Self(state, factor) = self;
|
||||
let tracks = state.tracks.as_ref() as &[ArrangementTrack];
|
||||
let scenes = state.scenes.as_ref();
|
||||
let cols = state.track_widths();
|
||||
let rows = Scene::ppqs(scenes, *factor);
|
||||
let bg = state.color;
|
||||
let clip_bg = Arranger::<Tui>::border_bg();
|
||||
let sep_fg = Arranger::<Tui>::separator_fg(false);
|
||||
let header_h = 3u16;//5u16;
|
||||
let scenes_w = 3 + Scene::longest_name(scenes) as u16; // x of 1st track
|
||||
let clock = &self.0.clock;
|
||||
let arrangement = Layers::new(move |add|{
|
||||
let rows: &[(usize, usize)] = rows.as_ref();
|
||||
let cols: &[(usize, usize)] = cols.as_ref();
|
||||
let any_size = |_|Ok(Some([0,0]));
|
||||
// column separators
|
||||
add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{
|
||||
let style = Some(Style::default().fg(sep_fg));
|
||||
Ok(for x in cols.iter().map(|col|col.1) {
|
||||
let x = scenes_w + to.area().x() + x as u16;
|
||||
for y in to.area().y()..to.area().y2() { to.blit(&"▎", x, y, style); }
|
||||
})
|
||||
}))?;
|
||||
// row separators
|
||||
add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{
|
||||
Ok(for y in rows.iter().map(|row|row.1) {
|
||||
let y = to.area().y() + (y / PPQ) as u16 + 1;
|
||||
if y >= to.buffer.area.height { break }
|
||||
for x in to.area().x()..to.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 = sep_fg;
|
||||
}
|
||||
}
|
||||
})
|
||||
}))?;
|
||||
// track titles
|
||||
let header = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{
|
||||
// name and width of track
|
||||
let name = track.name.read().unwrap();
|
||||
let player = &track.player;
|
||||
let max_w = w.saturating_sub(1).min(name.len()).max(2);
|
||||
let name = format!("▎{}", &name[0..max_w]);
|
||||
let name = TuiStyle::bold(name, true);
|
||||
// beats elapsed
|
||||
let elapsed = if let Some((_, Some(phrase))) = player.phrase.as_ref() {
|
||||
let length = phrase.read().unwrap().length;
|
||||
let elapsed = player.pulses_since_start().unwrap();
|
||||
let elapsed = clock.timebase().format_beats_1_short(
|
||||
(elapsed as usize % length) as f64
|
||||
);
|
||||
format!("▎+{elapsed:>}")
|
||||
} else {
|
||||
String::from("▎")
|
||||
};
|
||||
// beats until switchover
|
||||
let until_next = player.next_phrase.as_ref().map(|(t, _)|{
|
||||
let target = t.pulse.get();
|
||||
let current = clock.current.pulse.get();
|
||||
if target > current {
|
||||
let remaining = target - current;
|
||||
format!("▎-{:>}", clock.timebase().format_beats_0_short(remaining))
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}).unwrap_or(String::from("▎"));
|
||||
// name of active MIDI input
|
||||
let input = format!("▎>{}", track.player.midi_inputs.get(0)
|
||||
.map(|port|port.short_name())
|
||||
.transpose()?
|
||||
.unwrap_or("(none)".into()));
|
||||
// name of active MIDI output
|
||||
let output = format!("▎<{}", track.player.midi_outputs.get(0)
|
||||
.map(|port|port.short_name())
|
||||
.transpose()?
|
||||
.unwrap_or("(none)".into()));
|
||||
col!(name, /*input, output,*/ until_next, elapsed)
|
||||
.min_xy(w as u16, header_h)
|
||||
.bg(track.color.rgb)
|
||||
.push_x(scenes_w)
|
||||
});
|
||||
// scene titles
|
||||
let scene_name = |scene, playing: bool, height|row!(
|
||||
if playing { "▶ " } else { " " },
|
||||
TuiStyle::bold((scene as &Scene).name.read().unwrap().as_str(), true),
|
||||
).fixed_xy(scenes_w, height);
|
||||
// scene clips
|
||||
let scene_clip = |scene, track: usize, w: u16, h: u16|Layers::new(move |add|{
|
||||
let mut bg = clip_bg;
|
||||
match (tracks.get(track), (scene as &Scene).clips.get(track)) {
|
||||
(Some(track), 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));
|
||||
let color = phrase.read().unwrap().color;
|
||||
add(&name.as_str()[0..max_w].push_x(1).fixed_x(w))?;
|
||||
bg = color.dark.rgb;
|
||||
if let Some((_, Some(ref playing))) = track.player.phrase {
|
||||
if *playing.read().unwrap() == *phrase.read().unwrap() {
|
||||
bg = color.light.rgb
|
||||
}
|
||||
};
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
add(&Background(bg))
|
||||
}).fixed_xy(w, h);
|
||||
// tracks and scenes
|
||||
let content = 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.rgb))?;
|
||||
// clip per track:
|
||||
Ok(for (track, w) in cols.iter().map(|col|col.0).enumerate() {
|
||||
add(&scene_clip(scene, track, w as u16, height))?;
|
||||
})
|
||||
}).fixed_y(height)
|
||||
}
|
||||
).fixed_y((self.0.size.h() as u16).saturating_sub(header_h));
|
||||
// full grid with header and footer
|
||||
add(&col!(header, content))?;
|
||||
// cursor
|
||||
add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{
|
||||
let area = to.area();
|
||||
let focused = state.focused;
|
||||
let selected = state.selected;
|
||||
let get_track_area = |t: usize| [
|
||||
scenes_w + area.x() + cols[t].1 as u16, area.y(),
|
||||
cols[t].0 as u16, area.h(),
|
||||
];
|
||||
let get_scene_area = |s: usize| [
|
||||
area.x(), header_h + area.y() + (rows[s].1 / PPQ) as u16,
|
||||
area.w(), (rows[s].0 / PPQ) as u16
|
||||
];
|
||||
let get_clip_area = |t: usize, s: usize| [
|
||||
scenes_w + area.x() + cols[t].1 as u16,
|
||||
header_h + 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 => 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
|
||||
},
|
||||
};
|
||||
let bg = Arranger::<Tui>::border_bg();
|
||||
if let Some([x, y, width, height]) = track_area {
|
||||
to.fill_fg([x, y, 1, height], bg);
|
||||
to.fill_fg([x + width, y, 1, height], bg);
|
||||
}
|
||||
if let Some([_, y, _, height]) = scene_area {
|
||||
to.fill_ul([area.x(), y - 1, area.w(), 1], bg);
|
||||
to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg);
|
||||
}
|
||||
Ok(if focused {
|
||||
to.render_in(if let Some(clip_area) = clip_area { clip_area }
|
||||
else if let Some(track_area) = track_area { track_area.clip_h(header_h) }
|
||||
else if let Some(scene_area) = scene_area { scene_area.clip_w(scenes_w) }
|
||||
else { area.clip_w(scenes_w).clip_h(header_h) }, &CORNERS)?
|
||||
})
|
||||
}))
|
||||
}).bg(bg.rgb);
|
||||
let color = Arranger::<Tui>::title_fg(self.0.focused);
|
||||
let size = format!("{}x{}", self.0.size.w(), self.0.size.h());
|
||||
let lower_right = TuiStyle::fg(size, color).pull_x(1).align_se().fill_xy();
|
||||
lay!(arrangement, lower_right)
|
||||
}
|
||||
}
|
||||
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(Arranger::<Tui>::border_bg())),
|
||||
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;
|
||||
Ok(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;
|
||||
})
|
||||
}),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
/// Handle top-level events in standalone arranger.
|
||||
impl Handle<Tui> for Arranger<Tui> {
|
||||
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
||||
if let Some(entered) = self.entered() {
|
||||
use ArrangerFocus::*;
|
||||
if let Some(true) = match entered {
|
||||
Transport => self.transport.as_mut().map(|t|t.handle(i)).transpose()?.flatten(),
|
||||
Arrangement => self.arrangement.handle(i)?,
|
||||
PhrasePool => self.phrases.write().unwrap().handle(i)?,
|
||||
PhraseEditor => self.editor.handle(i)?,
|
||||
} {
|
||||
return Ok(Some(true))
|
||||
}
|
||||
}
|
||||
Ok(if let Some(command) = ArrangerCommand::input_to_command(self, i) {
|
||||
let _undo = command.execute(self)?;
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
/// Handle events for arrangement.
|
||||
impl Handle<Tui> for Arrangement<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
Ok(if let Some(command) = ArrangementCommand::input_to_command(self, from) {
|
||||
let _undo = command.execute(self)?;
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
impl InputToCommand<Tui, Arranger<Tui>> for ArrangerCommand {
|
||||
fn input_to_command (state: &Arranger<Tui>, input: &TuiInput) -> Option<Self> {
|
||||
use FocusCommand::*;
|
||||
use ArrangerCommand::*;
|
||||
match input.event() {
|
||||
key!(KeyCode::Tab) => Some(Focus(Next)),
|
||||
key!(Shift-KeyCode::Tab) => Some(Focus(Prev)),
|
||||
key!(KeyCode::BackTab) => Some(Focus(Prev)),
|
||||
key!(Shift-KeyCode::BackTab) => Some(Focus(Prev)),
|
||||
key!(KeyCode::Up) => Some(Focus(Up)),
|
||||
key!(KeyCode::Down) => Some(Focus(Down)),
|
||||
key!(KeyCode::Left) => Some(Focus(Left)),
|
||||
key!(KeyCode::Right) => Some(Focus(Right)),
|
||||
key!(KeyCode::Enter) => Some(Focus(Enter)),
|
||||
key!(KeyCode::Esc) => Some(Focus(Exit)),
|
||||
key!(KeyCode::Char(' ')) => Some(Transport(TransportCommand::PlayToggle)),
|
||||
_ => match state.focused() {
|
||||
ArrangerFocus::Transport => state.transport.as_ref()
|
||||
.map(|t|TransportCommand::input_to_command(&*t.read().unwrap(), input)
|
||||
.map(Transport))
|
||||
.flatten(),
|
||||
ArrangerFocus::PhrasePool =>
|
||||
PhrasePoolCommand::input_to_command(&*state.phrases.read().unwrap(), input)
|
||||
.map(Phrases),
|
||||
ArrangerFocus::PhraseEditor =>
|
||||
PhraseEditorCommand::input_to_command(&state.editor, input)
|
||||
.map(Editor),
|
||||
ArrangerFocus::Arrangement => match input.event() {
|
||||
key!(KeyCode::Char('e')) => Some(EditPhrase),
|
||||
_ => ArrangementCommand::input_to_command(&state.arrangement, &input)
|
||||
.map(Arrangement)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl InputToCommand<Tui, Arrangement<Tui>> for ArrangementCommand {
|
||||
fn input_to_command (_: &Arrangement<Tui>, input: &TuiInput) -> Option<Self> {
|
||||
use ArrangementCommand::*;
|
||||
match input.event() {
|
||||
key!(KeyCode::Char('`')) => Some(ToggleViewMode),
|
||||
key!(KeyCode::Delete) => Some(Delete),
|
||||
key!(KeyCode::Enter) => Some(Activate),
|
||||
key!(KeyCode::Char('.')) => Some(Increment),
|
||||
key!(KeyCode::Char(',')) => Some(Decrement),
|
||||
key!(KeyCode::Char('+')) => Some(ZoomIn),
|
||||
key!(KeyCode::Char('=')) => Some(ZoomOut),
|
||||
key!(KeyCode::Char('_')) => Some(ZoomOut),
|
||||
key!(KeyCode::Char('-')) => Some(ZoomOut),
|
||||
key!(KeyCode::Char('<')) => Some(MoveBack),
|
||||
key!(KeyCode::Char('>')) => Some(MoveForward),
|
||||
key!(KeyCode::Char('c')) => Some(RandomColor),
|
||||
key!(KeyCode::Char('s')) => Some(Put),
|
||||
key!(KeyCode::Char('g')) => Some(Get),
|
||||
key!(KeyCode::Char('e')) => Some(Edit),
|
||||
key!(Ctrl-KeyCode::Char('a')) => Some(AddScene),
|
||||
key!(Ctrl-KeyCode::Char('t')) => Some(AddTrack),
|
||||
key!(KeyCode::Char('l')) => Some(ToggleLoop),
|
||||
key!(KeyCode::Up) => Some(GoUp),
|
||||
key!(KeyCode::Down) => Some(GoDown),
|
||||
key!(KeyCode::Left) => Some(GoLeft),
|
||||
key!(KeyCode::Right) => Some(GoRight),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
//impl Arranger<Tui> {
|
||||
///// Helper for event passthru to focused component
|
||||
//fn handle_focused (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
//match self.focused() {
|
||||
//ArrangerFocus::Transport => self.transport.handle(from),
|
||||
//ArrangerFocus::PhrasePool => self.handle_pool(from),
|
||||
//ArrangerFocus::PhraseEditor => self.editor.handle(from),
|
||||
//ArrangerFocus::Arrangement => self.handle_arrangement(from)
|
||||
//.and_then(|result|{self.show_phrase();Ok(result)}),
|
||||
//}
|
||||
//}
|
||||
///// Helper for phrase event passthru when phrase pool is focused
|
||||
//fn handle_pool (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
//match from.event() {
|
||||
//key!(KeyCode::Char('<')) => {
|
||||
//self.phrases_split = self.phrases_split.saturating_sub(1).max(12);
|
||||
//},
|
||||
//key!(KeyCode::Char('>')) => {
|
||||
//self.phrases_split = self.phrases_split + 1;
|
||||
//},
|
||||
//_ => return self.phrases.handle(from)
|
||||
//}
|
||||
//Ok(Some(true))
|
||||
//}
|
||||
///// Helper for phrase event passthru when arrangement is focused
|
||||
//fn handle_arrangement (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
//let mut handle_phrase = ||{
|
||||
//let result = self.phrases.handle(from);
|
||||
//self.arrangement.phrase_put();
|
||||
//result
|
||||
//};
|
||||
//match from.event() {
|
||||
//key!(KeyCode::Char('a')) => return handle_phrase(),
|
||||
//key!(KeyCode::Char('i')) => return handle_phrase(),
|
||||
//key!(KeyCode::Char('d')) => return handle_phrase(),
|
||||
//key!(KeyCode::Char('<')) => if self.arrangement.selected == ArrangementFocus::Mix {
|
||||
//self.arrangement_split = self.arrangement_split.saturating_sub(1).max(12);
|
||||
//} else {
|
||||
//return self.arrangement.handle(from)
|
||||
//},
|
||||
//key!(KeyCode::Char('>')) => if self.arrangement.selected == ArrangementFocus::Mix {
|
||||
//self.arrangement_split = self.arrangement_split + 1;
|
||||
//} else {
|
||||
//return self.arrangement.handle(from)
|
||||
//},
|
||||
//_ => return self.arrangement.handle(from)
|
||||
//}
|
||||
//self.show_phrase();
|
||||
//Ok(Some(true))
|
||||
//}
|
||||
//}
|
||||
|
||||
trait ArrangerTheme<E: Engine> {
|
||||
fn border_bg () -> Color;
|
||||
fn border_fg (focused: bool) -> Color;
|
||||
fn title_fg (focused: bool) -> Color;
|
||||
fn separator_fg (focused: bool) -> Color;
|
||||
fn hotkey_fg () -> Color;
|
||||
fn mode_bg () -> Color;
|
||||
fn mode_fg () -> Color;
|
||||
fn status_bar_bg () -> Color;
|
||||
}
|
||||
|
||||
impl ArrangerTheme<Tui> for Arranger<Tui> {
|
||||
fn border_bg () -> Color {
|
||||
Color::Rgb(40, 50, 30)
|
||||
}
|
||||
fn border_fg (focused: bool) -> Color {
|
||||
if focused { Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) }
|
||||
}
|
||||
fn title_fg (focused: bool) -> Color {
|
||||
if focused { Color::Rgb(150, 160, 90) } else { Color::Rgb(120, 130, 100) }
|
||||
}
|
||||
fn separator_fg (_: bool) -> Color {
|
||||
Color::Rgb(0, 0, 0)
|
||||
}
|
||||
fn hotkey_fg () -> Color {
|
||||
Color::Rgb(255, 255, 0)
|
||||
}
|
||||
fn mode_bg () -> Color {
|
||||
Color::Rgb(150, 160, 90)
|
||||
}
|
||||
fn mode_fg () -> Color {
|
||||
Color::Rgb(255, 255, 255)
|
||||
}
|
||||
fn status_bar_bg () -> Color {
|
||||
Color::Rgb(28, 35, 25)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
97
crates/tek_sequencer/src/arranger_tui_bar.rs
Normal file
97
crates/tek_sequencer/src/arranger_tui_bar.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
use crate::*;
|
||||
|
||||
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 status_bar_bg = Arranger::<Tui>::status_bar_bg();
|
||||
let mode_bg = Arranger::<Tui>::mode_bg();
|
||||
let mode_fg = Arranger::<Tui>::mode_fg();
|
||||
let mode = TuiStyle::bold(format!(" {label} "), true).bg(mode_bg).fg(mode_fg);
|
||||
let commands = match self {
|
||||
Self::ArrangementMix => command(&[
|
||||
["", "c", "olor"],
|
||||
["", "<>", "resize"],
|
||||
["", "+-", "zoom"],
|
||||
["", "n", "ame/number"],
|
||||
["", "Enter", " stop all"],
|
||||
]),
|
||||
Self::ArrangementClip => command(&[
|
||||
["", "g", "et"],
|
||||
["", "s", "et"],
|
||||
["", "a", "dd"],
|
||||
["", "i", "ns"],
|
||||
["", "d", "up"],
|
||||
["", "e", "dit"],
|
||||
["", "c", "olor"],
|
||||
["re", "n", "ame"],
|
||||
["", ",.", "select"],
|
||||
["", "Enter", " launch"],
|
||||
]),
|
||||
Self::ArrangementTrack => command(&[
|
||||
["re", "n", "ame"],
|
||||
["", ",.", "resize"],
|
||||
["", "<>", "move"],
|
||||
["", "i", "nput"],
|
||||
["", "o", "utput"],
|
||||
["", "m", "ute"],
|
||||
["", "s", "olo"],
|
||||
["", "Del", "ete"],
|
||||
["", "Enter", " stop"],
|
||||
]),
|
||||
Self::ArrangementScene => command(&[
|
||||
["re", "n", "ame"],
|
||||
["", "Del", "ete"],
|
||||
["", "Enter", " launch"],
|
||||
]),
|
||||
Self::PhrasePool => command(&[
|
||||
["", "a", "ppend"],
|
||||
["", "i", "nsert"],
|
||||
["", "d", "uplicate"],
|
||||
["", "Del", "ete"],
|
||||
["", "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).fill_x().bg(status_bar_bg)
|
||||
}
|
||||
}
|
||||
|
||||
fn command (commands: &[[impl Widget<Engine = Tui>;3]]) -> impl Widget<Engine = Tui> + '_ {
|
||||
Stack::right(|add|{
|
||||
Ok(for [a, b, c] in commands.iter() {
|
||||
add(&row!(
|
||||
" ",
|
||||
widget(a),
|
||||
widget(b).bold(true).fg(Arranger::<Tui>::hotkey_fg()),
|
||||
widget(c),
|
||||
))?;
|
||||
})
|
||||
})
|
||||
}
|
||||
154
crates/tek_sequencer/src/arranger_tui_cmd.rs
Normal file
154
crates/tek_sequencer/src/arranger_tui_cmd.rs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
use crate::*;
|
||||
|
||||
/// Handle top-level events in standalone arranger.
|
||||
impl Handle<Tui> for Arranger<Tui> {
|
||||
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
||||
if let Some(entered) = self.entered() {
|
||||
use ArrangerFocus::*;
|
||||
if let Some(true) = match entered {
|
||||
Transport => self.transport.as_mut().map(|t|t.handle(i)).transpose()?.flatten(),
|
||||
Arrangement => self.arrangement.handle(i)?,
|
||||
PhrasePool => self.phrases.write().unwrap().handle(i)?,
|
||||
PhraseEditor => self.editor.handle(i)?,
|
||||
} {
|
||||
return Ok(Some(true))
|
||||
}
|
||||
}
|
||||
Ok(if let Some(command) = ArrangerCommand::input_to_command(self, i) {
|
||||
let _undo = command.execute(self)?;
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle events for arrangement.
|
||||
impl Handle<Tui> for Arrangement<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
Ok(if let Some(command) = ArrangementCommand::input_to_command(self, from) {
|
||||
let _undo = command.execute(self)?;
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, Arranger<Tui>> for ArrangerCommand {
|
||||
fn input_to_command (state: &Arranger<Tui>, input: &TuiInput) -> Option<Self> {
|
||||
use FocusCommand::*;
|
||||
use ArrangerCommand::*;
|
||||
match input.event() {
|
||||
key!(KeyCode::Tab) => Some(Focus(Next)),
|
||||
key!(Shift-KeyCode::Tab) => Some(Focus(Prev)),
|
||||
key!(KeyCode::BackTab) => Some(Focus(Prev)),
|
||||
key!(Shift-KeyCode::BackTab) => Some(Focus(Prev)),
|
||||
key!(KeyCode::Up) => Some(Focus(Up)),
|
||||
key!(KeyCode::Down) => Some(Focus(Down)),
|
||||
key!(KeyCode::Left) => Some(Focus(Left)),
|
||||
key!(KeyCode::Right) => Some(Focus(Right)),
|
||||
key!(KeyCode::Enter) => Some(Focus(Enter)),
|
||||
key!(KeyCode::Esc) => Some(Focus(Exit)),
|
||||
key!(KeyCode::Char(' ')) => Some(Transport(TransportCommand::PlayToggle)),
|
||||
_ => match state.focused() {
|
||||
ArrangerFocus::Transport => state.transport.as_ref()
|
||||
.map(|t|TransportCommand::input_to_command(&*t.read().unwrap(), input)
|
||||
.map(Transport))
|
||||
.flatten(),
|
||||
ArrangerFocus::PhrasePool =>
|
||||
PhrasePoolCommand::input_to_command(&*state.phrases.read().unwrap(), input)
|
||||
.map(Phrases),
|
||||
ArrangerFocus::PhraseEditor =>
|
||||
PhraseEditorCommand::input_to_command(&state.editor, input)
|
||||
.map(Editor),
|
||||
ArrangerFocus::Arrangement => match input.event() {
|
||||
key!(KeyCode::Char('e')) => Some(EditPhrase),
|
||||
_ => ArrangementCommand::input_to_command(&state.arrangement, &input)
|
||||
.map(Arrangement)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl InputToCommand<Tui, Arrangement<Tui>> for ArrangementCommand {
|
||||
fn input_to_command (_: &Arrangement<Tui>, input: &TuiInput) -> Option<Self> {
|
||||
use ArrangementCommand::*;
|
||||
match input.event() {
|
||||
key!(KeyCode::Char('`')) => Some(ToggleViewMode),
|
||||
key!(KeyCode::Delete) => Some(Delete),
|
||||
key!(KeyCode::Enter) => Some(Activate),
|
||||
key!(KeyCode::Char('.')) => Some(Increment),
|
||||
key!(KeyCode::Char(',')) => Some(Decrement),
|
||||
key!(KeyCode::Char('+')) => Some(ZoomIn),
|
||||
key!(KeyCode::Char('=')) => Some(ZoomOut),
|
||||
key!(KeyCode::Char('_')) => Some(ZoomOut),
|
||||
key!(KeyCode::Char('-')) => Some(ZoomOut),
|
||||
key!(KeyCode::Char('<')) => Some(MoveBack),
|
||||
key!(KeyCode::Char('>')) => Some(MoveForward),
|
||||
key!(KeyCode::Char('c')) => Some(RandomColor),
|
||||
key!(KeyCode::Char('s')) => Some(Put),
|
||||
key!(KeyCode::Char('g')) => Some(Get),
|
||||
key!(KeyCode::Char('e')) => Some(Edit),
|
||||
key!(Ctrl-KeyCode::Char('a')) => Some(AddScene),
|
||||
key!(Ctrl-KeyCode::Char('t')) => Some(AddTrack),
|
||||
key!(KeyCode::Char('l')) => Some(ToggleLoop),
|
||||
key!(KeyCode::Up) => Some(GoUp),
|
||||
key!(KeyCode::Down) => Some(GoDown),
|
||||
key!(KeyCode::Left) => Some(GoLeft),
|
||||
key!(KeyCode::Right) => Some(GoRight),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
//impl Arranger<Tui> {
|
||||
///// Helper for event passthru to focused component
|
||||
//fn handle_focused (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
//match self.focused() {
|
||||
//ArrangerFocus::Transport => self.transport.handle(from),
|
||||
//ArrangerFocus::PhrasePool => self.handle_pool(from),
|
||||
//ArrangerFocus::PhraseEditor => self.editor.handle(from),
|
||||
//ArrangerFocus::Arrangement => self.handle_arrangement(from)
|
||||
//.and_then(|result|{self.show_phrase();Ok(result)}),
|
||||
//}
|
||||
//}
|
||||
///// Helper for phrase event passthru when phrase pool is focused
|
||||
//fn handle_pool (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
//match from.event() {
|
||||
//key!(KeyCode::Char('<')) => {
|
||||
//self.phrases_split = self.phrases_split.saturating_sub(1).max(12);
|
||||
//},
|
||||
//key!(KeyCode::Char('>')) => {
|
||||
//self.phrases_split = self.phrases_split + 1;
|
||||
//},
|
||||
//_ => return self.phrases.handle(from)
|
||||
//}
|
||||
//Ok(Some(true))
|
||||
//}
|
||||
///// Helper for phrase event passthru when arrangement is focused
|
||||
//fn handle_arrangement (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
//let mut handle_phrase = ||{
|
||||
//let result = self.phrases.handle(from);
|
||||
//self.arrangement.phrase_put();
|
||||
//result
|
||||
//};
|
||||
//match from.event() {
|
||||
//key!(KeyCode::Char('a')) => return handle_phrase(),
|
||||
//key!(KeyCode::Char('i')) => return handle_phrase(),
|
||||
//key!(KeyCode::Char('d')) => return handle_phrase(),
|
||||
//key!(KeyCode::Char('<')) => if self.arrangement.selected == ArrangementFocus::Mix {
|
||||
//self.arrangement_split = self.arrangement_split.saturating_sub(1).max(12);
|
||||
//} else {
|
||||
//return self.arrangement.handle(from)
|
||||
//},
|
||||
//key!(KeyCode::Char('>')) => if self.arrangement.selected == ArrangementFocus::Mix {
|
||||
//self.arrangement_split = self.arrangement_split + 1;
|
||||
//} else {
|
||||
//return self.arrangement.handle(from)
|
||||
//},
|
||||
//_ => return self.arrangement.handle(from)
|
||||
//}
|
||||
//self.show_phrase();
|
||||
//Ok(Some(true))
|
||||
//}
|
||||
//}
|
||||
39
crates/tek_sequencer/src/arranger_tui_col.rs
Normal file
39
crates/tek_sequencer/src/arranger_tui_col.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait ArrangerTheme<E: Engine> {
|
||||
fn border_bg () -> Color;
|
||||
fn border_fg (focused: bool) -> Color;
|
||||
fn title_fg (focused: bool) -> Color;
|
||||
fn separator_fg (focused: bool) -> Color;
|
||||
fn hotkey_fg () -> Color;
|
||||
fn mode_bg () -> Color;
|
||||
fn mode_fg () -> Color;
|
||||
fn status_bar_bg () -> Color;
|
||||
}
|
||||
|
||||
impl ArrangerTheme<Tui> for Arranger<Tui> {
|
||||
fn border_bg () -> Color {
|
||||
Color::Rgb(40, 50, 30)
|
||||
}
|
||||
fn border_fg (focused: bool) -> Color {
|
||||
if focused { Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) }
|
||||
}
|
||||
fn title_fg (focused: bool) -> Color {
|
||||
if focused { Color::Rgb(150, 160, 90) } else { Color::Rgb(120, 130, 100) }
|
||||
}
|
||||
fn separator_fg (_: bool) -> Color {
|
||||
Color::Rgb(0, 0, 0)
|
||||
}
|
||||
fn hotkey_fg () -> Color {
|
||||
Color::Rgb(255, 255, 0)
|
||||
}
|
||||
fn mode_bg () -> Color {
|
||||
Color::Rgb(150, 160, 90)
|
||||
}
|
||||
fn mode_fg () -> Color {
|
||||
Color::Rgb(255, 255, 255)
|
||||
}
|
||||
fn status_bar_bg () -> Color {
|
||||
Color::Rgb(28, 35, 25)
|
||||
}
|
||||
}
|
||||
197
crates/tek_sequencer/src/arranger_tui_hor.rs
Normal file
197
crates/tek_sequencer/src/arranger_tui_hor.rs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
use crate::*;
|
||||
|
||||
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(Arranger::<Tui>::border_bg())),
|
||||
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;
|
||||
Ok(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;
|
||||
})
|
||||
}),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
188
crates/tek_sequencer/src/arranger_tui_ver.rs
Normal file
188
crates/tek_sequencer/src/arranger_tui_ver.rs
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
use crate::*;
|
||||
|
||||
impl<'a> Content for VerticalArranger<'a, Tui> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let Self(state, factor) = self;
|
||||
let tracks = state.tracks.as_ref() as &[ArrangementTrack];
|
||||
let scenes = state.scenes.as_ref();
|
||||
let cols = state.track_widths();
|
||||
let rows = Scene::ppqs(scenes, *factor);
|
||||
let bg = state.color;
|
||||
let clip_bg = Arranger::<Tui>::border_bg();
|
||||
let sep_fg = Arranger::<Tui>::separator_fg(false);
|
||||
let header_h = 3u16;//5u16;
|
||||
let scenes_w = 3 + Scene::longest_name(scenes) as u16; // x of 1st track
|
||||
let clock = &self.0.clock;
|
||||
let arrangement = Layers::new(move |add|{
|
||||
let rows: &[(usize, usize)] = rows.as_ref();
|
||||
let cols: &[(usize, usize)] = cols.as_ref();
|
||||
let any_size = |_|Ok(Some([0,0]));
|
||||
// column separators
|
||||
add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{
|
||||
let style = Some(Style::default().fg(sep_fg));
|
||||
Ok(for x in cols.iter().map(|col|col.1) {
|
||||
let x = scenes_w + to.area().x() + x as u16;
|
||||
for y in to.area().y()..to.area().y2() { to.blit(&"▎", x, y, style); }
|
||||
})
|
||||
}))?;
|
||||
// row separators
|
||||
add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{
|
||||
Ok(for y in rows.iter().map(|row|row.1) {
|
||||
let y = to.area().y() + (y / PPQ) as u16 + 1;
|
||||
if y >= to.buffer.area.height { break }
|
||||
for x in to.area().x()..to.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 = sep_fg;
|
||||
}
|
||||
}
|
||||
})
|
||||
}))?;
|
||||
// track titles
|
||||
let header = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{
|
||||
// name and width of track
|
||||
let name = track.name.read().unwrap();
|
||||
let player = &track.player;
|
||||
let max_w = w.saturating_sub(1).min(name.len()).max(2);
|
||||
let name = format!("▎{}", &name[0..max_w]);
|
||||
let name = TuiStyle::bold(name, true);
|
||||
// beats elapsed
|
||||
let elapsed = if let Some((_, Some(phrase))) = player.phrase.as_ref() {
|
||||
let length = phrase.read().unwrap().length;
|
||||
let elapsed = player.pulses_since_start().unwrap();
|
||||
let elapsed = clock.timebase().format_beats_1_short(
|
||||
(elapsed as usize % length) as f64
|
||||
);
|
||||
format!("▎+{elapsed:>}")
|
||||
} else {
|
||||
String::from("▎")
|
||||
};
|
||||
// beats until switchover
|
||||
let until_next = player.next_phrase.as_ref().map(|(t, _)|{
|
||||
let target = t.pulse.get();
|
||||
let current = clock.current.pulse.get();
|
||||
if target > current {
|
||||
let remaining = target - current;
|
||||
format!("▎-{:>}", clock.timebase().format_beats_0_short(remaining))
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}).unwrap_or(String::from("▎"));
|
||||
// name of active MIDI input
|
||||
let input = format!("▎>{}", track.player.midi_inputs.get(0)
|
||||
.map(|port|port.short_name())
|
||||
.transpose()?
|
||||
.unwrap_or("(none)".into()));
|
||||
// name of active MIDI output
|
||||
let output = format!("▎<{}", track.player.midi_outputs.get(0)
|
||||
.map(|port|port.short_name())
|
||||
.transpose()?
|
||||
.unwrap_or("(none)".into()));
|
||||
col!(name, /*input, output,*/ until_next, elapsed)
|
||||
.min_xy(w as u16, header_h)
|
||||
.bg(track.color.rgb)
|
||||
.push_x(scenes_w)
|
||||
});
|
||||
// scene titles
|
||||
let scene_name = |scene, playing: bool, height|row!(
|
||||
if playing { "▶ " } else { " " },
|
||||
TuiStyle::bold((scene as &Scene).name.read().unwrap().as_str(), true),
|
||||
).fixed_xy(scenes_w, height);
|
||||
// scene clips
|
||||
let scene_clip = |scene, track: usize, w: u16, h: u16|Layers::new(move |add|{
|
||||
let mut bg = clip_bg;
|
||||
match (tracks.get(track), (scene as &Scene).clips.get(track)) {
|
||||
(Some(track), 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));
|
||||
let color = phrase.read().unwrap().color;
|
||||
add(&name.as_str()[0..max_w].push_x(1).fixed_x(w))?;
|
||||
bg = color.dark.rgb;
|
||||
if let Some((_, Some(ref playing))) = track.player.phrase {
|
||||
if *playing.read().unwrap() == *phrase.read().unwrap() {
|
||||
bg = color.light.rgb
|
||||
}
|
||||
};
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
add(&Background(bg))
|
||||
}).fixed_xy(w, h);
|
||||
// tracks and scenes
|
||||
let content = 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.rgb))?;
|
||||
// clip per track:
|
||||
Ok(for (track, w) in cols.iter().map(|col|col.0).enumerate() {
|
||||
add(&scene_clip(scene, track, w as u16, height))?;
|
||||
})
|
||||
}).fixed_y(height)
|
||||
}
|
||||
).fixed_y((self.0.size.h() as u16).saturating_sub(header_h));
|
||||
// full grid with header and footer
|
||||
add(&col!(header, content))?;
|
||||
// cursor
|
||||
add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{
|
||||
let area = to.area();
|
||||
let focused = state.focused;
|
||||
let selected = state.selected;
|
||||
let get_track_area = |t: usize| [
|
||||
scenes_w + area.x() + cols[t].1 as u16, area.y(),
|
||||
cols[t].0 as u16, area.h(),
|
||||
];
|
||||
let get_scene_area = |s: usize| [
|
||||
area.x(), header_h + area.y() + (rows[s].1 / PPQ) as u16,
|
||||
area.w(), (rows[s].0 / PPQ) as u16
|
||||
];
|
||||
let get_clip_area = |t: usize, s: usize| [
|
||||
scenes_w + area.x() + cols[t].1 as u16,
|
||||
header_h + 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 => 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
|
||||
},
|
||||
};
|
||||
let bg = Arranger::<Tui>::border_bg();
|
||||
if let Some([x, y, width, height]) = track_area {
|
||||
to.fill_fg([x, y, 1, height], bg);
|
||||
to.fill_fg([x + width, y, 1, height], bg);
|
||||
}
|
||||
if let Some([_, y, _, height]) = scene_area {
|
||||
to.fill_ul([area.x(), y - 1, area.w(), 1], bg);
|
||||
to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg);
|
||||
}
|
||||
Ok(if focused {
|
||||
to.render_in(if let Some(clip_area) = clip_area { clip_area }
|
||||
else if let Some(track_area) = track_area { track_area.clip_h(header_h) }
|
||||
else if let Some(scene_area) = scene_area { scene_area.clip_w(scenes_w) }
|
||||
else { area.clip_w(scenes_w).clip_h(header_h) }, &CORNERS)?
|
||||
})
|
||||
}))
|
||||
}).bg(bg.rgb);
|
||||
let color = Arranger::<Tui>::title_fg(self.0.focused);
|
||||
let size = format!("{}x{}", self.0.size.w(), self.0.size.h());
|
||||
let lower_right = TuiStyle::fg(size, color).pull_x(1).align_se().fill_xy();
|
||||
lay!(arrangement, lower_right)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -7,9 +7,23 @@ pub(crate) use tek_core::jack::*;
|
|||
pub(crate) use std::sync::{Arc, RwLock};
|
||||
|
||||
submod! {
|
||||
arranger arranger_cmd arranger_tui arranger_snd
|
||||
sequencer sequencer_cmd sequencer_tui sequencer_snd
|
||||
transport transport_cmd transport_tui transport_snd
|
||||
arranger
|
||||
arranger_cmd
|
||||
arranger_snd
|
||||
arranger_tui
|
||||
arranger_tui_bar
|
||||
arranger_tui_cmd
|
||||
arranger_tui_col
|
||||
arranger_tui_hor
|
||||
arranger_tui_ver
|
||||
sequencer
|
||||
sequencer_cmd
|
||||
sequencer_snd
|
||||
sequencer_tui
|
||||
transport
|
||||
transport_cmd
|
||||
transport_snd
|
||||
transport_tui
|
||||
}
|
||||
|
||||
/// Octave number (from -1 to 9)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue