mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
midi import/export browser, pt.1
This commit is contained in:
parent
26eb5f0315
commit
6d8dbc6780
5 changed files with 263 additions and 140 deletions
|
|
@ -903,6 +903,16 @@ impl<E: Engine, A: Widget<Engine = E>, B: Widget<Engine = E>> Widget for Split<E
|
|||
/// A widget that tracks its render width and height
|
||||
pub struct Measure<E: Engine>(PhantomData<E>, AtomicUsize, AtomicUsize);
|
||||
|
||||
impl<E: Engine> Clone for Measure<E> {
|
||||
fn clone (&self) -> Self {
|
||||
Self(
|
||||
Default::default(),
|
||||
AtomicUsize::from(self.1.load(Ordering::Relaxed)),
|
||||
AtomicUsize::from(self.2.load(Ordering::Relaxed)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> std::fmt::Debug for Measure<E> {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("Measure")
|
||||
|
|
|
|||
|
|
@ -654,3 +654,37 @@ pub const fn shift (key: KeyEvent) -> KeyEvent {
|
|||
}))
|
||||
}
|
||||
}
|
||||
#[macro_export] macro_rules! key_lit {
|
||||
($code:expr) => {
|
||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||
code: $code,
|
||||
modifiers: crossterm::event::KeyModifiers::NONE,
|
||||
kind: crossterm::event::KeyEventKind::Press,
|
||||
state: crossterm::event::KeyEventState::NONE
|
||||
}))
|
||||
};
|
||||
(Ctrl-$code:expr) => {
|
||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||
code: $code,
|
||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||
kind: crossterm::event::KeyEventKind::Press,
|
||||
state: crossterm::event::KeyEventState::NONE
|
||||
}))
|
||||
};
|
||||
(Alt-$code:expr) => {
|
||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||
code: $code,
|
||||
modifiers: crossterm::event::KeyModifiers::ALT,
|
||||
kind: crossterm::event::KeyEventKind::Press,
|
||||
state: crossterm::event::KeyEventState::NONE
|
||||
}))
|
||||
};
|
||||
(Shift-$code:expr) => {
|
||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||
code: $code,
|
||||
modifiers: crossterm::event::KeyModifiers::SHIFT,
|
||||
kind: crossterm::event::KeyEventKind::Press,
|
||||
state: crossterm::event::KeyEventState::NONE
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,8 @@ pub enum PhrasesCommand {
|
|||
Phrase(PhrasePoolCommand),
|
||||
Rename(PhraseRenameCommand),
|
||||
Length(PhraseLengthCommand),
|
||||
Import(FileBrowserCommand),
|
||||
Export(FileBrowserCommand),
|
||||
}
|
||||
|
||||
impl<T: PhrasesControl> Command<T> for PhrasesCommand {
|
||||
|
|
@ -124,8 +126,44 @@ impl<T: PhrasesControl> Command<T> for PhrasesCommand {
|
|||
use PhrasesCommand::*;
|
||||
Ok(match self {
|
||||
Phrase(command) => command.execute(state)?.map(Phrase),
|
||||
Rename(command) => command.execute(state)?.map(Rename),
|
||||
Length(command) => command.execute(state)?.map(Length),
|
||||
Rename(command) => match command {
|
||||
PhraseRenameCommand::Begin => {
|
||||
let length = state.phrases()[state.phrase_index()].read().unwrap().length;
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PhrasesMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Rename)
|
||||
},
|
||||
Length(command) => match command {
|
||||
PhraseLengthCommand::Begin => {
|
||||
let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone();
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PhrasesMode::Rename(state.phrase_index(), name)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Length)
|
||||
},
|
||||
Import(command) => match command {
|
||||
FileBrowserCommand::Begin => {
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PhrasesMode::Import(state.phrase_index(), FileBrowser::new())
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Import)
|
||||
},
|
||||
Export(command) => match command {
|
||||
FileBrowserCommand::Begin => {
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PhrasesMode::Export(state.phrase_index(), FileBrowser::new())
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Export)
|
||||
},
|
||||
Select(phrase) => {
|
||||
state.set_phrase_index(phrase);
|
||||
None
|
||||
|
|
@ -137,21 +175,20 @@ impl<T: PhrasesControl> Command<T> for PhrasesCommand {
|
|||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum PhraseLengthCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Set(usize),
|
||||
Next,
|
||||
Prev,
|
||||
Inc,
|
||||
Dec,
|
||||
Set(usize),
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl<T: PhrasesControl> Command<T> for PhraseLengthCommand {
|
||||
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||
use PhraseLengthFocus::*;
|
||||
use PhraseLengthCommand::*;
|
||||
let mut mode = state.phrases_mode_mut().clone();
|
||||
if let Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) = mode {
|
||||
match self {
|
||||
match state.phrases_mode_mut().clone() {
|
||||
Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) => match self {
|
||||
Cancel => { *state.phrases_mode_mut() = None; },
|
||||
Prev => { focus.prev() },
|
||||
Next => { focus.next() },
|
||||
|
|
@ -174,15 +211,9 @@ impl<T: PhrasesControl> Command<T> for PhraseLengthCommand {
|
|||
return Ok(Some(Self::Set(old_length)))
|
||||
},
|
||||
_ => unreachable!()
|
||||
}
|
||||
} else if self == Begin {
|
||||
let length = state.phrases()[state.phrase_index()].read().unwrap().length;
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PhrasesMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar)
|
||||
);
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
|
@ -190,20 +221,16 @@ impl<T: PhrasesControl> Command<T> for PhraseLengthCommand {
|
|||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PhraseRenameCommand {
|
||||
Begin,
|
||||
Set(String),
|
||||
Confirm,
|
||||
Cancel,
|
||||
Confirm,
|
||||
Set(String),
|
||||
}
|
||||
|
||||
impl<T> Command<T> for PhraseRenameCommand
|
||||
where
|
||||
T: PhrasesControl
|
||||
{
|
||||
impl<T: PhrasesControl> Command<T> for PhraseRenameCommand {
|
||||
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||
use PhraseRenameCommand::*;
|
||||
let mut mode = state.phrases_mode_mut().clone();
|
||||
if let Some(PhrasesMode::Rename(phrase, ref mut old_name)) = mode {
|
||||
match self {
|
||||
match state.phrases_mode_mut().clone() {
|
||||
Some(PhrasesMode::Rename(phrase, ref mut old_name)) => match self {
|
||||
Set(s) => {
|
||||
state.phrases()[phrase].write().unwrap().name = s.into();
|
||||
return Ok(Some(Self::Set(old_name.clone())))
|
||||
|
|
@ -217,15 +244,40 @@ where
|
|||
state.phrases()[phrase].write().unwrap().name = old_name.clone();
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
} else if self == Begin {
|
||||
let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone();
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PhrasesMode::Rename(state.phrase_index(), name)
|
||||
);
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Commands supported by [FileBrowser]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum FileBrowserCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Confirm,
|
||||
Select(usize),
|
||||
Chdir(PathBuf)
|
||||
}
|
||||
|
||||
impl<T: PhrasesControl> Command<T> for FileBrowserCommand {
|
||||
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||
use FileBrowserCommand::*;
|
||||
match state.phrases_mode_mut().clone() {
|
||||
Some(PhrasesMode::Import(index, browser)) => {
|
||||
todo!()
|
||||
},
|
||||
Some(PhrasesMode::Export(index, browser)) => {
|
||||
todo!()
|
||||
},
|
||||
_ => match self {
|
||||
Begin => {
|
||||
todo!()
|
||||
},
|
||||
_ => unreachable!()
|
||||
}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -247,73 +247,79 @@ fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option<Arr
|
|||
|
||||
impl<T: PhrasesControl> InputToCommand<Tui, T> for PhrasesCommand {
|
||||
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
|
||||
use PhrasePoolCommand as Pool;
|
||||
use PhraseRenameCommand as Rename;
|
||||
use PhraseLengthCommand as Length;
|
||||
use KeyCode::{Up, Down, Delete, Char};
|
||||
let index = state.phrase_index();
|
||||
let count = state.phrases().len();
|
||||
Some(match input.event() {
|
||||
key!(Up) => Self::Select(
|
||||
state.phrase_index().overflowing_sub(1).0.min(state.phrases().len() - 1)
|
||||
),
|
||||
key!(Down) => Self::Select(
|
||||
state.phrase_index().saturating_add(1) % state.phrases().len()
|
||||
),
|
||||
key!(Char(',')) => if index > 1 {
|
||||
state.set_phrase_index(state.phrase_index().saturating_sub(1));
|
||||
Self::Phrase(Pool::Swap(index - 1, index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
key!(Char('.')) => if index < count.saturating_sub(1) {
|
||||
state.set_phrase_index(state.phrase_index() + 1);
|
||||
Self::Phrase(Pool::Swap(index + 1, index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
key!(Delete) => if index > 0 {
|
||||
state.set_phrase_index(index.min(count.saturating_sub(1)));
|
||||
Self::Phrase(Pool::Delete(index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
key!(Char('a')) => Self::Phrase(Pool::Add(
|
||||
count,
|
||||
Phrase::new(
|
||||
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
|
||||
)
|
||||
)),
|
||||
key!(Char('i')) => Self::Phrase(Pool::Add(
|
||||
index + 1,
|
||||
Phrase::new(
|
||||
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
|
||||
)
|
||||
)),
|
||||
key!(Char('d')) => {
|
||||
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
|
||||
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
|
||||
Self::Phrase(Pool::Add(index + 1, phrase))
|
||||
},
|
||||
key!(Char('c')) => Self::Phrase(Pool::SetColor(
|
||||
index,
|
||||
ItemColor::random()
|
||||
)),
|
||||
key!(Char('n')) => Self::Rename(Rename::Begin),
|
||||
key!(Char('t')) => Self::Length(Length::Begin),
|
||||
_ => match state.phrases_mode() {
|
||||
Some(PhrasesMode::Rename(..)) => {
|
||||
Self::Rename(Rename::input_to_command(state, input)?)
|
||||
},
|
||||
Some(PhrasesMode::Length(..)) => {
|
||||
Self::Length(Length::input_to_command(state, input)?)
|
||||
},
|
||||
_ => return None
|
||||
}
|
||||
use FileBrowserCommand as Browse;
|
||||
Some(match state.phrases_mode() {
|
||||
Some(PhrasesMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?),
|
||||
Some(PhrasesMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?),
|
||||
Some(PhrasesMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?),
|
||||
Some(PhrasesMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?),
|
||||
_ => to_phrases_command(state, input)?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn to_phrases_command <T: PhrasesControl> (state: &T, input: &TuiInput) -> Option<PhrasesCommand> {
|
||||
use KeyCode::{Up, Down, Delete, Char};
|
||||
use PhrasesCommand as Cmd;
|
||||
use PhrasePoolCommand as Pool;
|
||||
use PhraseRenameCommand as Rename;
|
||||
use PhraseLengthCommand as Length;
|
||||
use FileBrowserCommand as Browse;
|
||||
let index = state.phrase_index();
|
||||
let count = state.phrases().len();
|
||||
Some(match input.event() {
|
||||
key!(Char('n')) => Cmd::Rename(Rename::Begin),
|
||||
key!(Char('t')) => Cmd::Length(Length::Begin),
|
||||
key!(Char('m')) => Cmd::Import(Browse::Begin),
|
||||
key!(Char('x')) => Cmd::Export(Browse::Begin),
|
||||
key!(Up) => Cmd::Select(index.overflowing_sub(1).0.min(state.phrases().len() - 1)),
|
||||
key!(Down) => Cmd::Select(index.saturating_add(1) % state.phrases().len()),
|
||||
key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())),
|
||||
key!(Char(',')) => if index > 1 {
|
||||
state.set_phrase_index(state.phrase_index().saturating_sub(1));
|
||||
Cmd::Phrase(Pool::Swap(index - 1, index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
key!(Char('.')) => if index < count.saturating_sub(1) {
|
||||
state.set_phrase_index(state.phrase_index() + 1);
|
||||
Cmd::Phrase(Pool::Swap(index + 1, index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
key!(Delete) => if index > 0 {
|
||||
state.set_phrase_index(index.min(count.saturating_sub(1)));
|
||||
Cmd::Phrase(Pool::Delete(index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
key!(Char('a')) => Cmd::Phrase(Pool::Add(
|
||||
count, Phrase::new(
|
||||
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
|
||||
)
|
||||
)),
|
||||
key!(Char('i')) => Cmd::Phrase(Pool::Add(
|
||||
index + 1, Phrase::new(
|
||||
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
|
||||
)
|
||||
)),
|
||||
key!(Char('d')) => {
|
||||
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
|
||||
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
|
||||
Cmd::Phrase(Pool::Add(index + 1, phrase))
|
||||
},
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
|
||||
impl<T: PhrasesControl> InputToCommand<Tui, T> for FileBrowserCommand {
|
||||
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PhrasesControl> InputToCommand<Tui, T> for PhraseLengthCommand {
|
||||
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
|
||||
use KeyCode::{Up, Down, Right, Left, Enter, Esc};
|
||||
|
|
|
|||
|
|
@ -155,6 +155,72 @@ impl Default for PhrasesModel {
|
|||
}
|
||||
}
|
||||
|
||||
/// Modes for phrase pool
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PhrasesMode {
|
||||
/// Renaming a pattern
|
||||
Rename(usize, String),
|
||||
/// Editing the length of a pattern
|
||||
Length(usize, usize, PhraseLengthFocus),
|
||||
/// Load phrase from disk
|
||||
Import(usize, FileBrowser),
|
||||
/// Save phrase to disk
|
||||
Export(usize, FileBrowser),
|
||||
}
|
||||
|
||||
/// Browses for phrase to import/export
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileBrowser {
|
||||
pub cwd: PathBuf,
|
||||
pub dirs: Vec<PathBuf>,
|
||||
pub files: Vec<PathBuf>,
|
||||
pub index: usize,
|
||||
pub scroll: usize,
|
||||
pub size: Measure<Tui>
|
||||
}
|
||||
|
||||
impl FileBrowser {
|
||||
pub fn new () -> Self {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays and edits phrase length.
|
||||
pub struct PhraseLength {
|
||||
/// Pulses per beat (quaver)
|
||||
pub ppq: usize,
|
||||
/// Beats per bar
|
||||
pub bpb: usize,
|
||||
/// Length of phrase in pulses
|
||||
pub pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<PhraseLengthFocus>,
|
||||
}
|
||||
|
||||
impl PhraseLength {
|
||||
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> 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) -> String {
|
||||
format!("{}", self.bars())
|
||||
}
|
||||
pub fn beats_string (&self) -> String {
|
||||
format!("{}", self.beats())
|
||||
}
|
||||
pub fn ticks_string (&self) -> String {
|
||||
format!("{:>02}", self.ticks())
|
||||
}
|
||||
}
|
||||
|
||||
impl HasScenes<ArrangerScene> for ArrangerTui {
|
||||
fn scenes (&self) -> &Vec<ArrangerScene> {
|
||||
&self.scenes
|
||||
|
|
@ -267,48 +333,3 @@ impl ArrangerTrackApi for ArrangerTrack {
|
|||
self.color
|
||||
}
|
||||
}
|
||||
|
||||
/// Modes for phrase pool
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PhrasesMode {
|
||||
/// Renaming a pattern
|
||||
Rename(usize, String),
|
||||
/// Editing the length of a pattern
|
||||
Length(usize, usize, PhraseLengthFocus),
|
||||
}
|
||||
|
||||
/// Displays and edits phrase length.
|
||||
pub struct PhraseLength {
|
||||
/// Pulses per beat (quaver)
|
||||
pub ppq: usize,
|
||||
/// Beats per bar
|
||||
pub bpb: usize,
|
||||
/// Length of phrase in pulses
|
||||
pub pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<PhraseLengthFocus>,
|
||||
}
|
||||
|
||||
impl PhraseLength {
|
||||
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> 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) -> String {
|
||||
format!("{}", self.bars())
|
||||
}
|
||||
pub fn beats_string (&self) -> String {
|
||||
format!("{}", self.beats())
|
||||
}
|
||||
pub fn ticks_string (&self) -> String {
|
||||
format!("{:>02}", self.ticks())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue