mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-08 04:36:45 +01:00
548 lines
24 KiB
Rust
548 lines
24 KiB
Rust
use crate::*;
|
|
/// The standalone arranger consists of transport, clip grid, and sequencer.
|
|
impl Content for Arranger<Tui> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
Layers::new(move|add|{
|
|
add(&Stack::down(move|add|{
|
|
add(&self.transport)?;
|
|
let arrangement = &self.arrangement as &dyn Widget<Engine = Tui>;
|
|
if let Some(direction) = self.show_sequencer {
|
|
let editor = &self.editor as &dyn Widget<Engine = Tui>;
|
|
add(&arrangement.split(direction, 20, self.phrases.clone()
|
|
.split(direction.ccw(), 20, editor)
|
|
.min_y(20)).fill_y())
|
|
} else {
|
|
add(&self.arrangement)
|
|
}
|
|
}))?;
|
|
//if let Some(ref modal) = self.modal {
|
|
//add(&Background(COLOR_BG1))?;
|
|
//add(&Foreground(COLOR_BG2))?;
|
|
////add(modal as &dyn Widget<Engine = Tui>)?;
|
|
//}
|
|
Ok(())
|
|
})
|
|
}
|
|
}
|
|
/// Handle top-level events in standalone arranger.
|
|
impl Handle<Tui> for Arranger<Tui> {
|
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
if !match self.focused() {
|
|
ArrangerFocus::Transport => self.transport.handle(from)?,
|
|
ArrangerFocus::PhrasePool => self.phrases.handle(from)?,
|
|
ArrangerFocus::PhraseEditor => self.editor.handle(from)?,
|
|
ArrangerFocus::Arrangement => {
|
|
let mut handle_phrase = ||{
|
|
let result = self.phrases.handle(from);
|
|
self.arrangement.phrase_put();
|
|
result
|
|
};
|
|
match from.event() {
|
|
key!(KeyCode::Char('a')) => handle_phrase()?,
|
|
key!(KeyCode::Char('c')) => handle_phrase()?,
|
|
key!(KeyCode::Char('i')) => handle_phrase()?,
|
|
key!(KeyCode::Char('d')) => handle_phrase()?,
|
|
_ => self.arrangement.handle(from)?
|
|
}
|
|
},
|
|
}.unwrap_or(false) {
|
|
match from.event() {
|
|
// Tab navigation
|
|
key!(KeyCode::Tab) => { self.focus_next(); },
|
|
key!(Shift-KeyCode::Tab) => { self.focus_prev(); },
|
|
key!(KeyCode::BackTab) => { self.focus_prev(); },
|
|
key!(Shift-KeyCode::BackTab) => { self.focus_prev(); },
|
|
// Directional navigation
|
|
key!(KeyCode::Up) => { self.focus_up(); },
|
|
key!(KeyCode::Down) => { self.focus_down(); },
|
|
key!(KeyCode::Left) => { self.focus_left(); },
|
|
key!(KeyCode::Right) => { self.focus_right(); },
|
|
// Put selected phrase at position
|
|
key!(KeyCode::Char('s')) => { self.arrangement.phrase_put(); },
|
|
// Global play/pause binding
|
|
key!(KeyCode::Char(' ')) => match self.transport {
|
|
Some(ref mut transport) => { transport.write().unwrap().toggle_play()?; },
|
|
None => { return Ok(None) }
|
|
},
|
|
_ => {}
|
|
}
|
|
};
|
|
Ok(Some(true))
|
|
}
|
|
}
|
|
impl Arranger<Tui> {
|
|
pub fn rename_selected (&mut self) {
|
|
let Arrangement { selected, ref name, ref tracks, ref scenes, .. } = self.arrangement;
|
|
//self.modal = match selected {
|
|
//ArrangementFocus::Mix => {
|
|
//Some(Box::new(ArrangerRenameModal::new(selected, &*name.read().unwrap())))
|
|
//},
|
|
//ArrangementFocus::Track(t) => {
|
|
//Some(Box::new(ArrangerRenameModal::new(selected, &*tracks[t].name.read().unwrap())))
|
|
//},
|
|
//ArrangementFocus::Scene(s) => {
|
|
//Some(Box::new(ArrangerRenameModal::new(selected, &*scenes[s].name.read().unwrap())))
|
|
//},
|
|
//ArrangementFocus::Clip(t, s) => if let Some(ref clip) = scenes[s].clips[t] {
|
|
//Some(Box::new(ArrangerRenameModal::new(selected, &clip.read().unwrap().name)))
|
|
//} else {
|
|
//None
|
|
//}
|
|
//};
|
|
}
|
|
}
|
|
impl Handle<Tui> for Arrangement<Tui> {
|
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
match from.event() {
|
|
key!(KeyCode::Char('`')) => { self.mode.to_next(); },
|
|
key!(KeyCode::Delete) => { self.delete(); },
|
|
key!(KeyCode::Char('.')) => { self.increment(); },
|
|
key!(KeyCode::Char(',')) => { self.decrement(); },
|
|
// TODO: next/prev scene
|
|
key!(KeyCode::Enter) => { self.activate(); },
|
|
key!(Ctrl-KeyCode::Char('a')) => { self.scene_add(None, None)?; },
|
|
key!(Ctrl-KeyCode::Char('t')) => { self.track_add(None, None)?; },
|
|
key!(KeyCode::Char('n')) => { todo!("rename selected"); },
|
|
key!(KeyCode::Char('l')) => if let Some(phrase) = self.phrase() {
|
|
phrase.write().unwrap().toggle_loop()
|
|
},
|
|
key!(KeyCode::Up) => {
|
|
match self.mode {
|
|
ArrangementViewMode::Horizontal => self.track_prev(),
|
|
_ => self.scene_prev(),
|
|
};
|
|
self.show_phrase();
|
|
},
|
|
key!(KeyCode::Down) => {
|
|
match self.mode {
|
|
ArrangementViewMode::Horizontal => self.track_next(),
|
|
_ => self.scene_next(),
|
|
};
|
|
self.show_phrase();
|
|
},
|
|
key!(KeyCode::Left) => {
|
|
match self.mode {
|
|
ArrangementViewMode::Horizontal => self.scene_prev(),
|
|
_ => self.track_prev(),
|
|
};
|
|
self.show_phrase();
|
|
},
|
|
key!(KeyCode::Right) => {
|
|
match self.mode {
|
|
ArrangementViewMode::Horizontal => self.scene_next(),
|
|
_ => self.track_next(),
|
|
};
|
|
self.show_phrase();
|
|
},
|
|
_ => return Ok(None)
|
|
}
|
|
Ok(Some(true))
|
|
}
|
|
}
|
|
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))
|
|
}?;
|
|
add(&Align::SE(self.selected.description(
|
|
&self.tracks,
|
|
&self.scenes,
|
|
).as_str()))
|
|
})
|
|
}
|
|
}
|
|
impl<'a> Content for VerticalArranger<'a, Tui> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
let Self(state, factor) = self;
|
|
let (cols, rows) = if *factor == 0 {(
|
|
state.track_widths(),
|
|
Scene::ppqs(state.tracks.as_slice(), state.scenes.as_slice()),
|
|
)} else {(
|
|
state.track_widths(),
|
|
(0..=state.scenes.len()).map(|i|(factor*PPQ, factor*PPQ*i)).collect::<Vec<_>>(),
|
|
)};
|
|
//let height = rows.last().map(|(w,y)|(y+w)/PPQ).unwrap_or(16);
|
|
let tracks: &[ArrangementTrack<Tui>] = state.tracks.as_ref();
|
|
let scenes: &[Scene] = state.scenes.as_ref();
|
|
let offset = 3 + Scene::longest_name(scenes) as u16;
|
|
let content = Layers::new(move |add|{
|
|
let rows: &[(usize, usize)] = rows.as_ref();
|
|
let cols: &[(usize, usize)] = cols.as_ref();
|
|
|
|
let track_titles = row!((track, (w, _)) in tracks.iter().zip(cols) =>
|
|
(&track.name.read().unwrap().as_str() as &dyn Widget<Engine = Tui>)
|
|
.min_xy(*w as u16, 2)
|
|
.bg(track.color)
|
|
.push_x(offset));
|
|
|
|
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);
|
|
|
|
let scene_clip = |scene, track: usize, w: u16, h: u16|Layers::new(move |add|{
|
|
let mut color = Color::Rgb(40, 50, 30);
|
|
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);
|
|
add(&name.as_str().push_x(1).fixed_x(w))?;
|
|
color = (phrase as &Arc<RwLock<Phrase>>).read().unwrap().color;
|
|
//if let Some(playing_phrase) = &track.player.phrase {
|
|
//if *playing_phrase.read().unwrap() == *phrase.read().unwrap() {
|
|
//color = COLOR_PLAYING
|
|
//}
|
|
//}
|
|
},
|
|
_ => {}
|
|
};
|
|
add(&Background(color))
|
|
}).fixed_xy(w, h);
|
|
|
|
let tracks_clips = col!((scene, (pulses, _)) in scenes.iter().zip(rows) => {
|
|
let height = 1.max((pulses / PPQ) as u16);
|
|
let playing = scene.is_playing(tracks);
|
|
Stack::right(move |add| {
|
|
add(&scene_name(scene, playing, height).bg(scene.color))?;
|
|
for (track, (w, _)) in cols.iter().enumerate() {
|
|
add(&scene_clip(scene, track, *w as u16, height))?;
|
|
}
|
|
Ok(())
|
|
}).fixed_y(height)
|
|
});
|
|
|
|
add(&VerticalArrangerGrid(offset, &rows, &cols))?;
|
|
add(&VerticalArrangerCursor(state.focused, state.selected, offset, &cols, &rows))?;
|
|
add(&col!(track_titles, tracks_clips))?;
|
|
Ok(())
|
|
})
|
|
.bg(Color::Rgb(28, 35, 25))
|
|
.border(Lozenge(Style::default()
|
|
.bg(Color::Rgb(40, 50, 30))
|
|
.fg(if self.0.focused {
|
|
Color::Rgb(100, 110, 40)
|
|
} else {
|
|
Color::Rgb(70, 80, 50)
|
|
})));
|
|
lay!(content, TuiStyle::fg("Session", if self.0.focused {
|
|
Color::Rgb(150, 160, 90)
|
|
} else {
|
|
Color::Rgb(120, 130, 100)
|
|
}).push_x(1))
|
|
}
|
|
}
|
|
impl<'a> Widget for VerticalArrangerGrid<'a> {
|
|
type Engine = Tui;
|
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
let area = to.area();
|
|
let Self(offset, rows, cols) = self;
|
|
let style = Some(Style::default().fg(COLOR_SEPARATOR));
|
|
for (_, x) in cols.iter() {
|
|
let x = offset + area.x() + *x as u16 - 1;
|
|
for y in area.y()..area.y2() {
|
|
to.blit(&"▎", x, y, style);
|
|
}
|
|
}
|
|
for (_, y) in rows.iter() {
|
|
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(())
|
|
}
|
|
}
|
|
impl<'a> Widget for VerticalArrangerCursor<'a> {
|
|
type Engine = Tui;
|
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
let area = to.area();
|
|
let Self(focused, selected, offset, cols, rows) = *self;
|
|
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(2), &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));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
impl<'a> Content for HorizontalArranger<'a, Tui> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
let Arrangement { tracks, focused, selected, scenes, .. } = 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 { tracks, 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(())
|
|
}),
|
|
)
|
|
)
|
|
}
|
|
}
|