mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 04:06:45 +01:00
191 lines
9.5 KiB
Rust
191 lines
9.5 KiB
Rust
use crate::*;
|
|
|
|
/// Arrangement, rendered vertically (session/grid mode).
|
|
pub struct VerticalArranger<'a, E: Engine>(pub &'a Arrangement<E>, pub usize);
|
|
|
|
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: &ArrangementScene, playing: bool, height|row!(
|
|
if playing { "▶ " } else { " " },
|
|
TuiStyle::bold(scene.name.read().unwrap().as_str(), true),
|
|
).fixed_xy(scenes_w, height);
|
|
// scene clips
|
|
let scene_clip = |scene: &ArrangementScene, track: usize, w: u16, h: u16|Layers::new(move |add|{
|
|
let mut bg = clip_bg;
|
|
match (tracks.get(track), 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)
|
|
}
|
|
}
|
|
|