mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-08-07 22:17:07 +02:00
1062 lines
40 KiB
Rust
1062 lines
40 KiB
Rust
use crate::*;
|
||
|
||
/// Arranger.
|
||
///
|
||
/// ```
|
||
/// let arranger = tek::Arrangement::default();
|
||
/// ```
|
||
#[derive(Default, Debug)]
|
||
pub struct Arrangement {
|
||
/// JACK client handle.
|
||
pub jack: Jack<'static>,
|
||
/// Project name.
|
||
pub name: Arc<str>,
|
||
/// Base color.
|
||
pub color: ItemTheme,
|
||
/// FIXME a render of the project arrangement, redrawn on update.
|
||
/// TODO rename to "render_cache" or smth
|
||
pub arranger: Arc<RwLock<Buffer>>,
|
||
/// Display size
|
||
pub size: Sizer,
|
||
/// Display size of clips area
|
||
pub size_inner: Sizer,
|
||
/// Selected UI element
|
||
pub selection: Selection,
|
||
/// Source of time
|
||
#[cfg(feature = "clock")] pub clock: Clock,
|
||
/// Allows one MIDI clip to be edited
|
||
#[cfg(feature = "editor")] pub editor: Option<MidiEditor>,
|
||
/// List of global midi inputs
|
||
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
||
/// List of global midi outputs
|
||
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
||
/// List of global audio inputs
|
||
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
||
/// List of global audio outputs
|
||
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
||
/// Last track number (to avoid duplicate port names)
|
||
#[cfg(feature = "track")] pub track_last: usize,
|
||
/// List of tracks
|
||
#[cfg(feature = "track")] pub tracks: Vec<Track>,
|
||
/// Scroll offset of tracks
|
||
#[cfg(feature = "track")] pub track_scroll: usize,
|
||
/// List of scenes
|
||
#[cfg(feature = "scene")] pub scenes: Vec<Scene>,
|
||
/// Scroll offset of scenes
|
||
#[cfg(feature = "scene")] pub scene_scroll: usize,
|
||
}
|
||
|
||
/// Represents the current user selection in the arranger
|
||
#[derive(PartialEq, Clone, Copy, Debug, Default)]
|
||
pub enum Selection {
|
||
#[default]
|
||
/// Nothing is selected
|
||
Nothing,
|
||
/// The whole mix is selected
|
||
Mix,
|
||
/// A MIDI input is selected.
|
||
Input(usize),
|
||
/// A MIDI output is selected.
|
||
Output(usize),
|
||
/// A scene is selected.
|
||
#[cfg(feature = "scene")] Scene(usize),
|
||
/// A track is selected.
|
||
#[cfg(feature = "track")] Track(usize),
|
||
/// A clip (track × scene) is selected.
|
||
#[cfg(feature = "track")] TrackClip { track: usize, scene: usize },
|
||
/// A track's MIDI input connection is selected.
|
||
#[cfg(feature = "track")] TrackInput { track: usize, port: usize },
|
||
/// A track's MIDI output connection is selected.
|
||
#[cfg(feature = "track")] TrackOutput { track: usize, port: usize },
|
||
/// A track device slot is selected.
|
||
#[cfg(feature = "track")] TrackDevice { track: usize, device: usize },
|
||
}
|
||
|
||
impl Arrangement {
|
||
|
||
/// Create a new arrangement.
|
||
pub fn new (
|
||
jack: &Jack<'static>,
|
||
name: Arc<str>,
|
||
clock: Clock,
|
||
tracks: impl Iterator<Item = Track>,
|
||
scenes: impl Iterator<Item = Scene>,
|
||
midi_ins: impl Iterator<Item = MidiInput>,
|
||
midi_outs: impl Iterator<Item = MidiOutput>,
|
||
audio_ins: impl Iterator<Item = AudioInput>,
|
||
audio_outs: impl Iterator<Item = AudioOutput>,
|
||
) -> Self {
|
||
Self {
|
||
jack: jack.clone(),
|
||
color: ItemTheme::random(),
|
||
selection: Selection::TrackClip { track: 0, scene: 0 },
|
||
tracks: tracks.collect(),
|
||
scenes: scenes.collect(),
|
||
midi_ins: midi_ins.collect(),
|
||
midi_outs: midi_outs.collect(),
|
||
audio_ins: audio_ins.collect(),
|
||
audio_outs: audio_outs.collect(),
|
||
clock,
|
||
name,
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
/// Width of display
|
||
pub fn w (&self) -> u16 {
|
||
self.size.w() as u16
|
||
}
|
||
|
||
/// Width allocated for sidebar.
|
||
pub fn w_sidebar (&self, is_editing: bool) -> u16 {
|
||
self.w() / if is_editing { 16 } else { 8 } as u16
|
||
}
|
||
|
||
/// Width available to display tracks.
|
||
pub fn w_tracks_area (&self, is_editing: bool) -> u16 {
|
||
self.w().saturating_sub(self.w_sidebar(is_editing))
|
||
}
|
||
|
||
/// Height of display
|
||
pub fn h (&self) -> u16 {
|
||
self.size.h() as u16
|
||
}
|
||
|
||
/// Height taken by visible device slots.
|
||
pub fn h_devices (&self) -> u16 {
|
||
2
|
||
//1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||
}
|
||
|
||
/// Get the first sampler of the active track
|
||
#[cfg(feature = "sampler")]
|
||
pub fn sampler (&self) -> Option<&Sampler> {
|
||
self.selected_track()?.sampler(0)
|
||
}
|
||
|
||
/// Get the first sampler of the active track
|
||
#[cfg(feature = "sampler")]
|
||
pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
|
||
self.selected_track_mut()?.sampler_mut(0)
|
||
}
|
||
|
||
}
|
||
|
||
impl Selection {
|
||
pub fn describe (
|
||
&self,
|
||
#[cfg(feature = "track")] tracks: &[Track],
|
||
#[cfg(feature = "scene")] scenes: &[Scene],
|
||
) -> Arc<str> {
|
||
use Selection::*;
|
||
format!("{}", match self {
|
||
Mix => "Everything".to_string(),
|
||
#[cfg(feature = "scene")] Scene(s) =>
|
||
scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()),
|
||
#[cfg(feature = "track")] Track(t) =>
|
||
tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()),
|
||
TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) {
|
||
(Some(_), Some(s)) => match s.clip(*track) {
|
||
Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name),
|
||
None => format!("T{track} S{scene}: Empty")
|
||
},
|
||
_ => format!("T{track} S{scene}: Empty"),
|
||
},
|
||
_ => todo!()
|
||
}).into()
|
||
}
|
||
#[cfg(feature = "scene")] pub fn scene (&self) -> Option<usize> {
|
||
use Selection::*;
|
||
match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None }
|
||
}
|
||
#[cfg(feature = "scene")] pub fn select_scene (&self, scene_count: usize) -> Self {
|
||
use Selection::*;
|
||
match self {
|
||
Mix | Track(_) => Scene(0),
|
||
Scene(s) => Scene((s + 1) % scene_count),
|
||
TrackClip { scene, .. } => Track(*scene),
|
||
_ => todo!(),
|
||
}
|
||
}
|
||
#[cfg(feature = "scene")] pub fn select_scene_next (&self, len: usize) -> Self {
|
||
use Selection::*;
|
||
match self {
|
||
Mix => Scene(0),
|
||
Track(t) => TrackClip { track: *t, scene: 0 },
|
||
Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix },
|
||
TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) },
|
||
_ => todo!()
|
||
}
|
||
}
|
||
#[cfg(feature = "scene")] pub fn select_scene_prev (&self) -> Self {
|
||
use Selection::*;
|
||
match self {
|
||
Mix | Scene(0) => Mix,
|
||
Scene(s) => Scene(s - 1),
|
||
Track(t) => Track(*t),
|
||
TrackClip { track, scene: 0 } => Track(*track),
|
||
TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 },
|
||
_ => todo!()
|
||
}
|
||
}
|
||
#[cfg(feature = "track")] pub fn track (&self) -> Option<usize> {
|
||
use Selection::*;
|
||
if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self {
|
||
Some(*track)
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
#[cfg(feature = "track")] pub fn select_track (&self, track_count: usize) -> Self {
|
||
use Selection::*;
|
||
match self {
|
||
Mix => Track(0),
|
||
Scene(_) => Mix,
|
||
Track(t) => Track((t + 1) % track_count),
|
||
TrackClip { track, .. } => Track(*track),
|
||
_ => todo!(),
|
||
}
|
||
}
|
||
#[cfg(feature = "track")] pub fn select_track_next (&self, len: usize) -> Self {
|
||
use Selection::*;
|
||
match self {
|
||
Mix => Track(0),
|
||
Scene(s) => TrackClip { track: 0, scene: *s },
|
||
Track(t) => if t + 1 < len { Track(t + 1) } else { Mix },
|
||
TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) },
|
||
_ => todo!()
|
||
}
|
||
}
|
||
#[cfg(feature = "track")] pub fn select_track_prev (&self) -> Self {
|
||
use Selection::*;
|
||
match self {
|
||
Mix => Mix,
|
||
Scene(s) => Scene(*s),
|
||
Track(0) => Mix,
|
||
Track(t) => Track(t - 1),
|
||
TrackClip { track: 0, scene } => Scene(*scene),
|
||
TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene },
|
||
_ => todo!()
|
||
}
|
||
}
|
||
}
|
||
|
||
impl <T: AsRef<Selection>+AsMut<Selection>> HasSelection for T {}
|
||
|
||
pub trait HasSelection: AsRef<Selection> + AsMut<Selection> {
|
||
fn selection (&self) -> &Selection { self.as_ref() }
|
||
fn selection_mut (&mut self) -> &mut Selection { self.as_mut() }
|
||
/// Get the active track
|
||
#[cfg(feature = "track")]
|
||
fn selected_track (&self) -> Option<&Track> where Self: HasTracks {
|
||
let index = self.selection().track()?;
|
||
self.tracks().get(index)
|
||
}
|
||
/// Get a mutable reference to the active track
|
||
#[cfg(feature = "track")]
|
||
fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks {
|
||
let index = self.selection().track()?;
|
||
self.tracks_mut().get_mut(index)
|
||
}
|
||
/// Get the active scene
|
||
#[cfg(feature = "scene")]
|
||
fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes {
|
||
let index = self.selection().scene()?;
|
||
self.scenes().get(index)
|
||
}
|
||
/// Get a mutable reference to the active scene
|
||
#[cfg(feature = "scene")]
|
||
fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes {
|
||
let index = self.selection().scene()?;
|
||
self.scenes_mut().get_mut(index)
|
||
}
|
||
/// Get the active clip
|
||
#[cfg(feature = "clip")]
|
||
fn selected_clip (&self) -> Option<Arc<RwLock<MidiClip>>> where Self: HasScenes + HasTracks {
|
||
self.selected_scene()?.clips.get(self.selection().track()?)?.clone()
|
||
}
|
||
}
|
||
|
||
impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||
impl_has!(Jack<'static>: |self: Arrangement| self.jack);
|
||
impl_has!(Sizer: |self: Arrangement| self.size);
|
||
impl_has!(Vec<MidiInput>: |self: Arrangement| self.midi_ins);
|
||
impl_has!(Vec<MidiOutput>: |self: Arrangement| self.midi_outs);
|
||
impl_has!(Clock: |self: Arrangement| self.clock);
|
||
impl_has!(Selection: |self: Arrangement| self.selection);
|
||
impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref());
|
||
impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut());
|
||
|
||
pub trait HasClipsSize {
|
||
fn clips_size (&self) -> &Sizer;
|
||
}
|
||
pub trait ClipsView: TracksView + ScenesView {
|
||
/// Draw clips per scene
|
||
fn view_scenes_clips <'a> (&'a self) -> impl Draw<Tui> + 'a {
|
||
view_scenes_clips(
|
||
||self.scenes_with_sizes(),
|
||
self.tracks_with_sizes(),
|
||
self.selection(),
|
||
self.editor(),
|
||
self.clips_size(),
|
||
self.is_editing(),
|
||
)
|
||
}
|
||
}
|
||
|
||
impl HasClipsSize for App {
|
||
fn clips_size (&self) -> &Sizer { &self.project.size_inner }
|
||
}
|
||
|
||
impl HasClipsSize for Arrangement {
|
||
fn clips_size (&self) -> &Sizer { &self.size_inner }
|
||
}
|
||
|
||
/// TODO: Preserve the generic passthru syntax;
|
||
/// remove this macro (only used twice) and potentially the trait.
|
||
#[macro_export] macro_rules! impl_has_clips {
|
||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||
impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? {
|
||
fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> {
|
||
$cb.read().unwrap()
|
||
}
|
||
fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> {
|
||
$cb.write().unwrap()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
#[macro_export] macro_rules! has_clip {
|
||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
|
||
fn clip (&$self) -> Option<Arc<RwLock<MidiClip>>> { $cb }
|
||
}
|
||
}
|
||
}
|
||
def_command!(ClipCommand: |clip: MidiClip| {
|
||
SetColor { color: Option<ItemTheme> } => {
|
||
//(SetColor [t: usize, s: usize, c: ItemTheme]
|
||
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
|
||
//("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random())))
|
||
todo!()
|
||
},
|
||
SetLoop { looping: Option<bool> } => {
|
||
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
|
||
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
|
||
todo!()
|
||
}
|
||
});
|
||
impl Arrangement {
|
||
/// Put a clip in a slot
|
||
pub fn clip_put (
|
||
&mut self, track: usize, scene: usize, clip: Option<Arc<RwLock<MidiClip>>>
|
||
) -> Option<Arc<RwLock<MidiClip>>> {
|
||
let old = self.scenes[scene].clips[track].clone();
|
||
self.scenes[scene].clips[track] = clip;
|
||
old
|
||
}
|
||
|
||
/// Change the color of a clip, returning the previous one
|
||
pub fn clip_set_color (
|
||
&self, track: usize, scene: usize, color: ItemTheme
|
||
) -> Option<ItemTheme> {
|
||
self.scenes[scene].clips[track].as_ref().map(|clip|{
|
||
let mut clip = clip.write().unwrap();
|
||
let old = clip.color.clone();
|
||
clip.color = color.clone();
|
||
panic!("{color:?} {old:?}");
|
||
//old
|
||
})
|
||
}
|
||
|
||
/// Toggle looping for the active clip
|
||
pub fn toggle_loop (&mut self) {
|
||
if let Some(clip) = self.selected_clip() {
|
||
clip.write().unwrap().toggle_loop()
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Default scene height.
|
||
pub const H_SCENE: usize = 2;
|
||
|
||
/// Default editor height.
|
||
pub const H_EDITOR: usize = 15;
|
||
|
||
/// A scene consists of a set of clips to play together.
|
||
///
|
||
/// ```
|
||
/// let scene: tek::Scene = Default::default();
|
||
/// let _ = scene.pulses();
|
||
/// let _ = scene.is_playing(&[]);
|
||
/// ```
|
||
#[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 }
|
||
}
|
||
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!() }
|
||
}
|
||
|
||
pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync {
|
||
fn h_scenes (&self) -> u16;
|
||
fn w_side (&self) -> u16;
|
||
fn w_mid (&self) -> u16;
|
||
|
||
fn view_scenes_names (&self) -> impl Draw<Tui> {
|
||
let select = self.selection();
|
||
let editor = self.editor();
|
||
let editing = self.is_editing();
|
||
draw(move |to: &mut Tui|{
|
||
for (index, scene, ..) in self.scenes_with_sizes() {
|
||
view_scene_name(select, editor, index, scene, editing).draw(to)?;
|
||
}
|
||
Ok(Some(XYWH(1, 1, 1, 1)))
|
||
})
|
||
.exact_w(20)
|
||
}
|
||
|
||
fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
|
||
let mut y = 0;
|
||
self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{
|
||
let height = if self.selection().scene() == Some(s) && self.editor().is_some() {
|
||
8
|
||
} else {
|
||
H_SCENE
|
||
};
|
||
if y + height <= self.clips_size().h() as usize {
|
||
let data = (s, scene, y, y + height);
|
||
y += height;
|
||
Some(data)
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
pub trait HasSceneScroll: HasScenes {
|
||
fn scene_scroll (&self) -> usize;
|
||
}
|
||
|
||
pub trait HasScene: AsRefOpt<Scene> + AsMutOpt<Scene> {
|
||
fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() }
|
||
fn scene (&self) -> Option<&Scene> { self.as_ref_opt() }
|
||
}
|
||
|
||
pub trait HasScenes: AsRef<Vec<Scene>> + AsMut<Vec<Scene>> {
|
||
fn scenes (&self) -> &Vec<Scene> { self.as_ref() }
|
||
fn scenes_mut (&mut self) -> &mut Vec<Scene> { self.as_mut() }
|
||
/// Generate the default name for a new scene
|
||
fn scene_default_name (&self) -> Arc<str> { format!("s{:3>}", self.scenes().len() + 1).into() }
|
||
fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) }
|
||
/// Add multiple scenes
|
||
fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks {
|
||
let scene_color_1 = ItemColor::random();
|
||
let scene_color_2 = ItemColor::random();
|
||
for i in 0..n {
|
||
let _ = self.scene_add(None, Some(
|
||
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
||
))?;
|
||
}
|
||
Ok(())
|
||
}
|
||
/// Add a scene
|
||
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemTheme>)
|
||
-> Usually<(usize, &mut Scene)> where Self: HasTracks
|
||
{
|
||
let scene = Scene {
|
||
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
|
||
clips: vec![None;self.tracks().len()],
|
||
color: color.unwrap_or_else(ItemTheme::random),
|
||
};
|
||
self.scenes_mut().push(scene);
|
||
let index = self.scenes().len() - 1;
|
||
Ok((index, &mut self.scenes_mut()[index]))
|
||
}
|
||
}
|
||
|
||
impl HasSceneScroll for Arrangement {
|
||
fn scene_scroll (&self) -> usize { self.scene_scroll }
|
||
}
|
||
|
||
impl HasSceneScroll for App {
|
||
fn scene_scroll (&self) -> usize { self.project.scene_scroll() }
|
||
}
|
||
|
||
impl ScenesView for Arrangement {
|
||
fn h_scenes (&self) -> u16 { (self.size.h() as u16).saturating_sub(20) }
|
||
fn w_side (&self) -> u16 { (self.size.w() as u16 * 2 / 10).max(20) }
|
||
fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) }
|
||
}
|
||
|
||
pub type SceneWith<'a, T> =
|
||
(usize, &'a Scene, usize, usize, T);
|
||
|
||
def_command!(SceneCommand: |scene: Scene| {
|
||
SetSize { size: usize } => { todo!() },
|
||
SetZoom { size: usize } => { todo!() },
|
||
SetName { name: Arc<str> } =>
|
||
swap_value(&mut scene.name, name, |name|Self::SetName{name}),
|
||
SetColor { color: ItemTheme } =>
|
||
swap_value(&mut scene.color, color, |color|Self::SetColor{color}),
|
||
});
|
||
|
||
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt());
|
||
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
|
||
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene());
|
||
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut());
|
||
impl_has!(Vec<Scene>: |self: Arrangement| self.scenes);
|
||
impl<T: TracksView+ScenesView+Send+Sync> ClipsView for T {}
|
||
impl<T: AsRef<Vec<Scene>>+AsMut<Vec<Scene>>> HasScenes for T {}
|
||
impl<T: AsRefOpt<Scene>+AsMutOpt<Scene>+Send+Sync> HasScene for T {}
|
||
|
||
impl ScenesView for App {
|
||
fn w_mid (&self) -> u16 {
|
||
(self.size.w() as u16).saturating_sub(self.w_side())
|
||
}
|
||
fn w_side (&self) -> u16 {
|
||
20
|
||
}
|
||
fn h_scenes (&self) -> u16 {
|
||
(self.size.h() as u16).saturating_sub(20)
|
||
}
|
||
}
|
||
|
||
/// A track consists of a sequencer and zero or more devices chained after it.
|
||
///
|
||
/// ```
|
||
/// let track: tek::Track = Default::default();
|
||
/// ```
|
||
#[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>,
|
||
}
|
||
|
||
impl Track {
|
||
/// Create a new track with only the default [Sequencer].
|
||
pub fn new (
|
||
name: &impl AsRef<str>,
|
||
color: Option<ItemTheme>,
|
||
jack: &Jack<'static>,
|
||
clock: Option<&Clock>,
|
||
clip: Option<&Arc<RwLock<MidiClip>>>,
|
||
midi_from: &[Connect],
|
||
midi_to: &[Connect],
|
||
) -> 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()
|
||
})
|
||
}
|
||
pub fn audio_ins (&self) -> &[AudioInput] {
|
||
self.devices.first().map(|x|x.audio_ins()).unwrap_or_default()
|
||
}
|
||
pub fn audio_outs (&self) -> &[AudioOutput] {
|
||
self.devices.last().map(|x|x.audio_outs()).unwrap_or_default()
|
||
}
|
||
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!() }
|
||
|
||
pub fn per <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||
) -> impl Draw<Tui> + 'a {
|
||
view_track_per(tracks, callback)
|
||
}
|
||
}
|
||
|
||
#[cfg(feature = "sampler")]
|
||
impl Track {
|
||
|
||
/// Create a new track connecting the [Sequencer] to a [Sampler].
|
||
pub fn new_with_sampler (
|
||
name: &impl AsRef<str>,
|
||
color: Option<ItemTheme>,
|
||
jack: &Jack<'static>,
|
||
clock: Option<&Clock>,
|
||
clip: Option<&Arc<RwLock<MidiClip>>>,
|
||
midi_from: &[Connect],
|
||
midi_to: &[Connect],
|
||
audio_from: &[&[Connect];2],
|
||
audio_to: &[&[Connect];2],
|
||
) -> Usually<Self> {
|
||
let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?;
|
||
let client_name = jack.with_client(|c|c.name().to_string());
|
||
let port_name = track.sequencer.midi_outs[0].port_name();
|
||
let connect = [Connect::exact(format!("{client_name}:{}", port_name))];
|
||
track.devices.push(Device::Sampler(Sampler::new(
|
||
jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to
|
||
)?));
|
||
Ok(track)
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
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; } }
|
||
}
|
||
|
||
pub trait HasTracks: AsRef<Vec<Track>> + AsMut<Vec<Track>> {
|
||
fn tracks (&self) -> &Vec<Track> { self.as_ref() }
|
||
fn tracks_mut (&mut self) -> &mut Vec<Track> { self.as_mut() }
|
||
/// Run audio callbacks for every track and every device
|
||
fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||
for track in self.tracks_mut().iter_mut() {
|
||
if Control::Quit == Audio::process(&mut track.sequencer, client, scope) {
|
||
return Control::Quit
|
||
}
|
||
for device in track.devices.iter_mut() {
|
||
if Control::Quit == DeviceAudio(device).process(client, scope) {
|
||
return Control::Quit
|
||
}
|
||
}
|
||
}
|
||
Control::Continue
|
||
}
|
||
fn track_longest_name (&self) -> usize {
|
||
self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max)
|
||
}
|
||
/// Stop all playing clips
|
||
fn tracks_stop_all (&mut self) {
|
||
for track in self.tracks_mut().iter_mut() {
|
||
track.sequencer.enqueue_next(None);
|
||
}
|
||
}
|
||
/// Stop all playing clips
|
||
fn tracks_launch (&mut self, clips: Option<Vec<Option<Arc<RwLock<MidiClip>>>>>) {
|
||
if let Some(clips) = clips {
|
||
for (clip, track) in clips.iter().zip(self.tracks_mut()) {
|
||
track.sequencer.enqueue_next(clip.as_ref());
|
||
}
|
||
} else {
|
||
for track in self.tracks_mut().iter_mut() {
|
||
track.sequencer.enqueue_next(None);
|
||
}
|
||
}
|
||
}
|
||
/// Spacing between tracks.
|
||
const TRACK_SPACING: usize = 0;
|
||
}
|
||
|
||
pub trait HasTrack: AsRefOpt<Track> + AsMutOpt<Track> {
|
||
fn track (&self) -> Option<&Track> { self.as_ref_opt() }
|
||
fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() }
|
||
|
||
#[cfg(feature = "port")]
|
||
fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> + 'a {
|
||
view_midi_ins_status(theme, self.track())
|
||
}
|
||
|
||
#[cfg(feature = "port")]
|
||
fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> + '_ {
|
||
view_midi_outs_status(theme, self.track())
|
||
}
|
||
|
||
#[cfg(feature = "port")]
|
||
fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||
view_audio_ins_status(theme, self.track())
|
||
}
|
||
|
||
#[cfg(feature = "port")]
|
||
fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||
view_audio_outs_status(theme, self.track())
|
||
}
|
||
}
|
||
|
||
pub trait HasTrackScroll: HasTracks {
|
||
fn track_scroll (&self) -> usize;
|
||
}
|
||
|
||
pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll {
|
||
/// Draw name of each track
|
||
fn view_track_names (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||
let track_count = self.tracks().len();
|
||
let scene_count = self.scenes().len();
|
||
let selected = self.selection();
|
||
let button = south(
|
||
button_3("t", "rack ", format!("{}{track_count}", selected.track()
|
||
.map(|track|format!("{track}/")).unwrap_or_default()), false),
|
||
button_3("s", "cene ", format!("{}{scene_count}", selected.scene()
|
||
.map(|scene|format!("{scene}/")).unwrap_or_default()), false));
|
||
let button_2 = south(
|
||
button_2("T", "+", false),
|
||
button_2("S", "+", false));
|
||
view_track_row_section(theme, button, button_2, bg(theme.darker.term,
|
||
draw(|to: &mut Tui|{
|
||
for (index, track, x1, _x2) in self.tracks_with_sizes() {
|
||
let b = if selected.track() == Some(index) {
|
||
track.color.light.term
|
||
} else {
|
||
track.color.base.term
|
||
};
|
||
bg(b, south(east(
|
||
format!("·t{index:02} "),
|
||
fg(Rgb(255, 255, 255), bold(true, &track.name))
|
||
).align_nw().full_w(), ""))
|
||
.exact_w(track_width(index, track))
|
||
.push_x(x1 as u16)
|
||
.draw(to)?;
|
||
}
|
||
Ok(Some(XYWH(0, 0, 0, 0)))
|
||
}).exact_h(2)))
|
||
}
|
||
/// Draw outputs per track
|
||
fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw<Tui> {
|
||
view_track_row_section(theme,
|
||
south(button_2("o", "utput", false).align_w().full_w(),
|
||
draw(|to: &mut Tui|{
|
||
for port in self.midi_outs().iter() {
|
||
let _ = port.port_name().align_w().full_w().draw(to)?;
|
||
}
|
||
Ok(Some(XYWH(0, 0, 0, 0)))
|
||
})),
|
||
button_2("O", "+", false),
|
||
bg(theme.darker.term, draw(|to: &mut Tui|{
|
||
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||
let f = Rgb(255, 255, 255);
|
||
let b = track.color.dark.term;
|
||
let iter = ||track.sequencer.midi_outs.iter();
|
||
let draw = |port: &MidiOutput, _|fg(f, bg(b,
|
||
format!("·o{index:02} {}", port.port_name()).full_w().align_w()).exact_h(1));
|
||
iter_south(iter, draw).full_h().align_nw()
|
||
.exact_w(track_width(index, track))
|
||
.draw(to)?;
|
||
}
|
||
Ok(Some(XYWH(0, 0, 0, 0)))
|
||
}).align_w()))
|
||
}
|
||
/// Draw inputs per track
|
||
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> {
|
||
let mut height = 0u16;
|
||
for track in self.tracks().iter() {
|
||
height = height.max(track.sequencer.midi_ins.len() as u16);
|
||
}
|
||
view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false),
|
||
bg(theme.darker.term, draw(move|to: &mut Tui|{
|
||
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||
south(
|
||
bg(track.color.base.term,
|
||
east!(
|
||
either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "),
|
||
either(track.sequencer.recording, fg(Red, "●rec "), "·rec "),
|
||
either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "),
|
||
).align_w().full_w()),
|
||
iter_south(||track.sequencer.midi_ins.iter(),
|
||
|port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term,
|
||
format!("·i{index:02} {}", port.port_name()).align_w().full_w()))
|
||
).align_nw().exact_wh(track_width(index, track), height + 1).draw(to)?;
|
||
}
|
||
Ok(Some(XYWH(0, 0, 0, 0)))
|
||
}).align_w()))
|
||
}
|
||
/// Iterate over tracks with their corresponding sizes.
|
||
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
|
||
let _editor_width = self.editor().map(|e|e.size.w());
|
||
let _active_track = self.selection().track();
|
||
let mut x = 0;
|
||
let w = self.clips_size().w() as usize;
|
||
self.tracks().iter().enumerate().map_while(move |(index, track)|{
|
||
let width = track.width.max(8);
|
||
if x + width < w {
|
||
let data = (index, track, x, x + width);
|
||
x += width + Self::TRACK_SPACING;
|
||
Some(data)
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
}
|
||
|
||
}
|
||
|
||
impl HasTrackScroll for Arrangement {
|
||
fn track_scroll (&self) -> usize { self.track_scroll }
|
||
}
|
||
|
||
def_command!(TrackCommand: |track: Track| {
|
||
Stop => { track.sequencer.enqueue_next(None); Ok(None) },
|
||
SetRec { rec: Option<bool> } => toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }),
|
||
SetMon { mon: Option<bool> } => toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }),
|
||
SetMute { mute: Option<bool> } => todo!(),
|
||
SetSolo { solo: Option<bool> } => todo!(),
|
||
SetSize { size: usize } => todo!(),
|
||
SetZoom { zoom: usize } => todo!(),
|
||
SetName { name: Arc<str> } => swap_value(&mut track.name, name, |name|Self::SetName { name }),
|
||
SetColor { color: ItemTheme } => swap_value(&mut track.color, color, |color|Self::SetColor { color }),
|
||
});
|
||
|
||
impl<T: AsRef<Vec<Track>>+AsMut<Vec<Track>>> HasTracks for T {}
|
||
impl<T: AsRefOpt<Track>+AsMutOpt<Track>+Send+Sync> HasTrack for T {}
|
||
impl<T: ScenesView+HasMidiIns+HasMidiOuts+HasTrackScroll> TracksView for T {}
|
||
impl_has!(Vec<Track>: |self: Arrangement| self.tracks);
|
||
impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track());
|
||
impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut());
|
||
impl_as_ref!(Vec<Track>: |self: App| self.project.as_ref());
|
||
impl_as_mut!(Vec<Track>: |self: App| self.project.as_mut());
|
||
#[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt());
|
||
#[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt());
|
||
|
||
impl Arrangement {
|
||
|
||
pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw<Tui> + '_ {
|
||
let title_1 = button_3("i", "nput ", format!("{}", self.midi_ins().len()), false).align_w().exact_wh(20, 1);
|
||
let title_2 = button_2("I", "+", false).exact_wh(4, 1);
|
||
east(title_1, west(title_2, draw(move|to: &mut Tui|{
|
||
for (_index, track, x1, _x2) in self.tracks_with_sizes() {
|
||
let _ = south(
|
||
bg(track.color.dark.term, east!(
|
||
either(track.sequencer.monitoring, fg(Green, "mon "), "mon "),
|
||
either(track.sequencer.recording, fg(Red, "rec "), "rec "),
|
||
either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "),
|
||
).exact_w(track.width as u16)).align_w().push_x(x1 as u16),
|
||
draw(move |to: &mut Tui|{
|
||
for (index, port) in self.midi_ins().as_slice().iter().enumerate() {
|
||
let _ = east(
|
||
east(
|
||
" ● ",
|
||
bold(true, fg(Rgb(255,255,255), port.port_name()))
|
||
).align_w().exact_w(20),
|
||
west(
|
||
().exact_w(4),
|
||
bg(track.color.darker.term, east!(
|
||
either(track.sequencer.monitoring, fg(Green, " ● "), " · "),
|
||
either(track.sequencer.recording, fg(Red, " ● "), " · "),
|
||
either(track.sequencer.overdub, fg(Yellow, " ● "), " · "),
|
||
).exact_w(track.width as u16).align_w())
|
||
)
|
||
).push_x(index as u16 * 10).exact_h(1).draw(to)?;
|
||
}
|
||
todo!()
|
||
})
|
||
).draw(to)?;
|
||
}
|
||
Ok(Some(to.area()))
|
||
})))
|
||
}
|
||
|
||
pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||
let height = self.outputs_height();
|
||
let list = south(
|
||
button_3(
|
||
"o", "utput", format!("{}", self.midi_outs().len()), false
|
||
).align_w().full_w().exact_h(1),
|
||
draw(|to: &mut Tui|{
|
||
for (_index, port) in self.midi_outs().iter().enumerate() {
|
||
east(
|
||
east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name()))).align_w(),
|
||
format!("{}/{} ",
|
||
port.port().get_connections().len(),
|
||
port.connections.len()).align_e().full_w().exact_h(1)).full_w().draw(to)?;
|
||
for (index, conn) in port.connections.iter().enumerate() {
|
||
format!(" c{index:02}{}", conn.info()).align_w().full_w().exact_h(1).draw(to)?;
|
||
}
|
||
}
|
||
todo!();
|
||
}).align_nw().full_wh().exact_h(height - 1)
|
||
);
|
||
view_track_row_section(theme, list, button_2("O", "+", false),
|
||
bg(theme.darker.term, draw(|to: &mut Tui|{
|
||
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||
let _ = draw(|to: &mut Tui|{
|
||
east(
|
||
either(true, fg(Green, "play "), "play "),
|
||
either(false, fg(Yellow, "solo "), "solo "),
|
||
).align_w().exact_h(1).draw(to)?;
|
||
for (_index, port) in self.midi_outs().iter().enumerate() {
|
||
east(
|
||
either(true, fg(Green, " ● "), " · "),
|
||
either(false, fg(Yellow, " ● "), " · "),
|
||
).align_w().exact_h(1).draw(to)?;
|
||
for (_index, _conn) in port.connections.iter().enumerate() {
|
||
"".full_w().exact_h(1).draw(to)?;
|
||
}
|
||
}
|
||
todo!()
|
||
}).exact_w(track_width(index, track)).draw(to)?;
|
||
}
|
||
todo!()
|
||
}).align_w().full_w())).exact_h(height)
|
||
}
|
||
|
||
pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||
let height = self.devices_height();
|
||
view_track_row_section(theme,
|
||
button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false),
|
||
button_2("D", "+", false),
|
||
iter_once(self.tracks_with_sizes(), move|(_, track, _x1, _x2), index|bg(
|
||
track.color.dark.term,
|
||
iter_south(move||0..height,
|
||
|_, _index|fg_bg(
|
||
ItemTheme::G[32].lightest.term,
|
||
ItemTheme::G[32].dark.term,
|
||
format!(" · {}", "--").align_nw()
|
||
).exact_wh(track.width as u16, 2)
|
||
).align_nw()).exact_wh(
|
||
Some(track_width(index, track)),
|
||
Some(height + 1),
|
||
)))
|
||
}
|
||
|
||
fn devices_height (&self) -> u16 {
|
||
let mut h = 2;
|
||
for track in self.tracks().iter() {
|
||
h = h.max(track.devices.len() * 2);
|
||
}
|
||
h as u16
|
||
}
|
||
|
||
fn outputs_height (&self) -> u16 {
|
||
let mut h = 1;
|
||
for output in self.midi_outs().iter() {
|
||
h += 1 + output.connections.len();
|
||
}
|
||
h as u16
|
||
}
|
||
|
||
/// Add multiple tracks
|
||
pub fn tracks_add (
|
||
&mut self, count: usize, width: Option<usize>, mins: &[Connect], mouts: &[Connect],
|
||
) -> Usually<()> {
|
||
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 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: &[Connect],
|
||
mouts: &[Connect],
|
||
) -> Usually<(usize, &mut Track)> {
|
||
let name: Arc<str> = name.map_or_else(
|
||
||format!("trk{:02}", self.track_last).into(),
|
||
|x|x.to_string().into()
|
||
);
|
||
self.track_last += 1;
|
||
let 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]))
|
||
}
|
||
}
|
||
|
||
impl HasTrackScroll for App {
|
||
fn track_scroll (&self) -> usize {
|
||
self.project.track_scroll()
|
||
}
|
||
}
|
||
|
||
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
|
||
track.width as u16
|
||
}
|
||
|
||
/// Define a type alias for iterators of sized items (columns).
|
||
macro_rules! def_sizes_iter {
|
||
($Type:ident => $($Item:ty),+) => {
|
||
pub trait $Type<'a>: Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a {}
|
||
impl<'a, T: Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a> $Type<'a> for T {}
|
||
}
|
||
}
|
||
|
||
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
||
def_sizes_iter!(ScenesSizes => Scene);
|
||
def_sizes_iter!(TracksSizes => Track);
|