mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
206 lines
7.2 KiB
Rust
206 lines
7.2 KiB
Rust
use crate::*;
|
|
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
|
|
pub trait HasClips {
|
|
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
|
|
fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>;
|
|
fn add_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
|
let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None)));
|
|
self.clips_mut().push(clip.clone());
|
|
(self.clips().len() - 1, clip)
|
|
}
|
|
}
|
|
#[macro_export] macro_rules! 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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#[derive(Debug)]
|
|
pub struct MidiPool {
|
|
pub visible: bool,
|
|
/// Collection of clips
|
|
pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
|
/// Selected clip
|
|
pub clip: AtomicUsize,
|
|
/// Mode switch
|
|
pub mode: Option<PoolMode>,
|
|
}
|
|
/// Modes for clip pool
|
|
#[derive(Debug, Clone)]
|
|
pub enum PoolMode {
|
|
/// Renaming a pattern
|
|
Rename(usize, Arc<str>),
|
|
/// Editing the length of a pattern
|
|
Length(usize, usize, ClipLengthFocus),
|
|
/// Load clip from disk
|
|
Import(usize, FileBrowser),
|
|
/// Save clip to disk
|
|
Export(usize, FileBrowser),
|
|
}
|
|
impl Default for MidiPool {
|
|
fn default () -> Self {
|
|
Self {
|
|
visible: true,
|
|
clips: Arc::from(RwLock::from(vec![])),
|
|
clip: 0.into(),
|
|
mode: None,
|
|
}
|
|
}
|
|
}
|
|
from!(|clip:&Arc<RwLock<MidiClip>>|MidiPool = {
|
|
let model = Self::default();
|
|
model.clips.write().unwrap().push(clip.clone());
|
|
model.clip.store(1, Relaxed);
|
|
model
|
|
});
|
|
has_clips!(|self: MidiPool|self.clips);
|
|
has_clip!(|self: MidiPool|self.clips().get(self.clip_index()).map(|c|c.clone()));
|
|
impl MidiPool {
|
|
pub(crate) fn clip_index (&self) -> usize {
|
|
self.clip.load(Relaxed)
|
|
}
|
|
pub(crate) fn set_clip_index (&self, value: usize) {
|
|
self.clip.store(value, Relaxed);
|
|
}
|
|
pub(crate) fn mode (&self) -> &Option<PoolMode> {
|
|
&self.mode
|
|
}
|
|
pub(crate) fn mode_mut (&mut self) -> &mut Option<PoolMode> {
|
|
&mut self.mode
|
|
}
|
|
pub fn file_picker (&self) -> Option<&FileBrowser> {
|
|
match self.mode {
|
|
Some(PoolMode::Import(_, ref file_picker)) => Some(file_picker),
|
|
Some(PoolMode::Export(_, ref file_picker)) => Some(file_picker),
|
|
_ => None
|
|
}
|
|
}
|
|
pub fn begin_clip_length (&mut self) {
|
|
let length = self.clips()[self.clip_index()].read().unwrap().length;
|
|
*self.mode_mut() = Some(PoolMode::Length(
|
|
self.clip_index(),
|
|
length,
|
|
ClipLengthFocus::Bar
|
|
));
|
|
}
|
|
pub fn begin_clip_rename (&mut self) {
|
|
let name = self.clips()[self.clip_index()].read().unwrap().name.clone();
|
|
*self.mode_mut() = Some(PoolMode::Rename(
|
|
self.clip_index(),
|
|
name
|
|
));
|
|
}
|
|
pub fn begin_import (&mut self) -> Usually<()> {
|
|
*self.mode_mut() = Some(PoolMode::Import(
|
|
self.clip_index(),
|
|
FileBrowser::new(None)?
|
|
));
|
|
Ok(())
|
|
}
|
|
pub fn begin_export (&mut self) -> Usually<()> {
|
|
*self.mode_mut() = Some(PoolMode::Export(
|
|
self.clip_index(),
|
|
FileBrowser::new(None)?
|
|
));
|
|
Ok(())
|
|
}
|
|
}
|
|
/// Displays and edits clip length.
|
|
#[derive(Clone)]
|
|
pub struct ClipLength {
|
|
/// Pulses per beat (quaver)
|
|
pub ppq: usize,
|
|
/// Beats per bar
|
|
pub bpb: usize,
|
|
/// Length of clip in pulses
|
|
pub pulses: usize,
|
|
/// Selected subdivision
|
|
pub focus: Option<ClipLengthFocus>,
|
|
}
|
|
impl ClipLength {
|
|
pub fn new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
|
|
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
|
}
|
|
pub fn bars (&self) -> usize {
|
|
self.pulses / (self.bpb * self.ppq)
|
|
}
|
|
pub fn beats (&self) -> usize {
|
|
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
|
}
|
|
pub fn ticks (&self) -> usize {
|
|
self.pulses % self.ppq
|
|
}
|
|
pub fn bars_string (&self) -> Arc<str> {
|
|
format!("{}", self.bars()).into()
|
|
}
|
|
pub fn beats_string (&self) -> Arc<str> {
|
|
format!("{}", self.beats()).into()
|
|
}
|
|
pub fn ticks_string (&self) -> Arc<str> {
|
|
format!("{:>02}", self.ticks()).into()
|
|
}
|
|
}
|
|
/// Focused field of `ClipLength`
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub enum ClipLengthFocus {
|
|
/// Editing the number of bars
|
|
Bar,
|
|
/// Editing the number of beats
|
|
Beat,
|
|
/// Editing the number of ticks
|
|
Tick,
|
|
}
|
|
impl ClipLengthFocus {
|
|
pub fn next (&mut self) {
|
|
*self = match self { Self::Bar => Self::Beat, Self::Beat => Self::Tick, Self::Tick => Self::Bar, }
|
|
}
|
|
pub fn prev (&mut self) {
|
|
*self = match self { Self::Bar => Self::Tick, Self::Beat => Self::Bar, Self::Tick => Self::Beat, }
|
|
}
|
|
}
|
|
pub struct PoolView<'a>(pub bool, pub &'a MidiPool);
|
|
content!(TuiOut: |self: PoolView<'a>| {
|
|
let Self(compact, model) = self;
|
|
let MidiPool { clips, mode, .. } = self.1;
|
|
let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||TuiTheme::g(32).into());
|
|
let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x));
|
|
let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x);
|
|
let iter = | |model.clips().clone().into_iter();
|
|
Tui::bg(Color::Reset, Fixed::y(clips.read().unwrap().len() as u16, on_bg(border(Map::new(iter, move|clip, i|{
|
|
let item_height = 1;
|
|
let item_offset = i as u16 * item_height;
|
|
let selected = i == model.clip_index();
|
|
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
|
|
let bg = if selected { color.light.rgb } else { color.base.rgb };
|
|
let fg = color.lightest.rgb;
|
|
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
|
let length = if *compact { String::default() } else { format!("{length} ") };
|
|
Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!(
|
|
Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))),
|
|
Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))),
|
|
Fill::x(Align::w(When::new(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "▶"))))),
|
|
Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "◀"))))),
|
|
))))
|
|
})))))
|
|
});
|
|
content!(TuiOut: |self: ClipLength| {
|
|
let bars = ||self.bars_string();
|
|
let beats = ||self.beats_string();
|
|
let ticks = ||self.ticks_string();
|
|
match self.focus {
|
|
None =>
|
|
row!(" ", bars(), ".", beats(), ".", ticks()),
|
|
Some(ClipLengthFocus::Bar) =>
|
|
row!("[", bars(), "]", beats(), ".", ticks()),
|
|
Some(ClipLengthFocus::Beat) =>
|
|
row!(" ", bars(), "[", beats(), "]", ticks()),
|
|
Some(ClipLengthFocus::Tick) =>
|
|
row!(" ", bars(), ".", beats(), "[", ticks()),
|
|
}
|
|
});
|