midi import/export browser, pt.1

This commit is contained in:
🪞👃🪞 2024-11-24 18:34:32 +01:00
parent 26eb5f0315
commit 6d8dbc6780
5 changed files with 263 additions and 140 deletions

View file

@ -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")

View file

@ -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
}))
}
}

View file

@ -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)
}
}

View file

@ -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};

View file

@ -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())
}
}