remove ArrangerView<'a>

This commit is contained in:
🪞👃🪞 2025-05-17 09:02:33 +03:00
parent e9b4a2ca78
commit a7f37e52cf
12 changed files with 843 additions and 949 deletions

View file

@ -77,16 +77,13 @@ impl App {
self.project.view_audio_outs_status(self.color)
}
pub fn view_arranger (&self) -> impl Content<TuiOut> + use<'_> {
ArrangerView::new(&self.project, self.editor.as_ref())
}
pub fn view_arranger_scenes (&self) -> impl Content<TuiOut> + use<'_> {
self.scenes_view(&self.editor)
&self.project
}
pub fn view_arranger_scenes_names (&self) -> impl Content<TuiOut> + use<'_> {
self.scenes_names()
self.project.view_scenes_names()
}
pub fn view_arranger_scenes_clips (&self) -> impl Content<TuiOut> + use<'_> {
self.scenes_clips(&self.editor)
self.project.view_scenes_clips(&self.editor)
}
pub fn view_arranger_track_names (&self) -> impl Content<TuiOut> + use<'_> {
self.project.view_track_names(self.color)
@ -258,15 +255,9 @@ impl ScenesView for App {
fn scene_selected (&self) -> Option<usize> {
self.project.selection.scene()
}
fn scene_last (&self) -> usize {
self.project.scenes.len().saturating_sub(1)
}
fn track_selected (&self) -> Option<usize> {
self.project.selection.track()
}
fn is_editing (&self) -> bool {
self.editor.is_some()
}
}
pub(crate) fn heading <'a> (

View file

@ -11,11 +11,8 @@ macro_rules! def_sizes_iter {
mod arranger_api; pub use self::arranger_api::*;
mod arranger_clip; pub use self::arranger_clip::*;
mod arranger_model; pub use self::arranger_model::*;
mod arranger_port; pub use self::arranger_port::*;
mod arranger_scene; pub use self::arranger_scene::*;
mod arranger_scenes; pub use self::arranger_scenes::*;
mod arranger_select; pub use self::arranger_select::*;
mod arranger_track; pub use self::arranger_track::*;
mod arranger_tracks; pub use self::arranger_tracks::*;
mod arranger_view; pub use self::arranger_view::*;
@ -30,48 +27,3 @@ pub(crate) fn wrap (bg: Color, fg: Color, content: impl Content<TuiOut>) -> impl
let right = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV("")));
Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content)))
}
pub(crate) fn io_ports <'a, T: PortsSizes<'a>> (
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
Map::new(iter, move|(
index, name, connections, y, y2
): (usize, &'a Arc<str>, &'a [PortConnect], usize, usize), _|
map_south(y as u16, (y2-y) as u16, Bsp::s(
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(" 󰣲 ", name))))),
Map::new(||connections.iter(), move|connect: &'a PortConnect, index|map_south(index as u16, 1,
Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg,
&connect.info)))))))))
}
pub(crate) fn io_conns <'a, T: PortsSizes<'a>> (
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
Map::new(iter, move|(
index, name, connections, y, y2
): (usize, &'a Arc<str>, &'a [PortConnect], usize, usize), _|
map_south(y as u16, (y2-y) as u16, Bsp::s(
Fill::x(Tui::bold(true, wrap(bg, fg, Fill::x(Align::w("▞▞▞▞ ▞▞▞▞"))))),
Map::new(||connections.iter(), move|connect, index|map_south(index as u16, 1,
Fill::x(Align::w(Tui::bold(false, wrap(bg, fg, Fill::x(""))))))))))
}
pub trait HasWidth {
const MIN_WIDTH: usize;
/// Increment track width.
fn width_inc (&mut self);
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
fn width_dec (&mut self);
}
impl HasWidth for Track {
const MIN_WIDTH: usize = 9;
fn width_inc (&mut self) {
self.width += 1;
}
fn width_dec (&mut self) {
if self.width > Track::MIN_WIDTH {
self.width -= 1;
}
}
}

View file

@ -215,3 +215,15 @@ impl<'state> Context<'state, DeviceCommand> for Arrangement {
Context::get(&self, iter)
}
}
impl<'state> Context<'state, SceneCommand> for Arrangement {
fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option<SceneCommand> {
Context::get(&self, iter)
}
}
impl<'state> Context<'state, ClipCommand> for Arrangement {
fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option<ClipCommand> {
Context::get(&self, iter)
}
}

View file

@ -1,11 +1,5 @@
use crate::*;
impl<'state> Context<'state, ClipCommand> for Arrangement {
fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option<ClipCommand> {
Context::get(&self, iter)
}
}
#[tengri_proc::expose]
impl MidiClip {
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
@ -30,46 +24,3 @@ impl ClipCommand {
todo!()
}
}
impl ClipsView for Arrangement {}
impl<'a> ClipsView for ArrangerView<'a> {}
pub trait ClipsView: HasTrackScroll + HasSceneScroll + Send + Sync {
fn scenes_clips_2 <'a> (
&'a self,
theme: ItemTheme
) -> impl Content<TuiOut> + 'a {
Fixed::y(self.scenes().len() as u16 * 2, Tui::bg(theme.darker.rgb,
Align::w(Fill::x(Map::new(||self.scenes().iter().skip(self.scene_scroll()),
move|scene: &'a Scene, index|self.track_scenes(index, scene))))))
}
fn track_scenes <'a> (
&'a self,
scene_index: usize,
scene: &'a Scene
) -> impl Content<TuiOut> + 'a {
Push::y(scene_index as u16 * 2u16, Fixed::xy(20, 2, Map::new(
move||scene.clips.iter().skip(self.track_scroll()),
move|clip: &'a Option<Arc<RwLock<MidiClip>>>, track_index|
self.track_scene_clip(scene_index, scene, track_index, clip))))
}
fn track_scene_clip (
&self,
scene_index: usize,
scene: &Scene,
track_index: usize,
clip: &Option<Arc<RwLock<MidiClip>>>
) -> impl Content<TuiOut> {
let (theme, text) = if let Some(clip) = clip {
let clip = clip.read().unwrap();
(clip.color, clip.name.clone())
} else {
(scene.color, Default::default())
};
Push::x(track_index as u16 * 14, Tui::bg(theme.dark.rgb, Bsp::e(
format!(" {scene_index:2} {track_index:2} "),
Tui::fg(Rgb(255, 255, 255),
Tui::bold(true, format!("{}", text))))))
}
}

View file

@ -76,18 +76,6 @@ impl Arrangement {
pub fn h (&self) -> u16 {
self.size.h() as u16
}
/// Height taken by all scenes.
pub fn h_scenes (&self, is_editing: bool) -> u16 {
self.scenes_with_sizes(
is_editing,
ArrangerView::H_SCENE,
ArrangerView::H_EDITOR,
self.selection().track(),
self.selection().scene(),
)
.last()
.map(|(_, _, _, y)|y as u16).unwrap_or(0)
}
/// Height taken by all inputs.
pub fn h_inputs (&self) -> u16 {
self.midi_ins_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
@ -151,6 +139,64 @@ impl Arrangement {
clip.write().unwrap().toggle_loop()
}
}
/// Add multiple tracks
pub fn tracks_add (
&mut self,
count: usize,
width: Option<usize>,
mins: &[PortConnect],
mouts: &[PortConnect],
) -> Usually<()> {
let jack = self.jack().clone();
let track_color_1 = ItemColor::random();
let track_color_2 = ItemColor::random();
for i in 0..count {
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
let mut track = self.track_add(None, Some(color), mins, mouts)?.1;
if let Some(width) = width {
track.width = width;
}
}
Ok(())
}
/// Add a track
pub fn track_add (
&mut self,
name: Option<&str>,
color: Option<ItemTheme>,
mins: &[PortConnect],
mouts: &[PortConnect],
) -> Usually<(usize, &mut Track)> {
self.track_last += 1;
let name: Arc<str> = name.map_or_else(
||format!("Track{:02}", self.track_last).into(),
|x|x.to_string().into()
);
let mut track = Track {
width: (name.len() + 2).max(12),
color: color.unwrap_or_else(ItemTheme::random),
sequencer: Sequencer::new(
&format!("{name}"),
self.jack(),
Some(self.clock()),
None,
mins,
mouts
)?,
name,
..Default::default()
};
self.tracks_mut().push(track);
let len = self.tracks().len();
let index = len - 1;
for scene in self.scenes_mut().iter_mut() {
while scene.clips.len() < len {
scene.clips.push(None);
}
}
Ok((index, &mut self.tracks_mut()[index]))
}
}
#[cfg(feature = "sampler")]
@ -164,3 +210,24 @@ impl Arrangement {
self.get_track_mut()?.sampler_mut(0)
}
}
impl ScenesView for Arrangement {
fn arrangement (&self) -> &Arrangement {
self
}
fn scenes_height (&self) -> u16 {
(self.height() as u16).saturating_sub(20)
}
fn width_side (&self) -> u16 {
(self.width() as u16 * 2 / 10).max(20)
}
fn width_mid (&self) -> u16 {
(self.width() as u16).saturating_sub(2 * self.width_side()).max(40)
}
fn scene_selected (&self) -> Option<usize> {
self.selection().scene()
}
fn track_selected (&self) -> Option<usize> {
self.selection().track()
}
}

View file

@ -1,135 +0,0 @@
use crate::*;
impl<'a> ArrangerView<'a> {
pub(crate) fn input_routes (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(self.arrangement.h_inputs())
.left(self.width_side,
io_ports(Tui::g(224), Tui::g(32), ||self.arrangement.midi_ins_with_sizes()))
.middle(self.width_mid,
per_track_top(||self.tracks_with_sizes_scrolled(),
move|_, &Track { color, .. }|io_conns(
color.dark.rgb,
color.darker.rgb,
||self.arrangement.midi_ins_with_sizes()
)))
}
pub(crate) fn input_ports (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(1)
.left(self.width_side,
button_3("i", "midi ins", format!("{}", self.arrangement.midi_ins().len()), self.is_editing))
.right(self.width_side,
button_2("I", "add midi in", self.is_editing))
.middle(self.width_mid,
per_track_top(||self.tracks_with_sizes_scrolled(),
move|t, track|{
let rec = track.sequencer.recording;
let mon = track.sequencer.monitoring;
let rec = if rec { White } else { track.color.darkest.rgb };
let mon = if mon { White } else { track.color.darkest.rgb };
let bg = if self.track_selected == Some(t) {
track.color.light.rgb
} else {
track.color.base.rgb
};
//let bg2 = if t > 0 { track.color.base.rgb } else { Reset };
wrap(bg, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(
Tui::fg_bg(rec, bg, "Rec "),
Tui::fg_bg(mon, bg, "Mon "),
))))
}))
}
pub(crate) fn input_intos (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(2)
.left(self.width_side,
Bsp::s(Align::e("Input:"), Align::e("Into clip:")))
.middle(self.width_mid,
per_track_top(||self.tracks_with_sizes_scrolled(),
|_, _|Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ ")))))
}
}
impl<'a> ArrangerView<'a> {
pub(crate) fn output_nexts (&self) -> impl Content<TuiOut> + '_ {
Tryptich::top(2)
.left(self.width_side, Align::ne("From clip:"))
.middle(self.width_mid, per_track_top(||self.tracks_with_sizes_scrolled(),
|_, _|Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default())))))
}
pub(crate) fn output_froms (&'a self) -> impl Content<TuiOut> + 'a {
let label = Align::ne("Next clip:");
Tryptich::top(2).left(self.width_side, label).middle(self.width_mid, per_track_top(
||self.tracks_with_sizes_scrolled(), |t, track|{
let queued = track.sequencer.next_clip.is_some();
let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ "));
let queued_clip = Thunk::new(||{
Tui::bg(Reset, if let Some((_, clip)) = track.sequencer.next_clip.as_ref() {
if let Some(clip) = clip {
clip.read().unwrap().name.clone()
} else {
"Stop".into()
}
} else {
"".into()
})
});
Either(queued, queued_clip, queued_blank)
}))
}
pub(crate) fn output_ports (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(1)
.left(self.width_side, self.output_count())
.right(self.width_side, self.output_add())
.middle(self.width_mid, self.output_map())
}
pub(crate) fn output_count (&'a self) -> impl Content<TuiOut> + 'a {
button_3(
"o",
"midi outs",
format!("{}", self.arrangement.midi_outs().len()),
self.is_editing
)
}
pub(crate) fn output_add (&'a self) -> impl Content<TuiOut> + 'a {
button_2("O", "add midi out", self.is_editing)
}
pub(crate) fn output_map (&'a self) -> impl Content<TuiOut> + 'a {
per_track_top(||self.tracks_with_sizes_scrolled(), move|i, t|{
let mute = false;
let solo = false;
let mute = if mute { White } else { t.color.darkest.rgb };
let solo = if solo { White } else { t.color.darkest.rgb };
let bg_1 = if self.track_selected == Some(i) {
t.color.light.rgb
} else {
t.color.base.rgb
};
let bg_2 = if i > 0 { t.color.base.rgb } else { Reset };
let mute = Tui::fg_bg(mute, bg_1, "Play ");
let solo = Tui::fg_bg(solo, bg_1, "Solo ");
wrap(bg_1, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(mute, solo))))
})
}
pub(crate) fn output_conns (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(self.arrangement.h_outputs())
.left(self.width_side, io_ports(
Tui::g(224), Tui::g(32), ||self.arrangement.midi_outs_with_sizes()))
.middle(self.width_mid, per_track_top(||self.tracks_with_sizes_scrolled(),
|_, t|io_conns(
t.color.dark.rgb,
t.color.darker.rgb,
||self.arrangement.midi_outs_with_sizes()
)))
}
}

View file

@ -1,84 +0,0 @@
use crate::*;
impl<'state> Context<'state, SceneCommand> for Arrangement {
fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option<SceneCommand> {
Context::get(&self, iter)
}
}
#[tengri_proc::expose]
impl Scene {
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
fn _todo_usize_stub_ (&self) -> usize { todo!() }
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
}
#[tengri_proc::command(Scene)]
impl SceneCommand {
fn set_name (scene: &mut Scene, mut name: Arc<str>) -> Perhaps<Self> {
std::mem::swap(&mut scene.name, &mut name);
Ok(Some(Self::SetName { name }))
}
fn set_color (scene: &mut Scene, mut color: ItemTheme) -> Perhaps<Self> {
std::mem::swap(&mut scene.color, &mut color);
Ok(Some(Self::SetColor { color }))
}
fn set_size (scene: &mut Scene, size: usize) -> Perhaps<Self> {
todo!()
}
fn set_zoom (scene: &mut Scene, zoom: usize) -> Perhaps<Self> {
todo!()
}
}
impl<T: Has<Option<Scene>> + Send + Sync> HasScene for T {}
pub trait HasScene: Has<Option<Scene>> + Send + Sync {
fn scene (&self) -> Option<&Scene> {
Has::<Option<Scene>>::get(self).as_ref()
}
fn scene_mut (&mut self) -> &mut Option<Scene> {
Has::<Option<Scene>>::get_mut(self)
}
}
#[derive(Debug, Default)]
pub struct Scene {
/// Name of scene
pub name: Arc<str>,
/// Identifying color of scene
pub color: ItemTheme,
/// Clips in scene, one per track
pub clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
}
impl Scene {
/// Returns the pulse length of the longest clip in the scene
pub fn pulses (&self) -> usize {
self.clips.iter().fold(0, |a, p|{
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
})
}
/// Returns true if all clips in the scene are
/// currently playing on the given collection of tracks.
pub fn is_playing (&self, tracks: &[Track]) -> bool {
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
.all(|(track_index, clip)|match clip {
Some(c) => tracks
.get(track_index)
.map(|track|{
if let Some((_, Some(clip))) = track.sequencer().play_clip() {
*clip.read().unwrap() == *c.read().unwrap()
} else {
false
}
})
.unwrap_or(false),
None => true
})
}
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
}
}

View file

@ -41,179 +41,9 @@ pub trait HasSceneScroll: HasScenes {
impl HasSceneScroll for Arrangement {
fn scene_scroll (&self) -> usize { self.scene_scroll }
}
impl<'a> HasSceneScroll for ArrangerView<'a> {
fn scene_scroll (&self) -> usize { self.arrangement.scene_scroll() }
}
pub type SceneWith<'a, T: Send + Sync> = (usize, &'a Scene, usize, usize, T);
pub trait ScenesView: HasSceneScroll + Send + Sync {
/// Default scene height.
const H_SCENE: usize = 2;
/// Default editor height.
const H_EDITOR: usize = 15;
/// Render scenes with clips
fn scenes_view <'a> (&'a self, editor: &'a Option<MidiEditor>) -> impl Content<TuiOut> + 'a {
Tryptich::center(self.scenes_height())
.left(self.width_side(), self.scenes_names())
.middle(self.width_mid(), self.scenes_clips(editor))
}
fn is_editing (&self) -> bool;
fn arrangement (&self) -> &Arrangement;
fn scene_last (&self) -> usize;
fn scene_selected (&self) -> Option<usize>;
fn track_selected (&self) -> Option<usize>;
fn scenes_height (&self) -> u16;
fn width_side (&self) -> u16;
fn width_mid (&self) -> u16;
fn scenes_names (&self) -> impl Content<TuiOut> {
let h = self.scenes_with_prev_color().last().map(|(_,_,_,h,_)|h as u16).unwrap_or(0);
Fixed::y(h, Map::new(move||self.scenes_with_prev_color(),
move|(s, scene, y1, y2, previous): SceneWith<'_, Option<ItemTheme>>, _|{
let height = (1 + y2 - y1) as u16;
let name = Some(scene.name.clone());
let content = Fill::x(Align::w(Tui::bold(true, Bsp::e("", name))));
let selected = self.scene_selected() == Some(s);
let neighbor = s > 0 && self.scene_selected() == Some(s - 1);
let is_last = self.scene_last() == s;
let theme = scene.color;
let fg = theme.lightest.rgb;
let bg = if selected { theme.light } else { theme.base }.rgb;
let hi = if let Some(previous) = previous {
if neighbor { previous.light.rgb } else { previous.base.rgb }
} else {
Reset
};
let lo = if is_last { Reset } else if selected {
theme.light.rgb
} else {
theme.base.rgb
};
Fill::x(map_south(y1 as u16, height, Fixed::y(height, Phat {
width: 0, height: 0, content, colors: [fg, bg, hi, lo]
})))
}))
}
fn scenes_names_2 (&self, theme: ItemTheme) -> impl Content<TuiOut> {
let h = self.scenes().len() as u16 * 2;
let bg = theme.darker.rgb;
Fixed::y(h, Tui::bg(bg, Align::w(Fill::x(Map::new(
||self.scenes().iter().skip(self.scene_scroll()),
move|scene: &Scene, index|
Push::y(index as u16 * 2u16, Fixed::xy(20, 2,
Tui::bg(scene.color.dark.rgb, Align::nw(Bsp::e(
format!(" {index:2} "),
Tui::fg(Rgb(255, 255, 255),
Tui::bold(true, format!("{}", scene.name)))))))))))))
}
fn scenes_with_prev_color (&self) -> impl Iterator<Item=SceneWith<Option<ItemTheme>>> + Send + Sync {
self.scenes_iter().map(|(s, scene, y1, y2)|(s, scene, y1, y2,
(s>0).then(||self.arrangement().scenes()[s-1].color)))
}
fn per_track <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
Map::new(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
Tui::fg_bg(track.color.lightest.rgb, track.color.darker.rgb,
map_east(x1 as u16, (x2 - x1) as u16, callback(index, track))) })
}
fn scenes_clips <'a> (&'a self, editor: &'a Option<MidiEditor>)
-> impl Content<TuiOut> + 'a
{
let h = self.scenes_with_prev_color().last().map(|(_,_,_,h,_)|h as u16).unwrap_or(0);
Fixed::y(h, Self::per_track(||self.tracks_with_sizes_scrolled(),
move|track_index, track|Map::new(move||self.scenes_with_clip(track_index),
move|(s, scene, y1, y2, previous): SceneWith<'_, Option<ItemTheme>>, _|{
let (name, theme) = if let Some(clip) = &scene.clips[track_index] {
let clip = clip.read().unwrap();
(Some(clip.name.clone()), clip.color)
} else {
(None, ItemTheme::G[32])
};
let content = Fill::x(Align::w(Tui::bold(true, Bsp::e("", name))));
let same_track = self.track_selected() == Some(track_index);
let selected = same_track && self.scene_selected() == Some(s);
let neighbor = same_track && s > 0 && self.scene_selected() == Some(s - 1);
let is_last = self.scene_last() == s;
let fg = theme.lightest.rgb;
let bg = if selected { theme.light } else { theme.base }.rgb;
let hi = if let Some(previous) = previous {
if neighbor { previous.light.rgb } else { previous.base.rgb }
} else {
Reset
};
let lo = if is_last {
Reset
} else if selected {
theme.light.rgb
} else {
theme.base.rgb
};
let height = (1 + y2 - y1) as u16;
map_south(y1 as u16, height, Bsp::b(Fixed::y(height, Phat {
width: 0, height: 0, content, colors: [fg, bg, hi, lo]
}), When(
self.is_editing() && same_track && self.scene_selected() == Some(s),
editor
)))
})))
}
fn scenes_with_clip (&self, track_index: usize) -> impl Iterator<Item=SceneWith<'_, Option<ItemTheme>>> + Send + Sync {
self.scenes_iter().map(move|(s, scene, y1, y2)|(s, scene, y1, y2,
(s>0).then(||self.arrangement().scenes()[s-1].clips[track_index].as_ref()
.map(|c|c.read().unwrap().color)
.unwrap_or(ItemTheme::G[32]))))
}
/// A scene with size and color.
fn scenes_iter (&self) -> impl Iterator<Item=(usize, &Scene, usize, usize,)> + Send + Sync {
let selection = Has::<Selection>::get(self.arrangement());
self.arrangement().scenes_with_sizes(
self.is_editing(),
Self::H_SCENE, Self::H_EDITOR,
selection.track(), selection.scene(),
).map_while(|(s, scene, y1, y2)|(y2<=self.scenes_height() as usize)
.then_some((s, scene, y1, y2)))
}
fn tracks_with_sizes_scrolled <'t> (&'t self) -> impl TracksSizes<'t> {
self.arrangement()
.tracks_with_sizes(
&self.arrangement().selection(),
self.is_editing().then_some(20/*FIXME*/)
)
.map_while(move|(t, track, x1, x2)|{
(self.width_mid() > x2 as u16).then_some((t, track, x1, x2))
})
}
}
impl<'a> ScenesView for ArrangerView<'a> {
fn arrangement (&self) -> &Arrangement {
self.arrangement
}
fn scenes_height (&self) -> u16 {
self.scenes_height
}
fn width_side (&self) -> u16 {
self.width_side
}
fn width_mid (&self) -> u16 {
self.width_mid
}
fn scene_selected (&self) -> Option<usize> {
self.arrangement.selection.scene()
}
fn scene_last (&self) -> usize {
self.scene_last
}
fn track_selected (&self) -> Option<usize> {
self.arrangement.selection.track()
}
fn is_editing (&self) -> bool {
self.is_editing
}
}
impl<T: HasScenes + HasTracks> AddScene for T {}
pub trait AddScene: HasScenes + HasTracks {
@ -242,3 +72,85 @@ pub trait AddScene: HasScenes + HasTracks {
Ok((index, &mut self.scenes_mut()[index]))
}
}
#[tengri_proc::expose]
impl Scene {
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
fn _todo_usize_stub_ (&self) -> usize { todo!() }
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
}
#[tengri_proc::command(Scene)]
impl SceneCommand {
fn set_name (scene: &mut Scene, mut name: Arc<str>) -> Perhaps<Self> {
std::mem::swap(&mut scene.name, &mut name);
Ok(Some(Self::SetName { name }))
}
fn set_color (scene: &mut Scene, mut color: ItemTheme) -> Perhaps<Self> {
std::mem::swap(&mut scene.color, &mut color);
Ok(Some(Self::SetColor { color }))
}
fn set_size (scene: &mut Scene, size: usize) -> Perhaps<Self> {
todo!()
}
fn set_zoom (scene: &mut Scene, zoom: usize) -> Perhaps<Self> {
todo!()
}
}
impl<T: Has<Option<Scene>> + Send + Sync> HasScene for T {}
pub trait HasScene: Has<Option<Scene>> + Send + Sync {
fn scene (&self) -> Option<&Scene> {
Has::<Option<Scene>>::get(self).as_ref()
}
fn scene_mut (&mut self) -> &mut Option<Scene> {
Has::<Option<Scene>>::get_mut(self)
}
}
#[derive(Debug, Default)]
pub struct Scene {
/// Name of scene
pub name: Arc<str>,
/// Identifying color of scene
pub color: ItemTheme,
/// Clips in scene, one per track
pub clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
}
impl Scene {
/// Returns the pulse length of the longest clip in the scene
pub fn pulses (&self) -> usize {
self.clips.iter().fold(0, |a, p|{
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
})
}
/// Returns true if all clips in the scene are
/// currently playing on the given collection of tracks.
pub fn is_playing (&self, tracks: &[Track]) -> bool {
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
.all(|(track_index, clip)|match clip {
Some(c) => tracks
.get(track_index)
.map(|track|{
if let Some((_, Some(clip))) = track.sequencer().play_clip() {
*clip.read().unwrap() == *c.read().unwrap()
} else {
false
}
})
.unwrap_or(false),
None => true
})
}
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
}
}
//scene_scroll: Fill::y(Fixed::x(1, ScrollbarV {
//offset: arrangement.scene_scroll,
//length: h_scenes_area as usize,
//total: h_scenes as usize,
//})),

View file

@ -1,199 +0,0 @@
use crate::*;
impl<T: MaybeHas<Track>> HasTrack for T {
fn track (&self) -> Option<&Track> {
self.get()
}
fn track_mut (&mut self) -> Option<&mut Track> {
self.get_mut()
}
}
#[tengri_proc::expose]
impl Track {
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
fn _todo_usize_stub_ (&self) -> usize { todo!() }
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
}
#[tengri_proc::command(Track)]
impl TrackCommand {
fn set_name (track: &mut Track, mut name: Arc<str>) -> Perhaps<Self> {
std::mem::swap(&mut name, &mut track.name);
Ok(Some(Self::SetName { name }))
}
fn set_color (track: &mut Track, mut color: ItemTheme) -> Perhaps<Self> {
std::mem::swap(&mut color, &mut track.color);
Ok(Some(Self::SetColor { color }))
}
fn set_mute (track: &mut Track, value: Option<bool>) -> Perhaps<Self> {
todo!()
}
fn set_solo (track: &mut Track, value: Option<bool>) -> Perhaps<Self> {
todo!()
}
fn set_rec (track: &mut Track, value: Option<bool>) -> Perhaps<Self> {
let current = track.sequencer.recording;
let value = value.unwrap_or(!current);
Ok((value != current).then_some(Self::SetRec { value: Some(current) }))
}
fn set_mon (track: &mut Track, value: Option<bool>) -> Perhaps<Self> {
let current = track.sequencer.monitoring;
let value = value.unwrap_or(!current);
Ok((value != current).then_some(Self::SetRec { value: Some(current) }))
}
fn set_size (track: &mut Track, size: usize) -> Perhaps<Self> {
todo!()
}
fn set_zoom (track: &mut Track, zoom: usize) -> Perhaps<Self> {
todo!()
}
fn stop (track: &mut Track) -> Perhaps<Self> {
track.sequencer.enqueue_next(None);
Ok(None)
}
}
#[derive(Debug, Default)]
pub struct Track {
/// Name of track
pub name: Arc<str>,
/// Identifying color of track
pub color: ItemTheme,
/// Preferred width of track column
pub width: usize,
/// MIDI sequencer state
pub sequencer: Sequencer,
/// Device chain
pub devices: Vec<Device>,
}
has!(Clock: |self: Track|self.sequencer.clock);
has!(Sequencer: |self: Track|self.sequencer);
impl Track {
/// Create a new track with only the default [Sequencer].
pub fn new (
name: &impl AsRef<str>,
color: Option<ItemTheme>,
jack: &Jack,
clock: Option<&Clock>,
clip: Option<&Arc<RwLock<MidiClip>>>,
midi_from: &[PortConnect],
midi_to: &[PortConnect],
) -> Usually<Self> {
Ok(Self {
name: name.as_ref().into(),
color: color.unwrap_or_default(),
sequencer: Sequencer::new(
format!("{}/sequencer", name.as_ref()),
jack,
clock,
clip,
midi_from,
midi_to
)?,
..Default::default()
})
}
/// Create a new track connecting the [Sequencer] to a [Sampler].
pub fn new_with_sampler (
name: &impl AsRef<str>,
color: Option<ItemTheme>,
jack: &Jack,
clock: Option<&Clock>,
clip: Option<&Arc<RwLock<MidiClip>>>,
midi_from: &[PortConnect],
midi_to: &[PortConnect],
audio_from: &[&[PortConnect];2],
audio_to: &[&[PortConnect];2],
) -> Usually<Self> {
let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?;
track.devices.push(Device::Sampler(Sampler::new(
jack,
&format!("{}/sampler", name.as_ref()),
&[PortConnect::exact(format!("{}:{}",
jack.with_client(|c|c.name().to_string()),
track.sequencer.midi_outs[0].name()
))],
audio_from,
audio_to
)?));
Ok(track)
}
#[cfg(feature = "sampler")]
pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> {
for device in self.devices.iter() {
match device {
Device::Sampler(s) => if nth == 0 {
return Some(s);
} else {
nth -= 1;
},
_ => {}
}
}
None
}
#[cfg(feature = "sampler")]
pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> {
for device in self.devices.iter_mut() {
match device {
Device::Sampler(s) => if nth == 0 {
return Some(s);
} else {
nth -= 1;
},
_ => {}
}
}
None
}
}
pub trait HasTrack {
fn track (&self) -> Option<&Track>;
fn track_mut (&mut self) -> Option<&mut Track>;
fn view_midi_ins_status (&self, theme: ItemTheme) -> impl Content<TuiOut> {
self.track().map(|track|{
let ins = track.sequencer.midi_ins.len() as u16;
Fixed::xy(20, 1 + ins, Outer(true, Style::default().fg(Tui::g(96))).enclose(
Fixed::xy(20, 1 + ins, FieldV(theme, format!("MIDI ins: "),
Map::south(1, ||track.sequencer.midi_ins.iter(),
|port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))})
}
fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Content<TuiOut> {
self.track().map(|track|{
let outs = track.sequencer.midi_outs.len() as u16;
Fixed::xy(20, 1 + outs, Outer(true, Style::default().fg(Tui::g(96))).enclose(
Fixed::xy(20, 1 + outs, FieldV(theme, format!("MIDI outs: "),
Map::south(1, ||track.sequencer.midi_outs.iter(),
|port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))})
}
fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Content<TuiOut> {
self.track().and_then(|track|track.devices.get(0)).map(|device|{
let ins = device.audio_ins().len() as u16;
Fixed::xy(20, 1 + ins, Outer(true, Style::default().fg(Tui::g(96))).enclose(
Fixed::xy(20, 1 + ins, FieldV(theme, format!("Audio ins: "),
Map::south(1, ||device.audio_ins().iter(),
|port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))})
}
fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Content<TuiOut> {
self.track().and_then(|track|track.devices.last()).map(|device|{
let outs = device.audio_outs().len() as u16;
Fixed::xy(20, 1 + outs, Outer(true, Style::default().fg(Tui::g(96))).enclose(
Fixed::xy(20, 1 + outs, FieldV(theme, format!("Audio outs: "),
Map::south(1, ||device.audio_outs().iter(),
|port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))})
}
}
//impl<T: Has<Option<Track>>> HasTrack for T {
//fn track (&self) -> Option<&Track> {
//self.get().as_ref()
//}
//fn track_mut (&mut self) -> Option<&mut Track> {
//self.get_mut().as_mut()
//}
//}

View file

@ -73,143 +73,257 @@ pub trait HasTrackScroll: HasTracks {
impl HasTrackScroll for Arrangement {
fn track_scroll (&self) -> usize { self.track_scroll }
}
impl<'a> HasTrackScroll for ArrangerView<'a> {
fn track_scroll (&self) -> usize { self.arrangement.track_scroll() }
}
impl<T: HasTrackScroll + HasSelection> TracksView for T {}
pub trait TracksView: HasTrackScroll + HasSelection {
fn view_track_names (&self, theme: ItemTheme) -> impl Content<TuiOut> {
let mut max_outputs = 0u16;
for track in self.tracks().iter() {
max_outputs = max_outputs.max(track.sequencer.midi_outs.len() as u16);
}
Bsp::w(
Fixed::x(20, Tui::bg(theme.darkest.rgb,
col!(Tui::bold(true, "[t]rack"), "[T] Add"))),
Align::w(Fixed::y(max_outputs + 1, Tui::bg(theme.darker.rgb,
Align::w(Fill::x(Map::new(
||self.tracks_with_sizes(&self.selection(), None).skip(self.track_scroll()),
move|(index, track, x1, x2): (usize, &Track, usize, usize), _|
Push::x(index as u16 * 14, Fixed::xy(track.width as u16, max_outputs + 1,
Tui::bg(track.color.dark.rgb, Align::nw(Bsp::s(Tui::fg(
Rgb(255, 255, 255),
Tui::bold(true, format!("{}", track.name))
), format!("{index} {x1} {x2}")))))))))))))
impl<T: MaybeHas<Track>> HasTrack for T {
fn track (&self) -> Option<&Track> {
self.get()
}
fn view_track_outputs <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> {
let mut max_outputs = 0u16;
for track in self.tracks().iter() {
max_outputs = max_outputs.max(track.sequencer.midi_outs.len() as u16);
}
Bsp::w(
Fixed::x(20, Tui::bg(theme.darkest.rgb,
col!(Tui::bold(true, "[o]utput"), "[O] Add"))),
Align::w(Fixed::y(max_outputs + 1, Tui::bg(theme.darker.rgb, Align::w(Fill::x(Map::new(
||self.tracks_with_sizes(&self.selection(), None).skip(self.track_scroll()),
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|
Push::x(x2 as u16, Tui::bg(track.color.dark.rgb, Fixed::xy(
track.width as u16,
max_outputs + 1,
Align::nw(Bsp::s(
format!("[mut] [sol]"),
Map::south(1, ||track.sequencer.midi_outs.iter(),
|port, index|Tui::fg(Rgb(255, 255, 255),
format!("{index}: {}", port.name())))))))))))))))
}
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> {
let mut max_inputs = 0u16;
for track in self.tracks().iter() {
max_inputs = max_inputs.max(track.sequencer.midi_ins.len() as u16);
}
Bsp::w(
Fixed::x(20, Tui::bg(theme.darkest.rgb,
col!(Tui::bold(true, "[i]nputs"), "[I] Add"))),
Fill::x(Align::w(Fixed::y(max_inputs + 1, Tui::bg(theme.darker.rgb, Align::w(Fill::x(Map::new(
||self.tracks_with_sizes(&self.selection(), None).skip(self.track_scroll()),
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|
Push::x(x2 as u16, Fixed::xy(track.width as u16, max_inputs + 1,
Tui::bg(track.color.dark.rgb, Align::nw(Bsp::s(
format!("[rec] [mon]"),
Map::south(1, ||track.sequencer.midi_ins.iter(),
|port, index|Tui::fg(Rgb(255, 255, 255),
format!("{index}: {}", port.name()))))))))))))))))
}
fn view_track_devices <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> {
let mut max_devices = 2u16;
for track in self.tracks().iter() {
max_devices = max_devices.max(track.devices.len() as u16);
}
Bsp::w(
Fixed::x(20, Tui::bg(theme.darkest.rgb,
col!(Tui::bold(true, "[d]evice"), "[D] Add"))),
Fixed::y(max_devices, Tui::bg(theme.darker.rgb, Align::w(Fill::x(Map::new(
||self.tracks_with_sizes(&self.selection(), None).skip(self.track_scroll()),
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|
Push::x(x2 as u16, Fixed::xy(track.width as u16, max_devices + 1,
Tui::bg(track.color.dark.rgb, Align::nw(Map::south(1, move||0..max_devices,
|_, index|format!("{index}: {}", "--------"))))))))))))
fn track_mut (&mut self) -> Option<&mut Track> {
self.get_mut()
}
}
impl Arrangement {
/// Add multiple tracks
pub fn tracks_add (
&mut self,
count: usize,
width: Option<usize>,
mins: &[PortConnect],
mouts: &[PortConnect],
) -> Usually<()> {
let jack = self.jack().clone();
let track_color_1 = ItemColor::random();
let track_color_2 = ItemColor::random();
for i in 0..count {
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
let mut track = self.track_add(None, Some(color), mins, mouts)?.1;
if let Some(width) = width {
track.width = width;
}
}
Ok(())
}
#[tengri_proc::expose]
impl Track {
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
fn _todo_usize_stub_ (&self) -> usize { todo!() }
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
}
/// Add a track
pub fn track_add (
&mut self,
name: Option<&str>,
color: Option<ItemTheme>,
mins: &[PortConnect],
mouts: &[PortConnect],
) -> Usually<(usize, &mut Track)> {
self.track_last += 1;
let name: Arc<str> = name.map_or_else(
||format!("Track{:02}", self.track_last).into(),
|x|x.to_string().into()
);
let mut track = Track {
width: (name.len() + 2).max(12),
color: color.unwrap_or_else(ItemTheme::random),
#[tengri_proc::command(Track)]
impl TrackCommand {
fn set_name (track: &mut Track, mut name: Arc<str>) -> Perhaps<Self> {
std::mem::swap(&mut name, &mut track.name);
Ok(Some(Self::SetName { name }))
}
fn set_color (track: &mut Track, mut color: ItemTheme) -> Perhaps<Self> {
std::mem::swap(&mut color, &mut track.color);
Ok(Some(Self::SetColor { color }))
}
fn set_mute (track: &mut Track, value: Option<bool>) -> Perhaps<Self> {
todo!()
}
fn set_solo (track: &mut Track, value: Option<bool>) -> Perhaps<Self> {
todo!()
}
fn set_rec (track: &mut Track, value: Option<bool>) -> Perhaps<Self> {
let current = track.sequencer.recording;
let value = value.unwrap_or(!current);
Ok((value != current).then_some(Self::SetRec { value: Some(current) }))
}
fn set_mon (track: &mut Track, value: Option<bool>) -> Perhaps<Self> {
let current = track.sequencer.monitoring;
let value = value.unwrap_or(!current);
Ok((value != current).then_some(Self::SetRec { value: Some(current) }))
}
fn set_size (track: &mut Track, size: usize) -> Perhaps<Self> {
todo!()
}
fn set_zoom (track: &mut Track, zoom: usize) -> Perhaps<Self> {
todo!()
}
fn stop (track: &mut Track) -> Perhaps<Self> {
track.sequencer.enqueue_next(None);
Ok(None)
}
}
#[derive(Debug, Default)]
pub struct Track {
/// Name of track
pub name: Arc<str>,
/// Identifying color of track
pub color: ItemTheme,
/// Preferred width of track column
pub width: usize,
/// MIDI sequencer state
pub sequencer: Sequencer,
/// Device chain
pub devices: Vec<Device>,
}
has!(Clock: |self: Track|self.sequencer.clock);
has!(Sequencer: |self: Track|self.sequencer);
impl Track {
/// Create a new track with only the default [Sequencer].
pub fn new (
name: &impl AsRef<str>,
color: Option<ItemTheme>,
jack: &Jack,
clock: Option<&Clock>,
clip: Option<&Arc<RwLock<MidiClip>>>,
midi_from: &[PortConnect],
midi_to: &[PortConnect],
) -> Usually<Self> {
Ok(Self {
name: name.as_ref().into(),
color: color.unwrap_or_default(),
sequencer: Sequencer::new(
&format!("{name}"),
self.jack(),
Some(self.clock()),
None,
mins,
mouts
format!("{}/sequencer", name.as_ref()),
jack,
clock,
clip,
midi_from,
midi_to
)?,
name,
..Default::default()
};
self.tracks_mut().push(track);
let len = self.tracks().len();
let index = len - 1;
for scene in self.scenes_mut().iter_mut() {
while scene.clips.len() < len {
scene.clips.push(None);
})
}
/// Create a new track connecting the [Sequencer] to a [Sampler].
pub fn new_with_sampler (
name: &impl AsRef<str>,
color: Option<ItemTheme>,
jack: &Jack,
clock: Option<&Clock>,
clip: Option<&Arc<RwLock<MidiClip>>>,
midi_from: &[PortConnect],
midi_to: &[PortConnect],
audio_from: &[&[PortConnect];2],
audio_to: &[&[PortConnect];2],
) -> Usually<Self> {
let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?;
track.devices.push(Device::Sampler(Sampler::new(
jack,
&format!("{}/sampler", name.as_ref()),
&[PortConnect::exact(format!("{}:{}",
jack.with_client(|c|c.name().to_string()),
track.sequencer.midi_outs[0].name()
))],
audio_from,
audio_to
)?));
Ok(track)
}
#[cfg(feature = "sampler")]
pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> {
for device in self.devices.iter() {
match device {
Device::Sampler(s) => if nth == 0 {
return Some(s);
} else {
nth -= 1;
},
_ => {}
}
}
Ok((index, &mut self.tracks_mut()[index]))
None
}
#[cfg(feature = "sampler")]
pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> {
for device in self.devices.iter_mut() {
match device {
Device::Sampler(s) => if nth == 0 {
return Some(s);
} else {
nth -= 1;
},
_ => {}
}
}
None
}
}
pub trait HasTrack {
fn track (&self) -> Option<&Track>;
fn track_mut (&mut self) -> Option<&mut Track>;
fn view_midi_ins_status (&self, theme: ItemTheme) -> impl Content<TuiOut> {
self.track().map(|track|{
let ins = track.sequencer.midi_ins.len() as u16;
Fixed::xy(20, 1 + ins, Outer(true, Style::default().fg(Tui::g(96))).enclose(
Fixed::xy(20, 1 + ins, FieldV(theme, format!("MIDI ins: "),
Map::south(1, ||track.sequencer.midi_ins.iter(),
|port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))})
}
fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Content<TuiOut> {
self.track().map(|track|{
let outs = track.sequencer.midi_outs.len() as u16;
Fixed::xy(20, 1 + outs, Outer(true, Style::default().fg(Tui::g(96))).enclose(
Fixed::xy(20, 1 + outs, FieldV(theme, format!("MIDI outs: "),
Map::south(1, ||track.sequencer.midi_outs.iter(),
|port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))})
}
fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Content<TuiOut> {
self.track().and_then(|track|track.devices.get(0)).map(|device|{
let ins = device.audio_ins().len() as u16;
Fixed::xy(20, 1 + ins, Outer(true, Style::default().fg(Tui::g(96))).enclose(
Fixed::xy(20, 1 + ins, FieldV(theme, format!("Audio ins: "),
Map::south(1, ||device.audio_ins().iter(),
|port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))})
}
fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Content<TuiOut> {
self.track().and_then(|track|track.devices.last()).map(|device|{
let outs = device.audio_outs().len() as u16;
Fixed::xy(20, 1 + outs, Outer(true, Style::default().fg(Tui::g(96))).enclose(
Fixed::xy(20, 1 + outs, FieldV(theme, format!("Audio outs: "),
Map::south(1, ||device.audio_outs().iter(),
|port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))})
}
}
impl Track {
pub fn per <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
Map::new(tracks,
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
let width = (x2 - x1) as u16;
map_east(x1 as u16, width, Fixed::x(width, Tui::fg_bg(
track.color.lightest.rgb,
track.color.base.rgb,
callback(index, track))))})
}
}
pub(crate) fn per_track_top <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
Align::x(Tui::bg(Reset, Map::new(tracks,
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
let width = (x2 - x1) as u16;
map_east(x1 as u16, width, Fixed::x(width, Tui::fg_bg(
track.color.lightest.rgb,
track.color.base.rgb,
callback(index, track))))})))
}
pub(crate) fn per_track <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
per_track_top(tracks, move|index, track|Fill::y(Align::y(callback(index, track))))
}
pub(crate) fn io_ports <'a, T: PortsSizes<'a>> (
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
Map::new(iter, move|(
index, name, connections, y, y2
): (usize, &'a Arc<str>, &'a [PortConnect], usize, usize), _|
map_south(y as u16, (y2-y) as u16, Bsp::s(
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(" 󰣲 ", name))))),
Map::new(||connections.iter(), move|connect: &'a PortConnect, index|map_south(index as u16, 1,
Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg,
&connect.info)))))))))
}
pub(crate) fn io_conns <'a, T: PortsSizes<'a>> (
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
Map::new(iter, move|(
index, name, connections, y, y2
): (usize, &'a Arc<str>, &'a [PortConnect], usize, usize), _|
map_south(y as u16, (y2-y) as u16, Bsp::s(
Fill::x(Tui::bold(true, wrap(bg, fg, Fill::x(Align::w("▞▞▞▞ ▞▞▞▞"))))),
Map::new(||connections.iter(), move|connect, index|map_south(index as u16, 1,
Fill::x(Align::w(Tui::bold(false, wrap(bg, fg, Fill::x(""))))))))))
}
//track_scroll: Fill::x(Fixed::y(1, ScrollbarH {
//offset: arrangement.track_scroll,
//length: h_tracks_area as usize,
//total: h_scenes as usize,
//})),

View file

@ -1,110 +1,159 @@
use crate::*;
pub struct ArrangerView<'a> {
pub arrangement: &'a Arrangement,
pub is_editing: bool,
pub width: u16,
pub width_mid: u16,
pub width_side: u16,
pub scene_last: usize,
pub scene_scroll: Fill<Fixed<u16, ScrollbarV>>,
pub scene_selected: Option<usize>,
/// Height available to display scene/track content.
pub scenes_height: u16,
pub track_scroll: Fill<Fixed<u16, ScrollbarH>>,
pub track_selected: Option<usize>,
/// Height available to display track headers.
pub tracks_height: u16,
}
impl<'a> Has<Vec<Scene>> for ArrangerView<'a> {
fn get (&self) -> &Vec<Scene> {
&self.arrangement.scenes
}
fn get_mut (&mut self) -> &mut Vec<Scene> {
unreachable!()
}
}
impl<'a> Has<Vec<Track>> for ArrangerView<'a> {
fn get (&self) -> &Vec<Track> {
&self.arrangement.tracks
}
fn get_mut (&mut self) -> &mut Vec<Track> {
unreachable!()
}
}
impl<'a> ArrangerView<'a> {
pub fn new (
arrangement: &'a Arrangement,
editor: Option<&'a MidiEditor>
) -> Self {
let is_editing = editor.is_some();
let h_tracks_area = 5;
let h_scenes_area = (arrangement.height() as u16).saturating_sub(20);
let h_scenes = arrangement.h_scenes(is_editing);
Self {
arrangement,
is_editing,
width: arrangement.w_tracks_area(is_editing),
width_mid: arrangement.w_tracks_area(is_editing).saturating_sub(20),
width_side: 20,
scenes_height: h_scenes_area,
scene_selected: arrangement.selection().scene(),
scene_last: arrangement.scenes.len().saturating_sub(1),
scene_scroll: Fill::y(Fixed::x(1, ScrollbarV {
offset: arrangement.scene_scroll,
length: h_scenes_area as usize,
total: h_scenes as usize,
})),
tracks_height: h_tracks_area,
track_selected: arrangement.selection().track(),
track_scroll: Fill::x(Fixed::y(1, ScrollbarH {
offset: arrangement.track_scroll,
length: h_tracks_area as usize,
total: h_scenes as usize,
})),
}
}
}
impl<'a> Content<TuiOut> for ArrangerView<'a> {
impl Content<TuiOut> for Arrangement {
fn content (&self) -> impl Render<TuiOut> {
let ins = |x|Bsp::n(self.inputs(), x);
let tracks = |x|Bsp::s(self.tracks(), x);
let devices = |x|Bsp::s(self.devices(), x);
let outs = |x|Bsp::s(self.outputs(), x);
let ins = |x|Bsp::n(self.view_inputs_0(), x);
let tracks = |x|Bsp::s(self.view_tracks_0(), x);
let devices = |x|Bsp::s(self.view_devices_0(), x);
let outs = |x|Bsp::s(self.view_outputs_0(), x);
let bg = |x|Tui::bg(Reset, x);
//let track_scroll = |x|Bsp::s(&self.track_scroll, x);
//let scene_scroll = |x|Bsp::e(&self.scene_scroll, x);
self.arrangement.size.of(outs(tracks(devices(ins(bg(self.scenes_view(&None)))))))
self.size.of(outs(tracks(devices(ins(bg(self.view_scenes_clips(&None)))))))
}
}
impl<'a> ArrangerView<'a> {
impl Arrangement {
/// Render input matrix.
pub(crate) fn inputs (&'a self) -> impl Content<TuiOut> + 'a {
fn view_inputs_0 (&self) -> impl Content<TuiOut> + '_ {
Tui::bg(Reset, Bsp::s(
self.input_intos(),
Bsp::s(self.input_routes(), self.input_ports()),
self.view_input_intos(),
Bsp::s(self.view_input_routes(), self.view_input_ports()),
))
}
fn view_input_ports (&self) -> impl Content<TuiOut> + '_ {
let is_editing = false; //FIXME
Tryptich::top(1)
.left(20, button_3("i", "midi ins", format!("{}",
self.midi_ins().len()), is_editing))
.right(20, button_2("I", "add midi in", is_editing))
.middle(self.width().saturating_sub(40) as u16,
per_track_top(||self.tracks_with_sizes_scrolled(),
move|t, track|{
let rec = track.sequencer.recording;
let mon = track.sequencer.monitoring;
let rec = if rec { White } else { track.color.darkest.rgb };
let mon = if mon { White } else { track.color.darkest.rgb };
let bg = if self.track_selected() == Some(t) {
track.color.light.rgb
} else {
track.color.base.rgb
};
//let bg2 = if t > 0 { track.color.base.rgb } else { Reset };
wrap(bg, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(
Tui::fg_bg(rec, bg, "Rec "),
Tui::fg_bg(mon, bg, "Mon "),
))))
}))
}
fn view_input_routes (&self) -> impl Content<TuiOut> + '_ {
Tryptich::top(self.h_inputs())
.left(self.width_side(),
io_ports(Tui::g(224), Tui::g(32), ||self.midi_ins_with_sizes()))
.middle(self.width_mid(),
per_track_top(||self.tracks_with_sizes_scrolled(),
move|_, &Track { color, .. }|io_conns(
color.dark.rgb, color.darker.rgb, ||self.midi_ins_with_sizes())))
}
fn view_input_intos (&self) -> impl Content<TuiOut> + '_ {
Tryptich::top(2)
.left(self.width_side(),
Bsp::s(Align::e("Input:"), Align::e("Into clip:")))
.middle(self.width_mid(),
per_track_top(||self.tracks_with_sizes_scrolled(),
|_, _|Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ ")))))
}
/// Render output matrix.
pub(crate) fn outputs (&'a self) -> impl Content<TuiOut> + 'a {
fn view_outputs_0 (&self) -> impl Content<TuiOut> + '_ {
Tui::bg(Reset, Align::n(Bsp::s(
Bsp::s(self.output_ports(), self.output_conns()),
Bsp::s(self.output_nexts(), self.output_froms()),
Bsp::s(self.view_output_ports(), self.view_output_conns()),
Bsp::s(self.view_output_nexts(), self.view_output_froms()),
)))
}
fn view_output_ports (&self) -> impl Content<TuiOut> + '_ {
Tryptich::top(1)
.left(self.width_side(), self.view_output_count())
.right(self.width_side(), self.view_output_add())
.middle(self.width_mid(), self.view_output_map())
}
fn view_output_count (&self) -> impl Content<TuiOut> {
button_3(
"o",
"midi outs",
format!("{}", self.midi_outs().len()),
false // self.is_editing()
)
}
fn view_output_add (&self) -> impl Content<TuiOut> {
button_2("O", "add midi out", false /* is_editing */)
}
fn view_output_map (&self) -> impl Content<TuiOut> + '_ {
per_track_top(||self.tracks_with_sizes_scrolled(), move|i, t|{
let mute = false;
let solo = false;
let mute = if mute { White } else { t.color.darkest.rgb };
let solo = if solo { White } else { t.color.darkest.rgb };
let bg_1 = if self.track_selected() == Some(i) {
t.color.light.rgb
} else {
t.color.base.rgb
};
let bg_2 = if i > 0 { t.color.base.rgb } else { Reset };
let mute = Tui::fg_bg(mute, bg_1, "Play ");
let solo = Tui::fg_bg(solo, bg_1, "Solo ");
wrap(bg_1, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(mute, solo))))
})
}
fn view_output_conns (&self) -> impl Content<TuiOut> + '_ {
Tryptich::top(self.h_outputs())
.left(self.width_side(), io_ports(
Tui::g(224), Tui::g(32), ||self.midi_outs_with_sizes()))
.middle(self.width_mid(), per_track_top(||self.tracks_with_sizes_scrolled(),
|_, t|io_conns(
t.color.dark.rgb,
t.color.darker.rgb,
||self.midi_outs_with_sizes()
)))
}
fn view_output_nexts (&self) -> impl Content<TuiOut> + '_ {
Tryptich::top(2).left(self.width_side(), Align::ne("From clip:"))
.middle(self.width_mid(), per_track_top(||self.tracks_with_sizes_scrolled(),
|_, _|Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default())))))
}
fn view_output_froms (&self) -> impl Content<TuiOut> + '_ {
let label = Align::ne("Next clip:");
Tryptich::top(2).left(self.width_side(), label)
.middle(self.width_mid(), per_track_top(
||self.tracks_with_sizes_scrolled(), |t, track|{
let queued = track.sequencer.next_clip.is_some();
let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ "));
let queued_clip = Thunk::new(||{
Tui::bg(Reset, if let Some((_, clip)) = track.sequencer.next_clip.as_ref() {
if let Some(clip) = clip {
clip.read().unwrap().name.clone()
} else {
"Stop".into()
}
} else {
"".into()
})
});
Either(queued, queued_clip, queued_blank)
}))
}
/// Render track headers
pub(crate) fn tracks (&'a self) -> impl Content<TuiOut> + 'a {
let Self { width_side, width_mid, track_selected, is_editing, .. } = self;
fn view_tracks_0 (&self) -> impl Content<TuiOut> + '_ {
let width_side = self.width_side();
let width_mid = self.width_mid();
let is_editing = false; // FIXME
let track_selected = self.track_selected();
Tryptich::center(3)
.left(*width_side,
button_3("t", "track", format!("{}", self.arrangement.tracks.len()), *is_editing))
.right(*width_side, button_2("T", "add track", *is_editing))
.middle(*width_mid, per_track(||self.tracks_with_sizes_scrolled(),
|index, track|wrap(
if *track_selected == Some(index) {
.left(width_side,
button_3("t", "track", format!("{}", self.tracks().len()), is_editing))
.right(width_side, button_2("T", "add track", is_editing))
.middle(width_mid, per_track(||self.tracks_with_sizes_scrolled(),
move|index, track|wrap(
if track_selected == Some(index) {
track.color.light
} else {
track.color.base
@ -114,14 +163,17 @@ impl<'a> ArrangerView<'a> {
)))
}
/// Render device switches.
pub(crate) fn devices (&'a self) -> impl Content<TuiOut> + 'a {
let Self { width_side, width_mid, track_selected, is_editing, .. } = self;
fn view_devices_0 (&self) -> impl Content<TuiOut> + '_ {
let width_side = self.width_side();
let width_mid = self.width_mid();
let is_editing = false; // FIXME
let track_selected = self.track_selected();
Tryptich::top(1)
.left(*width_side, button_3("d", "devices", format!("{}", 0), *is_editing))
.right(*width_side, button_2("D", "add device", *is_editing))
.middle(*width_mid, per_track_top(||self.tracks_with_sizes_scrolled(),
.left(width_side, button_3("d", "devices", format!("{}", 0), is_editing))
.right(width_side, button_2("D", "add device", is_editing))
.middle(width_mid, per_track_top(||self.tracks_with_sizes_scrolled(),
move|index, track|{
let bg = if *track_selected == Some(index) {
let bg = if track_selected == Some(index) {
track.color.light
} else {
track.color.base
@ -132,22 +184,285 @@ impl<'a> ArrangerView<'a> {
}
}
pub(crate) fn per_track_top <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
Align::x(Tui::bg(Reset, Map::new(tracks,
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
let width = (x2 - x1) as u16;
map_east(x1 as u16, width, Fixed::x(width, Tui::fg_bg(
track.color.lightest.rgb,
track.color.base.rgb,
callback(index, track))))})))
impl<T> TracksView for T
where T: HasSize<TuiOut> + HasTrackScroll + HasSelection + HasMidiIns {}
impl ClipsView for Arrangement {}
pub trait TracksView: HasSize<TuiOut> + HasTrackScroll + HasSelection + HasMidiIns {
fn is_editing (&self) -> bool { false }
fn tracks_width_available (&self) -> u16 {
(self.width() as u16).saturating_sub(40)
}
fn tracks_with_sizes_scrolled <'t> (&'t self) -> impl TracksSizes<'t> {
self.tracks_with_sizes(&self.selection(), self.is_editing().then_some(20/*FIXME*/))
.map_while(move|(t, track, x1, x2)|
((x2 as u16) < self.tracks_width_available())
.then_some((t, track, x1, x2)))
}
fn view_track_names (&self, theme: ItemTheme) -> impl Content<TuiOut> {
let mut max_outputs = 0u16;
for track in self.tracks().iter() {
max_outputs = max_outputs.max(track.sequencer.midi_outs.len() as u16);
}
Bsp::w(
Fixed::x(20, Tui::bg(theme.darkest.rgb,
col!(Tui::bold(true, "[t]rack"), "[T] Add"))),
Align::w(Fixed::y(max_outputs + 1, Tui::bg(theme.darker.rgb,
Align::w(Fill::x(Map::new(
||self.tracks_with_sizes(&self.selection(), None).skip(self.track_scroll()),
move|(index, track, x1, x2): (usize, &Track, usize, usize), _|
Push::x(index as u16 * 14, Fixed::xy(track.width as u16, max_outputs + 1,
Tui::bg(track.color.dark.rgb, Align::nw(Bsp::s(Tui::fg(
Rgb(255, 255, 255),
Tui::bold(true, format!("{}", track.name))
), format!("{index} {x1} {x2}")))))))))))))
}
fn view_track_outputs <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> {
let mut max_outputs = 0u16;
for track in self.tracks().iter() {
max_outputs = max_outputs.max(track.sequencer.midi_outs.len() as u16);
}
Bsp::w(
Fixed::x(20, Tui::bg(theme.darkest.rgb,
col!(Tui::bold(true, "[o]utput"), "[O] Add"))),
Align::w(Fixed::y(max_outputs + 1, Tui::bg(theme.darker.rgb, Align::w(Fill::x(Map::new(
||self.tracks_with_sizes(&self.selection(), None).skip(self.track_scroll()),
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|
Push::x(x2 as u16, Tui::bg(track.color.dark.rgb, Fixed::xy(
track.width as u16,
max_outputs + 1,
Align::nw(Bsp::s(
format!("[mut] [sol]"),
Map::south(1, ||track.sequencer.midi_outs.iter(),
|port, index|Tui::fg(Rgb(255, 255, 255),
format!("{index}: {}", port.name())))))))))))))))
}
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> {
let mut max_inputs = 0u16;
for track in self.tracks().iter() {
max_inputs = max_inputs.max(track.sequencer.midi_ins.len() as u16);
}
Bsp::w(
Fixed::x(20, Tui::bg(theme.darkest.rgb,
col!(Tui::bold(true, "[i]nputs"), "[I] Add"))),
Fill::x(Align::w(Fixed::y(max_inputs + 1, Tui::bg(theme.darker.rgb, Align::w(Fill::x(Map::new(
||self.tracks_with_sizes(&self.selection(), None).skip(self.track_scroll()),
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|
Push::x(x2 as u16, Fixed::xy(track.width as u16, max_inputs + 1,
Tui::bg(track.color.dark.rgb, Align::nw(Bsp::s(
format!("[rec] [mon]"),
Map::south(1, ||track.sequencer.midi_ins.iter(),
|port, index|Tui::fg(Rgb(255, 255, 255),
format!("{index}: {}", port.name()))))))))))))))))
}
fn view_track_devices <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> {
let mut max_devices = 2u16;
for track in self.tracks().iter() {
max_devices = max_devices.max(track.devices.len() as u16);
}
Bsp::w(
Fixed::x(20, Tui::bg(theme.darkest.rgb,
col!(Tui::bold(true, "[d]evice"), "[D] Add"))),
Fixed::y(max_devices, Tui::bg(theme.darker.rgb, Align::w(Fill::x(Map::new(
||self.tracks_with_sizes(&self.selection(), None).skip(self.track_scroll()),
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|
Push::x(x2 as u16, Fixed::xy(track.width as u16, max_devices + 1,
Tui::bg(track.color.dark.rgb, Align::nw(Map::south(1, move||0..max_devices,
|_, index|format!("{index}: {}", "--------"))))))))))))
}
}
pub(crate) fn per_track <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
per_track_top(tracks, move|index, track|Fill::y(Align::y(callback(index, track))))
pub trait ScenesView: HasSelection + HasSceneScroll + Send + Sync {
/// Default scene height.
const H_SCENE: usize = 2;
/// Default editor height.
const H_EDITOR: usize = 15;
fn arrangement (&self) -> &Arrangement;
fn scene_selected (&self) -> Option<usize>;
fn track_selected (&self) -> Option<usize>;
fn scenes_height (&self) -> u16;
fn width_side (&self) -> u16;
fn width_mid (&self) -> u16;
fn view_scenes_names (&self) -> impl Content<TuiOut> {
let h = self.scenes_with_prev_color().last().map(|(_,_,_,h,_)|h as u16).unwrap_or(0);
Fixed::y(h, Map::new(move||self.scenes_with_prev_color(),
move|(s, scene, y1, y2, previous): SceneWith<'_, Option<ItemTheme>>, _|{
let height = (1 + y2 - y1) as u16;
let name = Some(scene.name.clone());
let content = Fill::x(Align::w(Tui::bold(true, Bsp::e("", name))));
let selected = self.scene_selected() == Some(s);
let neighbor = s > 0 && self.scene_selected() == Some(s - 1);
let is_last = self.scenes().len().saturating_sub(1) == s;
let theme = scene.color;
let fg = theme.lightest.rgb;
let bg = if selected { theme.light } else { theme.base }.rgb;
let hi = if let Some(previous) = previous {
if neighbor { previous.light.rgb } else { previous.base.rgb }
} else {
Reset
};
let lo = if is_last { Reset } else if selected {
theme.light.rgb
} else {
theme.base.rgb
};
Fill::x(map_south(y1 as u16, height, Fixed::y(height, Phat {
width: 0, height: 0, content, colors: [fg, bg, hi, lo]
})))
}))
}
fn scenes_names_2 (&self, theme: ItemTheme) -> impl Content<TuiOut> {
let h = self.scenes().len() as u16 * 2;
let bg = theme.darker.rgb;
Fixed::y(h, Tui::bg(bg, Align::w(Fill::x(Map::new(
||self.scenes().iter().skip(self.scene_scroll()),
move|scene: &Scene, index|
Push::y(index as u16 * 2u16, Fixed::xy(20, 2,
Tui::bg(scene.color.dark.rgb, Align::nw(Bsp::e(
format!(" {index:2} "),
Tui::fg(Rgb(255, 255, 255),
Tui::bold(true, format!("{}", scene.name)))))))))))))
}
fn scenes_with_prev_color (&self) -> impl Iterator<Item=SceneWith<Option<ItemTheme>>> + Send + Sync {
self.scenes_iter().map(|(s, scene, y1, y2)|(s, scene, y1, y2,
(s>0).then(||self.arrangement().scenes()[s-1].color)))
}
fn per_track <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
Map::new(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
Tui::fg_bg(track.color.lightest.rgb, track.color.darker.rgb,
map_east(x1 as u16, (x2 - x1) as u16, callback(index, track))) })
}
fn scenes_with_clip (&self, track_index: usize) -> impl Iterator<Item=SceneWith<'_, Option<ItemTheme>>> + Send + Sync {
self.scenes_iter().map(move|(s, scene, y1, y2)|(s, scene, y1, y2,
(s>0).then(||self.arrangement().scenes()[s-1].clips[track_index].as_ref()
.map(|c|c.read().unwrap().color)
.unwrap_or(ItemTheme::G[32]))))
}
/// A scene with size and color.
fn scenes_iter (&self) -> impl Iterator<Item=(usize, &Scene, usize, usize,)> + Send + Sync {
let selection = Has::<Selection>::get(self.arrangement());
self.arrangement().scenes_with_sizes(
false, // FIXME self.is_editing(),
Self::H_SCENE, Self::H_EDITOR,
selection.track(), selection.scene(),
).map_while(|(s, scene, y1, y2)|(y2<=self.scenes_height() as usize)
.then_some((s, scene, y1, y2)))
}
/// Height required to display all scenes.
fn scenes_height_total (&self, is_editing: bool) -> u16 {
self.scenes_with_sizes(
is_editing,
Self::H_SCENE,
Self::H_EDITOR,
self.selection().track(),
self.selection().scene(),
)
.last()
.map(|(_, _, _, y)|y as u16).unwrap_or(0)
}
}
pub trait ClipsView: TracksView + ScenesView + Send + Sync {
fn view_scenes_clips <'a> (&'a self, editor: &'a Option<MidiEditor>)
-> impl Content<TuiOut> + 'a
{
let is_editing = false; //FIXME
let h = self.scenes_with_prev_color().last().map(|(_,_,_,h,_)|h as u16).unwrap_or(0);
Fixed::y(h, Self::per_track(||self.tracks_with_sizes_scrolled(),
move|track_index, track|Map::new(move||self.scenes_with_clip(track_index),
move|(s, scene, y1, y2, previous): SceneWith<'_, Option<ItemTheme>>, _|{
let (name, theme) = if let Some(clip) = &scene.clips[track_index] {
let clip = clip.read().unwrap();
(Some(clip.name.clone()), clip.color)
} else {
(None, ItemTheme::G[32])
};
let content = Fill::x(Align::w(Tui::bold(true, Bsp::e("", name))));
let same_track = self.track_selected() == Some(track_index);
let selected = same_track && self.scene_selected() == Some(s);
let neighbor = same_track && s > 0 && self.scene_selected() == Some(s - 1);
let is_last = self.scenes().len().saturating_sub(1) == s;
let fg = theme.lightest.rgb;
let bg = if selected { theme.light } else { theme.base }.rgb;
let hi = if let Some(previous) = previous {
if neighbor { previous.light.rgb } else { previous.base.rgb }
} else {
Reset
};
let lo = if is_last {
Reset
} else if selected {
theme.light.rgb
} else {
theme.base.rgb
};
let height = (1 + y2 - y1) as u16;
map_south(y1 as u16, height, Bsp::b(Fixed::y(height, Phat {
width: 0, height: 0, content, colors: [fg, bg, hi, lo]
}), When(
is_editing && same_track && self.scene_selected() == Some(s),
editor
)))
})))
}
fn scenes_clips_2 <'a> (
&'a self,
theme: ItemTheme
) -> impl Content<TuiOut> + 'a {
Fixed::y(self.scenes().len() as u16 * 2, Tui::bg(theme.darker.rgb,
Align::w(Fill::x(Map::new(||self.scenes().iter().skip(self.scene_scroll()),
move|scene: &'a Scene, index|self.track_scenes(index, scene))))))
}
fn track_scenes <'a> (
&'a self,
scene_index: usize,
scene: &'a Scene
) -> impl Content<TuiOut> + 'a {
Push::y(scene_index as u16 * 2u16, Fixed::xy(20, 2, Map::new(
move||scene.clips.iter().skip(self.track_scroll()),
move|clip: &'a Option<Arc<RwLock<MidiClip>>>, track_index|
self.track_scene_clip(scene_index, scene, track_index, clip))))
}
fn track_scene_clip (
&self,
scene_index: usize,
scene: &Scene,
track_index: usize,
clip: &Option<Arc<RwLock<MidiClip>>>
) -> impl Content<TuiOut> {
let (theme, text) = if let Some(clip) = clip {
let clip = clip.read().unwrap();
(clip.color, clip.name.clone())
} else {
(scene.color, Default::default())
};
Push::x(track_index as u16 * 14, Tui::bg(theme.dark.rgb, Bsp::e(
format!(" {scene_index:2} {track_index:2} "),
Tui::fg(Rgb(255, 255, 255),
Tui::bold(true, format!("{}", text))))))
}
}
pub trait HasWidth {
const MIN_WIDTH: usize;
/// Increment track width.
fn width_inc (&mut self);
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
fn width_dec (&mut self);
}
impl HasWidth for Track {
const MIN_WIDTH: usize = 9;
fn width_inc (&mut self) {
self.width += 1;
}
fn width_dec (&mut self) {
if self.width > Track::MIN_WIDTH {
self.width -= 1;
}
}
}