mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-08 20:56:43 +01:00
compaaaaact
This commit is contained in:
parent
4ce4742959
commit
271f431a6a
25 changed files with 1367 additions and 1361 deletions
|
|
@ -1,4 +1,12 @@
|
|||
use crate::*;
|
||||
use crate::{
|
||||
*,
|
||||
api::{
|
||||
ArrangerTrackCommand,
|
||||
ArrangerSceneCommand,
|
||||
ArrangerClipCommand
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for ArrangerTui {
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
|
|
@ -853,3 +861,451 @@ pub fn arranger_content_horizontal (
|
|||
//)
|
||||
//)
|
||||
//}
|
||||
|
||||
impl HasScenes<ArrangerScene> for ArrangerTui {
|
||||
fn scenes (&self) -> &Vec<ArrangerScene> {
|
||||
&self.scenes
|
||||
}
|
||||
fn scenes_mut (&mut self) -> &mut Vec<ArrangerScene> {
|
||||
&mut self.scenes
|
||||
}
|
||||
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
|
||||
-> Usually<&mut ArrangerScene>
|
||||
{
|
||||
let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string());
|
||||
let scene = ArrangerScene {
|
||||
name: Arc::new(name.into()),
|
||||
clips: vec![None;self.tracks().len()],
|
||||
color: color.unwrap_or_else(||ItemColor::random()),
|
||||
};
|
||||
self.scenes_mut().push(scene);
|
||||
let index = self.scenes().len() - 1;
|
||||
Ok(&mut self.scenes_mut()[index])
|
||||
}
|
||||
fn selected_scene (&self) -> Option<&ArrangerScene> {
|
||||
self.selected.scene().map(|s|self.scenes().get(s)).flatten()
|
||||
}
|
||||
fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
|
||||
self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct ArrangerScene {
|
||||
/// Name of scene
|
||||
pub(crate) name: Arc<RwLock<String>>,
|
||||
/// Clips in scene, one per track
|
||||
pub(crate) clips: Vec<Option<Arc<RwLock<Phrase>>>>,
|
||||
/// Identifying color of scene
|
||||
pub(crate) color: ItemColor,
|
||||
}
|
||||
|
||||
impl ArrangerSceneApi for ArrangerScene {
|
||||
fn name (&self) -> &Arc<RwLock<String>> {
|
||||
&self.name
|
||||
}
|
||||
fn clips (&self) -> &Vec<Option<Arc<RwLock<Phrase>>>> {
|
||||
&self.clips
|
||||
}
|
||||
fn color (&self) -> ItemColor {
|
||||
self.color
|
||||
}
|
||||
}
|
||||
|
||||
impl HasTracks<ArrangerTrack> for ArrangerTui {
|
||||
fn tracks (&self) -> &Vec<ArrangerTrack> {
|
||||
&self.tracks
|
||||
}
|
||||
fn tracks_mut (&mut self) -> &mut Vec<ArrangerTrack> {
|
||||
&mut self.tracks
|
||||
}
|
||||
}
|
||||
|
||||
impl ArrangerTracksApi<ArrangerTrack> for ArrangerTui {
|
||||
fn track_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
|
||||
-> Usually<&mut ArrangerTrack>
|
||||
{
|
||||
let name = name.map_or_else(||self.track_default_name(), |x|x.to_string());
|
||||
let track = ArrangerTrack {
|
||||
width: name.len() + 2,
|
||||
name: Arc::new(name.into()),
|
||||
color: color.unwrap_or_else(||ItemColor::random()),
|
||||
player: PhrasePlayerModel::from(&self.clock),
|
||||
};
|
||||
self.tracks_mut().push(track);
|
||||
let index = self.tracks().len() - 1;
|
||||
Ok(&mut self.tracks_mut()[index])
|
||||
}
|
||||
fn track_del (&mut self, index: usize) {
|
||||
self.tracks_mut().remove(index);
|
||||
for scene in self.scenes_mut().iter_mut() {
|
||||
scene.clips.remove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArrangerTrack {
|
||||
/// Name of track
|
||||
pub(crate) name: Arc<RwLock<String>>,
|
||||
/// Preferred width of track column
|
||||
pub(crate) width: usize,
|
||||
/// Identifying color of track
|
||||
pub(crate) color: ItemColor,
|
||||
/// MIDI player state
|
||||
pub(crate) player: PhrasePlayerModel,
|
||||
}
|
||||
|
||||
impl HasPlayer for ArrangerTrack {
|
||||
fn player (&self) -> &impl MidiPlayerApi {
|
||||
&self.player
|
||||
}
|
||||
fn player_mut (&mut self) -> &mut impl MidiPlayerApi {
|
||||
&mut self.player
|
||||
}
|
||||
}
|
||||
|
||||
impl ArrangerTrackApi for ArrangerTrack {
|
||||
/// Name of track
|
||||
fn name (&self) -> &Arc<RwLock<String>> {
|
||||
&self.name
|
||||
}
|
||||
/// Preferred width of track column
|
||||
fn width (&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
/// Preferred width of track column
|
||||
fn width_mut (&mut self) -> &mut usize {
|
||||
&mut self.width
|
||||
}
|
||||
/// Identifying color of track
|
||||
fn color (&self) -> ItemColor {
|
||||
self.color
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Copy, Debug)]
|
||||
/// Represents the current user selection in the arranger
|
||||
pub enum ArrangerSelection {
|
||||
/// The whole mix is selected
|
||||
Mix,
|
||||
/// A track is selected.
|
||||
Track(usize),
|
||||
/// A scene is selected.
|
||||
Scene(usize),
|
||||
/// A clip (track × scene) is selected.
|
||||
Clip(usize, usize),
|
||||
}
|
||||
|
||||
/// Focus identification methods
|
||||
impl ArrangerSelection {
|
||||
pub fn description <E: Engine> (
|
||||
&self,
|
||||
tracks: &Vec<ArrangerTrack>,
|
||||
scenes: &Vec<ArrangerScene>,
|
||||
) -> String {
|
||||
format!("Selected: {}", match self {
|
||||
Self::Mix => format!("Everything"),
|
||||
Self::Track(t) => match tracks.get(*t) {
|
||||
Some(track) => format!("T{t}: {}", &track.name.read().unwrap()),
|
||||
None => format!("T??"),
|
||||
},
|
||||
Self::Scene(s) => match scenes.get(*s) {
|
||||
Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()),
|
||||
None => format!("S??"),
|
||||
},
|
||||
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
|
||||
(Some(_), Some(scene)) => match scene.clip(*t) {
|
||||
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
|
||||
None => format!("T{t} S{s}: Empty")
|
||||
},
|
||||
_ => format!("T{t} S{s}: Empty"),
|
||||
}
|
||||
})
|
||||
}
|
||||
pub fn is_mix (&self) -> bool {
|
||||
match self { Self::Mix => true, _ => false }
|
||||
}
|
||||
pub fn is_track (&self) -> bool {
|
||||
match self { Self::Track(_) => true, _ => false }
|
||||
}
|
||||
pub fn is_scene (&self) -> bool {
|
||||
match self { Self::Scene(_) => true, _ => false }
|
||||
}
|
||||
pub fn is_clip (&self) -> bool {
|
||||
match self { Self::Clip(_, _) => true, _ => false }
|
||||
}
|
||||
pub fn track (&self) -> Option<usize> {
|
||||
use ArrangerSelection::*;
|
||||
match self {
|
||||
Clip(t, _) => Some(*t),
|
||||
Track(t) => Some(*t),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
pub fn scene (&self) -> Option<usize> {
|
||||
use ArrangerSelection::*;
|
||||
match self {
|
||||
Clip(_, s) => Some(*s),
|
||||
Scene(s) => Some(*s),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Handle<Tui> for ArrangerTui {
|
||||
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
||||
ArrangerCommand::execute_with_state(self, i)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ArrangerCommand {
|
||||
Focus(FocusCommand),
|
||||
Undo,
|
||||
Redo,
|
||||
Clear,
|
||||
Color(ItemColor),
|
||||
Clock(ClockCommand),
|
||||
Scene(ArrangerSceneCommand),
|
||||
Track(ArrangerTrackCommand),
|
||||
Clip(ArrangerClipCommand),
|
||||
Select(ArrangerSelection),
|
||||
Zoom(usize),
|
||||
Phrases(PhrasesCommand),
|
||||
Editor(PhraseCommand),
|
||||
}
|
||||
|
||||
impl Command<ArrangerTui> for ArrangerCommand {
|
||||
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
use ArrangerCommand::*;
|
||||
Ok(match self {
|
||||
Focus(cmd) => cmd.execute(state)?.map(Focus),
|
||||
Scene(cmd) => cmd.execute(state)?.map(Scene),
|
||||
Track(cmd) => cmd.execute(state)?.map(Track),
|
||||
Clip(cmd) => cmd.execute(state)?.map(Clip),
|
||||
Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases),
|
||||
Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor),
|
||||
Clock(cmd) => cmd.execute(state)?.map(Clock),
|
||||
Zoom(_) => { todo!(); },
|
||||
Select(selected) => {
|
||||
*state.selected_mut() = selected;
|
||||
None
|
||||
},
|
||||
_ => { todo!() }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<ArrangerTui> for ArrangerSceneCommand {
|
||||
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
//todo!();
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<ArrangerTui> for ArrangerTrackCommand {
|
||||
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
//todo!();
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<ArrangerTui> for ArrangerClipCommand {
|
||||
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
//todo!();
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ArrangerControl: TransportControl {
|
||||
fn selected (&self) -> ArrangerSelection;
|
||||
fn selected_mut (&mut self) -> &mut ArrangerSelection;
|
||||
fn activate (&mut self) -> Usually<()>;
|
||||
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>>;
|
||||
fn toggle_loop (&mut self);
|
||||
fn randomize_color (&mut self);
|
||||
}
|
||||
|
||||
impl ArrangerControl for ArrangerTui {
|
||||
fn selected (&self) -> ArrangerSelection {
|
||||
self.selected
|
||||
}
|
||||
fn selected_mut (&mut self) -> &mut ArrangerSelection {
|
||||
&mut self.selected
|
||||
}
|
||||
fn activate (&mut self) -> Usually<()> {
|
||||
if let ArrangerSelection::Scene(s) = self.selected {
|
||||
for (t, track) in self.tracks.iter_mut().enumerate() {
|
||||
let phrase = self.scenes[s].clips[t].clone();
|
||||
if track.player.play_phrase.is_some() || phrase.is_some() {
|
||||
track.player.enqueue_next(phrase.as_ref());
|
||||
}
|
||||
}
|
||||
if self.clock().is_stopped() {
|
||||
self.clock().play_from(Some(0))?;
|
||||
}
|
||||
} else if let ArrangerSelection::Clip(t, s) = self.selected {
|
||||
let phrase = self.scenes()[s].clips[t].clone();
|
||||
self.tracks_mut()[t].player.enqueue_next(phrase.as_ref());
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>> {
|
||||
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
|
||||
}
|
||||
fn toggle_loop (&mut self) {
|
||||
if let Some(phrase) = self.selected_phrase() {
|
||||
phrase.write().unwrap().toggle_loop()
|
||||
}
|
||||
}
|
||||
fn randomize_color (&mut self) {
|
||||
match self.selected {
|
||||
ArrangerSelection::Mix => {
|
||||
self.color = ItemColor::random_dark()
|
||||
},
|
||||
ArrangerSelection::Track(t) => {
|
||||
self.tracks_mut()[t].color = ItemColor::random()
|
||||
},
|
||||
ArrangerSelection::Scene(s) => {
|
||||
self.scenes_mut()[s].color = ItemColor::random()
|
||||
},
|
||||
ArrangerSelection::Clip(t, s) => {
|
||||
if let Some(phrase) = &self.scenes_mut()[s].clips[t] {
|
||||
phrase.write().unwrap().color = ItemColorTriplet::random();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
|
||||
fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option<Self> {
|
||||
to_arranger_command(state, input)
|
||||
.or_else(||to_focus_command(input).map(ArrangerCommand::Focus))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
|
||||
use ArrangerCommand as Cmd;
|
||||
use KeyCode::Char;
|
||||
if !state.entered() {
|
||||
return None
|
||||
}
|
||||
Some(match input.event() {
|
||||
key!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(
|
||||
state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()
|
||||
))),
|
||||
_ => match state.focused() {
|
||||
ArrangerFocus::Transport(_) => {
|
||||
match TransportCommand::input_to_command(state, input)? {
|
||||
TransportCommand::Clock(command) => Cmd::Clock(command),
|
||||
_ => return None,
|
||||
}
|
||||
},
|
||||
ArrangerFocus::PhraseEditor => {
|
||||
Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?)
|
||||
},
|
||||
ArrangerFocus::Phrases => {
|
||||
Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?)
|
||||
},
|
||||
ArrangerFocus::Arranger => {
|
||||
use ArrangerSelection::*;
|
||||
match input.event() {
|
||||
key!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)),
|
||||
key!(Char('+')) => Cmd::Zoom(0), // TODO
|
||||
key!(Char('=')) => Cmd::Zoom(0), // TODO
|
||||
key!(Char('_')) => Cmd::Zoom(0), // TODO
|
||||
key!(Char('-')) => Cmd::Zoom(0), // TODO
|
||||
key!(Char('`')) => { todo!("toggle state mode") },
|
||||
key!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add),
|
||||
key!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add),
|
||||
_ => match state.selected() {
|
||||
Mix => to_arranger_mix_command(input)?,
|
||||
Track(t) => to_arranger_track_command(input, t)?,
|
||||
Scene(s) => to_arranger_scene_command(input, s)?,
|
||||
Clip(t, s) => to_arranger_clip_command(input, t, s)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn to_arranger_mix_command (input: &TuiInput) -> Option<ArrangerCommand> {
|
||||
use KeyCode::{Char, Down, Right, Delete};
|
||||
use ArrangerCommand as Cmd;
|
||||
use ArrangerSelection as Select;
|
||||
Some(match input.event() {
|
||||
key!(Down) => Cmd::Select(Select::Scene(0)),
|
||||
key!(Right) => Cmd::Select(Select::Track(0)),
|
||||
key!(Char(',')) => Cmd::Zoom(0),
|
||||
key!(Char('.')) => Cmd::Zoom(0),
|
||||
key!(Char('<')) => Cmd::Zoom(0),
|
||||
key!(Char('>')) => Cmd::Zoom(0),
|
||||
key!(Delete) => Cmd::Clear,
|
||||
key!(Char('c')) => Cmd::Color(ItemColor::random()),
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
|
||||
fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option<ArrangerCommand> {
|
||||
use KeyCode::{Char, Down, Left, Right, Delete};
|
||||
use ArrangerCommand as Cmd;
|
||||
use ArrangerSelection as Select;
|
||||
use ArrangerTrackCommand as Track;
|
||||
Some(match input.event() {
|
||||
key!(Down) => Cmd::Select(Select::Clip(t, 0)),
|
||||
key!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }),
|
||||
key!(Right) => Cmd::Select(Select::Track(t + 1)),
|
||||
key!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)),
|
||||
key!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)),
|
||||
key!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)),
|
||||
key!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)),
|
||||
key!(Delete) => Cmd::Track(Track::Delete(t)),
|
||||
//key!(Char('c')) => Cmd::Track(Track::Color(t, ItemColor::random())),
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
|
||||
fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option<ArrangerCommand> {
|
||||
use KeyCode::{Char, Up, Down, Right, Enter, Delete};
|
||||
use ArrangerCommand as Cmd;
|
||||
use ArrangerSelection as Select;
|
||||
use ArrangerSceneCommand as Scene;
|
||||
Some(match input.event() {
|
||||
key!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }),
|
||||
key!(Down) => Cmd::Select(Select::Scene(s + 1)),
|
||||
key!(Right) => Cmd::Select(Select::Clip(0, s)),
|
||||
key!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)),
|
||||
key!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)),
|
||||
key!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)),
|
||||
key!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)),
|
||||
key!(Enter) => Cmd::Scene(Scene::Play(s)),
|
||||
key!(Delete) => Cmd::Scene(Scene::Delete(s)),
|
||||
//key!(Char('c')) => Cmd::Track(Scene::Color(s, ItemColor::random())),
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
|
||||
fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option<ArrangerCommand> {
|
||||
use KeyCode::{Char, Up, Down, Left, Right, Delete};
|
||||
use ArrangerCommand as Cmd;
|
||||
use ArrangerSelection as Select;
|
||||
use ArrangerClipCommand as Clip;
|
||||
Some(match input.event() {
|
||||
key!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }),
|
||||
key!(Down) => Cmd::Select(Select::Clip(t, s + 1)),
|
||||
key!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }),
|
||||
key!(Right) => Cmd::Select(Select::Clip(t + 1, s)),
|
||||
key!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||
key!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||
key!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||
key!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||
key!(Delete) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||
//key!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemColor::random())),
|
||||
//key!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))),
|
||||
//key!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))),
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,30 @@
|
|||
use crate::*;
|
||||
use crate::{*, api::ClockCommand::{Play, Pause}};
|
||||
use super::phrase_editor::PhraseCommand::Show;
|
||||
use super::app_transport::TransportCommand;
|
||||
use KeyCode::{Char, Enter};
|
||||
use SequencerCommand::*;
|
||||
use SequencerFocus::*;
|
||||
use super::app_transport::TransportFocus::*;
|
||||
|
||||
/// Create app state from JACK handle.
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerTui {
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||
let clock = ClockModel::from(jack);
|
||||
let mut phrase = Phrase::default();
|
||||
phrase.name = "New".into();
|
||||
phrase.color = ItemColor::random().into();
|
||||
phrase.set_length(384);
|
||||
let mut phrases = PhraseListModel::default();
|
||||
let phrase = Arc::new(RwLock::new(phrase));
|
||||
phrases.phrases.push(phrase.clone());
|
||||
phrases.phrase.store(1, Ordering::Relaxed);
|
||||
let mut editor = PhraseEditorModel::default();
|
||||
editor.show_phrase(Some(phrase));
|
||||
Ok(Self {
|
||||
jack: jack.clone(),
|
||||
phrases: PhraseListModel::default(),
|
||||
phrases: phrases,
|
||||
player: PhrasePlayerModel::from(&clock),
|
||||
editor: PhraseEditorModel::default(),
|
||||
editor: editor,
|
||||
size: Measure::new(),
|
||||
cursor: (0, 0),
|
||||
entered: false,
|
||||
|
|
@ -20,7 +33,7 @@ impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerTui {
|
|||
note_buf: vec![],
|
||||
clock,
|
||||
perf: PerfModel::default(),
|
||||
focus: FocusState::Focused(SequencerFocus::Transport(TransportFocus::PlayPause))
|
||||
focus: FocusState::Focused(SequencerFocus::PhraseEditor)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -216,6 +229,7 @@ impl StatusBar for SequencerStatusBar {
|
|||
|
||||
impl From<&SequencerTui> for SequencerStatusBar {
|
||||
fn from (state: &SequencerTui) -> Self {
|
||||
use super::app_transport::TransportFocus::*;
|
||||
let samples = state.clock.chunk.load(Ordering::Relaxed);
|
||||
let rate = state.clock.timebase.sr.get() as f64;
|
||||
let buffer = samples as f64 / rate;
|
||||
|
|
@ -358,3 +372,95 @@ render!(|self:SequencerStats<'a>|{
|
|||
Tui::fg(orange, size),
|
||||
]))
|
||||
});
|
||||
|
||||
impl Handle<Tui> for SequencerTui {
|
||||
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
||||
SequencerCommand::execute_with_state(self, i)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SequencerCommand {
|
||||
Focus(FocusCommand),
|
||||
Clock(ClockCommand),
|
||||
Phrases(PhrasesCommand),
|
||||
Editor(PhraseCommand),
|
||||
Enqueue(Option<Arc<RwLock<Phrase>>>),
|
||||
Clear,
|
||||
Undo,
|
||||
Redo,
|
||||
}
|
||||
|
||||
impl Command<SequencerTui> for SequencerCommand {
|
||||
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
|
||||
Ok(match self {
|
||||
Self::Focus(cmd) => cmd.execute(state)?.map(Focus),
|
||||
Self::Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases),
|
||||
Self::Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor),
|
||||
Self::Clock(cmd) => cmd.execute(state)?.map(Clock),
|
||||
Self::Enqueue(phrase) => {
|
||||
state.player.enqueue_next(phrase.as_ref());
|
||||
None
|
||||
},
|
||||
Self::Undo => { todo!() },
|
||||
Self::Redo => { todo!() },
|
||||
Self::Clear => { todo!() },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
|
||||
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
|
||||
if state.entered() {
|
||||
to_sequencer_command(state, input)
|
||||
.or_else(||to_focus_command(input).map(SequencerCommand::Focus))
|
||||
} else {
|
||||
to_focus_command(input).map(SequencerCommand::Focus)
|
||||
.or_else(||to_sequencer_command(state, input))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option<SequencerCommand> {
|
||||
Some(match input.event() {
|
||||
// Play/pause
|
||||
key!(Char(' ')) => Clock(
|
||||
if state.clock().is_stopped() { Play(None) } else { Pause(None) }
|
||||
),
|
||||
// Play from start/rewind to start
|
||||
key!(Shift-Char(' ')) => Clock(
|
||||
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
|
||||
),
|
||||
// Edit phrase
|
||||
key!(Char('e')) => match state.focused() {
|
||||
SequencerFocus::PhrasePlay => Editor(Show(
|
||||
state.player.play_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone())
|
||||
)),
|
||||
SequencerFocus::PhraseNext => Editor(Show(
|
||||
state.player.next_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone())
|
||||
)),
|
||||
SequencerFocus::PhraseList => Editor(Show(
|
||||
Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())
|
||||
)),
|
||||
_ => return None,
|
||||
},
|
||||
_ => match state.focused() {
|
||||
SequencerFocus::Transport(_) => match TransportCommand::input_to_command(state, input)? {
|
||||
TransportCommand::Clock(command) => Clock(command),
|
||||
_ => return None,
|
||||
},
|
||||
SequencerFocus::PhraseEditor => Editor(
|
||||
PhraseCommand::input_to_command(&state.editor, input)?
|
||||
),
|
||||
SequencerFocus::PhraseList => match input.event() {
|
||||
key!(Enter) => Enqueue(Some(
|
||||
state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()
|
||||
)),
|
||||
_ => Phrases(
|
||||
PhrasesCommand::input_to_command(&state.phrases, input)?
|
||||
),
|
||||
}
|
||||
_ => return None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,10 +82,10 @@ impl<T: HasClock> From<&T> for TransportView {
|
|||
bpm,
|
||||
ppq,
|
||||
started: true,
|
||||
global_sample: format!("{:.0}", started.sample.get()),
|
||||
global_second: format!("{:.0}", started.usec.get()),
|
||||
current_sample: format!("{:.0}", clock.global.sample.get() - started.sample.get()),
|
||||
current_second: format!("{:.0}", clock.global.usec.get() - started.usec.get()),
|
||||
global_sample: format!("{:.0}k", started.sample.get()/1000.),
|
||||
global_second: format!("{:.1}s", started.usec.get()/1000.),
|
||||
current_sample: format!("{:.0}k", (clock.global.sample.get() - started.sample.get())/1000.),
|
||||
current_second: format!("{:.1}s", (clock.global.usec.get() - started.usec.get())/1000000.),
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
|
|
@ -93,8 +93,8 @@ impl<T: HasClock> From<&T> for TransportView {
|
|||
bpm,
|
||||
ppq,
|
||||
started: false,
|
||||
global_sample: format!("{:.0}", clock.global.sample.get()),
|
||||
global_second: format!("{:.0}", clock.global.usec.get()),
|
||||
global_sample: format!("{:.0}k", clock.global.sample.get()/1000.),
|
||||
global_second: format!("{:.1}s", clock.global.usec.get()/1000000.),
|
||||
current_sample: "".to_string(),
|
||||
current_second: "".to_string(),
|
||||
}
|
||||
|
|
@ -102,37 +102,43 @@ impl<T: HasClock> From<&T> for TransportView {
|
|||
}
|
||||
}
|
||||
|
||||
struct TransportField<'a>(&'a str, &'a str);
|
||||
render!(|self: TransportField<'a>|{
|
||||
col!([
|
||||
Tui::fg(Color::Rgb(150, 150, 150), self.0),
|
||||
Tui::bold(true, Tui::fg(Color::Rgb(200, 200, 200), self.1)),
|
||||
])
|
||||
});
|
||||
|
||||
render!(|self: TransportView|{
|
||||
let bg = TuiTheme::border_bg();
|
||||
let border_style = Style::default().bg(bg).fg(TuiTheme::border_fg(false));
|
||||
lay!([
|
||||
Tui::fill_x(Lozenge(border_style)),
|
||||
Tui::bg(bg, Tui::outset_xy(1, 1, row!([
|
||||
Tui::outset_x(1, row!([
|
||||
row!([
|
||||
col!(["SR", self.sr ]), " ",
|
||||
col!(["BPM", self.bpm]), " ",
|
||||
col!(["PPQ", self.ppq]), " ",
|
||||
]),
|
||||
row!([
|
||||
col!(["Sample", self.global_sample]), " ",
|
||||
col!(["Second", self.global_second]), " ",
|
||||
TransportField("SR ", self.sr.as_str()),
|
||||
" ",
|
||||
TransportField("BPM ", self.bpm.as_str()),
|
||||
" ",
|
||||
TransportField("PPQ ", self.ppq.as_str()),
|
||||
]),
|
||||
lay!(|add|{
|
||||
if self.started {
|
||||
add(&row!([
|
||||
col!(["", Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING ")]),
|
||||
" ",
|
||||
col!(["Sample", self.current_sample]),
|
||||
TransportField("Beat", "00X+0/0B+00/00P"),
|
||||
" ",
|
||||
col!(["Second", self.current_second]),
|
||||
TransportField("Second", self.current_second.as_str()),
|
||||
" ",
|
||||
col!(["Beat", "00B 0b 00/00"]),
|
||||
TransportField("Sample", self.current_sample.as_str()),
|
||||
]))
|
||||
} else {
|
||||
add(&col!([Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED "), ""]))
|
||||
}
|
||||
}),
|
||||
])))
|
||||
]))
|
||||
])
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,266 +0,0 @@
|
|||
use super::model_arranger::ArrangerSelection;
|
||||
use crate::{
|
||||
*,
|
||||
api::{
|
||||
ArrangerTrackCommand,
|
||||
ArrangerSceneCommand,
|
||||
ArrangerClipCommand
|
||||
}
|
||||
};
|
||||
|
||||
impl Handle<Tui> for ArrangerTui {
|
||||
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
||||
ArrangerCommand::execute_with_state(self, i)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ArrangerCommand {
|
||||
Focus(FocusCommand),
|
||||
Undo,
|
||||
Redo,
|
||||
Clear,
|
||||
Color(ItemColor),
|
||||
Clock(ClockCommand),
|
||||
Scene(ArrangerSceneCommand),
|
||||
Track(ArrangerTrackCommand),
|
||||
Clip(ArrangerClipCommand),
|
||||
Select(ArrangerSelection),
|
||||
Zoom(usize),
|
||||
Phrases(PhrasesCommand),
|
||||
Editor(PhraseCommand),
|
||||
}
|
||||
|
||||
impl Command<ArrangerTui> for ArrangerCommand {
|
||||
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
use ArrangerCommand::*;
|
||||
Ok(match self {
|
||||
Focus(cmd) => cmd.execute(state)?.map(Focus),
|
||||
Scene(cmd) => cmd.execute(state)?.map(Scene),
|
||||
Track(cmd) => cmd.execute(state)?.map(Track),
|
||||
Clip(cmd) => cmd.execute(state)?.map(Clip),
|
||||
Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases),
|
||||
Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor),
|
||||
Clock(cmd) => cmd.execute(state)?.map(Clock),
|
||||
Zoom(_) => { todo!(); },
|
||||
Select(selected) => {
|
||||
*state.selected_mut() = selected;
|
||||
None
|
||||
},
|
||||
_ => { todo!() }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<ArrangerTui> for ArrangerSceneCommand {
|
||||
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
//todo!();
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<ArrangerTui> for ArrangerTrackCommand {
|
||||
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
//todo!();
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<ArrangerTui> for ArrangerClipCommand {
|
||||
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
//todo!();
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ArrangerControl: TransportControl {
|
||||
fn selected (&self) -> ArrangerSelection;
|
||||
fn selected_mut (&mut self) -> &mut ArrangerSelection;
|
||||
fn activate (&mut self) -> Usually<()>;
|
||||
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>>;
|
||||
fn toggle_loop (&mut self);
|
||||
fn randomize_color (&mut self);
|
||||
}
|
||||
|
||||
impl ArrangerControl for ArrangerTui {
|
||||
fn selected (&self) -> ArrangerSelection {
|
||||
self.selected
|
||||
}
|
||||
fn selected_mut (&mut self) -> &mut ArrangerSelection {
|
||||
&mut self.selected
|
||||
}
|
||||
fn activate (&mut self) -> Usually<()> {
|
||||
if let ArrangerSelection::Scene(s) = self.selected {
|
||||
for (t, track) in self.tracks.iter_mut().enumerate() {
|
||||
let phrase = self.scenes[s].clips[t].clone();
|
||||
if track.player.play_phrase.is_some() || phrase.is_some() {
|
||||
track.player.enqueue_next(phrase.as_ref());
|
||||
}
|
||||
}
|
||||
if self.clock().is_stopped() {
|
||||
self.clock().play_from(Some(0))?;
|
||||
}
|
||||
} else if let ArrangerSelection::Clip(t, s) = self.selected {
|
||||
let phrase = self.scenes()[s].clips[t].clone();
|
||||
self.tracks_mut()[t].player.enqueue_next(phrase.as_ref());
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>> {
|
||||
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
|
||||
}
|
||||
fn toggle_loop (&mut self) {
|
||||
if let Some(phrase) = self.selected_phrase() {
|
||||
phrase.write().unwrap().toggle_loop()
|
||||
}
|
||||
}
|
||||
fn randomize_color (&mut self) {
|
||||
match self.selected {
|
||||
ArrangerSelection::Mix => {
|
||||
self.color = ItemColor::random_dark()
|
||||
},
|
||||
ArrangerSelection::Track(t) => {
|
||||
self.tracks_mut()[t].color = ItemColor::random()
|
||||
},
|
||||
ArrangerSelection::Scene(s) => {
|
||||
self.scenes_mut()[s].color = ItemColor::random()
|
||||
},
|
||||
ArrangerSelection::Clip(t, s) => {
|
||||
if let Some(phrase) = &self.scenes_mut()[s].clips[t] {
|
||||
phrase.write().unwrap().color = ItemColorTriplet::random();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
|
||||
fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option<Self> {
|
||||
to_arranger_command(state, input)
|
||||
.or_else(||to_focus_command(input).map(ArrangerCommand::Focus))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
|
||||
use ArrangerCommand as Cmd;
|
||||
use KeyCode::Char;
|
||||
if !state.entered() {
|
||||
return None
|
||||
}
|
||||
Some(match input.event() {
|
||||
key!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(
|
||||
state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()
|
||||
))),
|
||||
_ => match state.focused() {
|
||||
ArrangerFocus::Transport(_) => {
|
||||
match TransportCommand::input_to_command(state, input)? {
|
||||
TransportCommand::Clock(command) => Cmd::Clock(command),
|
||||
_ => return None,
|
||||
}
|
||||
},
|
||||
ArrangerFocus::PhraseEditor => {
|
||||
Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?)
|
||||
},
|
||||
ArrangerFocus::Phrases => {
|
||||
Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?)
|
||||
},
|
||||
ArrangerFocus::Arranger => {
|
||||
use ArrangerSelection::*;
|
||||
match input.event() {
|
||||
key!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)),
|
||||
key!(Char('+')) => Cmd::Zoom(0), // TODO
|
||||
key!(Char('=')) => Cmd::Zoom(0), // TODO
|
||||
key!(Char('_')) => Cmd::Zoom(0), // TODO
|
||||
key!(Char('-')) => Cmd::Zoom(0), // TODO
|
||||
key!(Char('`')) => { todo!("toggle state mode") },
|
||||
key!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add),
|
||||
key!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add),
|
||||
_ => match state.selected() {
|
||||
Mix => to_arranger_mix_command(input)?,
|
||||
Track(t) => to_arranger_track_command(input, t)?,
|
||||
Scene(s) => to_arranger_scene_command(input, s)?,
|
||||
Clip(t, s) => to_arranger_clip_command(input, t, s)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn to_arranger_mix_command (input: &TuiInput) -> Option<ArrangerCommand> {
|
||||
use KeyCode::{Char, Down, Right, Delete};
|
||||
use ArrangerCommand as Cmd;
|
||||
use ArrangerSelection as Select;
|
||||
Some(match input.event() {
|
||||
key!(Down) => Cmd::Select(Select::Scene(0)),
|
||||
key!(Right) => Cmd::Select(Select::Track(0)),
|
||||
key!(Char(',')) => Cmd::Zoom(0),
|
||||
key!(Char('.')) => Cmd::Zoom(0),
|
||||
key!(Char('<')) => Cmd::Zoom(0),
|
||||
key!(Char('>')) => Cmd::Zoom(0),
|
||||
key!(Delete) => Cmd::Clear,
|
||||
key!(Char('c')) => Cmd::Color(ItemColor::random()),
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
|
||||
fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option<ArrangerCommand> {
|
||||
use KeyCode::{Char, Down, Left, Right, Delete};
|
||||
use ArrangerCommand as Cmd;
|
||||
use ArrangerSelection as Select;
|
||||
use ArrangerTrackCommand as Track;
|
||||
Some(match input.event() {
|
||||
key!(Down) => Cmd::Select(Select::Clip(t, 0)),
|
||||
key!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }),
|
||||
key!(Right) => Cmd::Select(Select::Track(t + 1)),
|
||||
key!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)),
|
||||
key!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)),
|
||||
key!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)),
|
||||
key!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)),
|
||||
key!(Delete) => Cmd::Track(Track::Delete(t)),
|
||||
//key!(Char('c')) => Cmd::Track(Track::Color(t, ItemColor::random())),
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
|
||||
fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option<ArrangerCommand> {
|
||||
use KeyCode::{Char, Up, Down, Right, Enter, Delete};
|
||||
use ArrangerCommand as Cmd;
|
||||
use ArrangerSelection as Select;
|
||||
use ArrangerSceneCommand as Scene;
|
||||
Some(match input.event() {
|
||||
key!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }),
|
||||
key!(Down) => Cmd::Select(Select::Scene(s + 1)),
|
||||
key!(Right) => Cmd::Select(Select::Clip(0, s)),
|
||||
key!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)),
|
||||
key!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)),
|
||||
key!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)),
|
||||
key!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)),
|
||||
key!(Enter) => Cmd::Scene(Scene::Play(s)),
|
||||
key!(Delete) => Cmd::Scene(Scene::Delete(s)),
|
||||
//key!(Char('c')) => Cmd::Track(Scene::Color(s, ItemColor::random())),
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
|
||||
fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option<ArrangerCommand> {
|
||||
use KeyCode::{Char, Up, Down, Left, Right, Delete};
|
||||
use ArrangerCommand as Cmd;
|
||||
use ArrangerSelection as Select;
|
||||
use ArrangerClipCommand as Clip;
|
||||
Some(match input.event() {
|
||||
key!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }),
|
||||
key!(Down) => Cmd::Select(Select::Clip(t, s + 1)),
|
||||
key!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }),
|
||||
key!(Right) => Cmd::Select(Select::Clip(t + 1, s)),
|
||||
key!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||
key!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||
key!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||
key!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||
key!(Delete) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||
//key!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemColor::random())),
|
||||
//key!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))),
|
||||
//key!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))),
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PhraseCommand {
|
||||
// TODO: 1-9 seek markers that by default start every 8th of the phrase
|
||||
AppendNote,
|
||||
PutNote,
|
||||
SetNoteCursor(usize),
|
||||
SetNoteLength(usize),
|
||||
SetNoteScroll(usize),
|
||||
SetTimeCursor(usize),
|
||||
SetTimeScroll(usize),
|
||||
SetTimeZoom(usize),
|
||||
Show(Option<Arc<RwLock<Phrase>>>),
|
||||
SetEditMode(PhraseEditMode),
|
||||
ToggleDirection,
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhraseEditorModel> for PhraseCommand {
|
||||
fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option<Self> {
|
||||
use PhraseCommand::*;
|
||||
use KeyCode::{Char, Esc, Up, Down, PageUp, PageDown, Left, Right};
|
||||
let note_lo = state.note_lo.load(Ordering::Relaxed);
|
||||
let note_point = state.note_point.load(Ordering::Relaxed);
|
||||
let time_start = state.time_start.load(Ordering::Relaxed);
|
||||
let time_point = state.time_point.load(Ordering::Relaxed);
|
||||
let time_zoom = state.view_mode.time_zoom();
|
||||
let length = state.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
|
||||
Some(match from.event() {
|
||||
key!(Char('`')) => ToggleDirection,
|
||||
key!(Esc) => SetEditMode(PhraseEditMode::Scroll),
|
||||
key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)),
|
||||
key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)),
|
||||
key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)),
|
||||
key!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)),
|
||||
key!(Char('a')) => AppendNote,
|
||||
key!(Char('s')) => PutNote,
|
||||
key!(Char('[')) => SetNoteLength(prev_note_length(state.note_len)),
|
||||
key!(Char(']')) => SetNoteLength(next_note_length(state.note_len)),
|
||||
key!(Char('n')) => { todo!("toggle keys vs notes") },
|
||||
_ => match state.edit_mode {
|
||||
PhraseEditMode::Scroll => match from.event() {
|
||||
key!(Char('e')) => SetEditMode(PhraseEditMode::Note),
|
||||
key!(Up) => SetNoteScroll(note_lo + 1),
|
||||
key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)),
|
||||
key!(PageUp) => SetNoteScroll(note_lo + 3),
|
||||
key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)),
|
||||
key!(Left) => SetTimeScroll(time_start.saturating_sub(1)),
|
||||
key!(Right) => SetTimeScroll(time_start + 1),
|
||||
_ => return None
|
||||
},
|
||||
PhraseEditMode::Note => match from.event() {
|
||||
key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll),
|
||||
key!(Up) => SetNoteCursor(note_point + 1),
|
||||
key!(Down) => SetNoteCursor(note_point.saturating_sub(1)),
|
||||
key!(PageUp) => SetNoteCursor(note_point + 3),
|
||||
key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)),
|
||||
key!(Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)),
|
||||
key!(Right) => SetTimeCursor((time_point + time_zoom) % length),
|
||||
_ => return None
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<PhraseEditorModel> for PhraseCommand {
|
||||
fn execute (self, state: &mut PhraseEditorModel) -> Perhaps<Self> {
|
||||
use PhraseCommand::*;
|
||||
Ok(match self {
|
||||
Show(phrase) => {
|
||||
state.show_phrase(phrase);
|
||||
None
|
||||
},
|
||||
ToggleDirection => {
|
||||
todo!()
|
||||
},
|
||||
SetEditMode(mode) => {
|
||||
state.edit_mode = mode;
|
||||
None
|
||||
}
|
||||
AppendNote => {
|
||||
state.put_note();
|
||||
state.time_cursor_advance();
|
||||
None
|
||||
},
|
||||
PutNote => {
|
||||
state.put_note();
|
||||
None
|
||||
},
|
||||
SetTimeCursor(time) => {
|
||||
state.time_point.store(time, Ordering::Relaxed);
|
||||
None
|
||||
},
|
||||
SetTimeScroll(time) => {
|
||||
state.time_start.store(time, Ordering::Relaxed);
|
||||
None
|
||||
},
|
||||
SetTimeZoom(zoom) => {
|
||||
state.view_mode.set_time_zoom(zoom);
|
||||
state.show_phrase(state.phrase.clone());
|
||||
None
|
||||
},
|
||||
SetNoteScroll(note) => {
|
||||
state.note_lo.store(note, Ordering::Relaxed);
|
||||
None
|
||||
},
|
||||
SetNoteLength(time) => {
|
||||
state.note_len = time;
|
||||
None
|
||||
},
|
||||
SetNoteCursor(note) => {
|
||||
let note = 127.min(note);
|
||||
let start = state.note_lo.load(Ordering::Relaxed);
|
||||
state.note_point.store(note, Ordering::Relaxed);
|
||||
if note < start {
|
||||
state.note_lo.store(note, Ordering::Relaxed);
|
||||
}
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum PhraseEditMode {
|
||||
Note,
|
||||
Scroll,
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
use crate::*;
|
||||
use super::model_phrase_list::{PhraseListModel, PhrasesMode};
|
||||
use super::model_phrase_length::PhraseLengthFocus::*;
|
||||
use PhraseLengthCommand::*;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum PhraseLengthCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Set(usize),
|
||||
Next,
|
||||
Prev,
|
||||
Inc,
|
||||
Dec,
|
||||
}
|
||||
|
||||
impl Command<PhraseListModel> for PhraseLengthCommand {
|
||||
fn execute (self, state: &mut PhraseListModel) -> Perhaps<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() },
|
||||
Inc => match focus {
|
||||
Bar => { *length += 4 * PPQ },
|
||||
Beat => { *length += PPQ },
|
||||
Tick => { *length += 1 },
|
||||
},
|
||||
Dec => match focus {
|
||||
Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||
Beat => { *length = length.saturating_sub(PPQ) },
|
||||
Tick => { *length = length.saturating_sub(1) },
|
||||
},
|
||||
Set(length) => {
|
||||
let mut phrase = state.phrases()[phrase].write().unwrap();
|
||||
let old_length = phrase.length;
|
||||
phrase.length = length;
|
||||
std::mem::drop(phrase);
|
||||
*state.phrases_mode_mut() = None;
|
||||
return Ok(Some(Self::Set(old_length)))
|
||||
},
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
use crate::{
|
||||
*,
|
||||
api::PhrasePoolCommand as Pool,
|
||||
tui::{
|
||||
ctrl_phrase_rename::PhraseRenameCommand as Rename,
|
||||
ctrl_phrase_length::PhraseLengthCommand as Length,
|
||||
ctrl_file_browser::FileBrowserCommand as Browse,
|
||||
}
|
||||
};
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum PhrasesCommand {
|
||||
Select(usize),
|
||||
Phrase(Pool),
|
||||
Rename(Rename),
|
||||
Length(Length),
|
||||
Import(Browse),
|
||||
Export(Browse),
|
||||
}
|
||||
|
||||
impl Command<PhraseListModel> for PhrasesCommand {
|
||||
fn execute (self, state: &mut PhraseListModel) -> Perhaps<Self> {
|
||||
use PhrasesCommand::*;
|
||||
Ok(match self {
|
||||
Phrase(command) => command.execute(state)?.map(Phrase),
|
||||
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)?)
|
||||
);
|
||||
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)?)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Export)
|
||||
},
|
||||
Select(phrase) => {
|
||||
state.set_phrase_index(phrase);
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HasPhrases for PhraseListModel {
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
||||
&self.phrases
|
||||
}
|
||||
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
|
||||
&mut self.phrases
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhraseListModel> for PhrasesCommand {
|
||||
fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option<Self> {
|
||||
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 (state: &PhraseListModel, input: &TuiInput) -> Option<PhrasesCommand> {
|
||||
use KeyCode::{Up, Down, Delete, Char};
|
||||
use PhrasesCommand as Cmd;
|
||||
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!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())),
|
||||
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(',')) => 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
|
||||
})
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
use crate::{*, api::ClockCommand::{Play, Pause}};
|
||||
use super::ctrl_phrase_editor::PhraseCommand::Show;
|
||||
use KeyCode::{Char, Enter};
|
||||
use SequencerCommand::*;
|
||||
use super::app_transport::TransportCommand;
|
||||
|
||||
impl Handle<Tui> for SequencerTui {
|
||||
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
||||
SequencerCommand::execute_with_state(self, i)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SequencerCommand {
|
||||
Focus(FocusCommand),
|
||||
Clock(ClockCommand),
|
||||
Phrases(PhrasesCommand),
|
||||
Editor(PhraseCommand),
|
||||
Enqueue(Option<Arc<RwLock<Phrase>>>),
|
||||
Clear,
|
||||
Undo,
|
||||
Redo,
|
||||
}
|
||||
|
||||
impl Command<SequencerTui> for SequencerCommand {
|
||||
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
|
||||
Ok(match self {
|
||||
Focus(cmd) => cmd.execute(state)?.map(Focus),
|
||||
Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases),
|
||||
Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor),
|
||||
Clock(cmd) => cmd.execute(state)?.map(Clock),
|
||||
Enqueue(phrase) => {
|
||||
state.player.enqueue_next(phrase.as_ref());
|
||||
None
|
||||
},
|
||||
Undo => { todo!() },
|
||||
Redo => { todo!() },
|
||||
Clear => { todo!() },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
|
||||
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
|
||||
if state.entered() {
|
||||
to_sequencer_command(state, input)
|
||||
.or_else(||to_focus_command(input).map(SequencerCommand::Focus))
|
||||
} else {
|
||||
to_focus_command(input).map(SequencerCommand::Focus)
|
||||
.or_else(||to_sequencer_command(state, input))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option<SequencerCommand> {
|
||||
Some(match input.event() {
|
||||
// Play/pause
|
||||
key!(Char(' ')) => Clock(
|
||||
if state.clock().is_stopped() { Play(None) } else { Pause(None) }
|
||||
),
|
||||
// Play from start/rewind to start
|
||||
key!(Shift-Char(' ')) => Clock(
|
||||
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
|
||||
),
|
||||
// Edit phrase
|
||||
key!(Char('e')) => match state.focused() {
|
||||
SequencerFocus::PhrasePlay => Editor(Show(
|
||||
state.player.play_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone())
|
||||
)),
|
||||
SequencerFocus::PhraseNext => Editor(Show(
|
||||
state.player.next_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone())
|
||||
)),
|
||||
SequencerFocus::PhraseList => Editor(Show(
|
||||
Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())
|
||||
)),
|
||||
_ => return None,
|
||||
},
|
||||
_ => match state.focused() {
|
||||
SequencerFocus::Transport(_) => match TransportCommand::input_to_command(state, input)? {
|
||||
TransportCommand::Clock(command) => Clock(command),
|
||||
_ => return None,
|
||||
},
|
||||
SequencerFocus::PhraseEditor => Editor(
|
||||
PhraseCommand::input_to_command(&state.editor, input)?
|
||||
),
|
||||
SequencerFocus::PhraseList => match input.event() {
|
||||
key!(Enter) => Enqueue(Some(
|
||||
state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()
|
||||
)),
|
||||
_ => Phrases(
|
||||
PhrasesCommand::input_to_command(&state.phrases, input)?
|
||||
),
|
||||
}
|
||||
_ => return None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,7 +1,88 @@
|
|||
use crate::*;
|
||||
use KeyCode::{Up, Down, Right, Left, Enter, Esc, Char, Backspace};
|
||||
use FileBrowserCommand::*;
|
||||
use super::model_phrase_list::PhrasesMode::{Import, Export};
|
||||
use super::phrase_list::PhrasesMode::{Import, Export};
|
||||
|
||||
/// Browses for phrase to import/export
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileBrowser {
|
||||
pub cwd: PathBuf,
|
||||
pub dirs: Vec<(OsString, String)>,
|
||||
pub files: Vec<(OsString, String)>,
|
||||
pub filter: String,
|
||||
pub index: usize,
|
||||
pub scroll: usize,
|
||||
pub size: Measure<Tui>
|
||||
}
|
||||
|
||||
render!(|self: FileBrowser|{
|
||||
Stack::down(|add|{
|
||||
let mut i = 0;
|
||||
for (_, name) in self.dirs.iter() {
|
||||
if i >= self.scroll {
|
||||
add(&Tui::bold(i == self.index, name.as_str()))?;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
for (_, name) in self.files.iter() {
|
||||
if i >= self.scroll {
|
||||
add(&Tui::bold(i == self.index, name.as_str()))?;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
add(&format!("{}/{i}", self.index))?;
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
|
||||
impl FileBrowser {
|
||||
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
|
||||
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
|
||||
let mut dirs = vec![];
|
||||
let mut files = vec![];
|
||||
for entry in std::fs::read_dir(&cwd)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name();
|
||||
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
|
||||
let meta = entry.metadata()?;
|
||||
if meta.is_dir() {
|
||||
dirs.push((name, format!("📁 {decoded}")));
|
||||
} else if meta.is_file() {
|
||||
files.push((name, format!("📄 {decoded}")));
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
cwd,
|
||||
dirs,
|
||||
files,
|
||||
filter: "".to_string(),
|
||||
index: 0,
|
||||
scroll: 0,
|
||||
size: Measure::new(),
|
||||
})
|
||||
}
|
||||
pub fn len (&self) -> usize {
|
||||
self.dirs.len() + self.files.len()
|
||||
}
|
||||
pub fn is_dir (&self) -> bool {
|
||||
self.index < self.dirs.len()
|
||||
}
|
||||
pub fn is_file (&self) -> bool {
|
||||
self.index >= self.dirs.len()
|
||||
}
|
||||
pub fn path (&self) -> PathBuf {
|
||||
self.cwd.join(if self.is_dir() {
|
||||
&self.dirs[self.index].0
|
||||
} else if self.is_file() {
|
||||
&self.files[self.index - self.dirs.len()].0
|
||||
} else {
|
||||
unreachable!()
|
||||
})
|
||||
}
|
||||
pub fn chdir (&self) -> Usually<Self> {
|
||||
Self::new(Some(self.path()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Commands supported by [FileBrowser]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
impl HasScenes<ArrangerScene> for ArrangerTui {
|
||||
fn scenes (&self) -> &Vec<ArrangerScene> {
|
||||
&self.scenes
|
||||
}
|
||||
fn scenes_mut (&mut self) -> &mut Vec<ArrangerScene> {
|
||||
&mut self.scenes
|
||||
}
|
||||
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
|
||||
-> Usually<&mut ArrangerScene>
|
||||
{
|
||||
let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string());
|
||||
let scene = ArrangerScene {
|
||||
name: Arc::new(name.into()),
|
||||
clips: vec![None;self.tracks().len()],
|
||||
color: color.unwrap_or_else(||ItemColor::random()),
|
||||
};
|
||||
self.scenes_mut().push(scene);
|
||||
let index = self.scenes().len() - 1;
|
||||
Ok(&mut self.scenes_mut()[index])
|
||||
}
|
||||
fn selected_scene (&self) -> Option<&ArrangerScene> {
|
||||
self.selected.scene().map(|s|self.scenes().get(s)).flatten()
|
||||
}
|
||||
fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
|
||||
self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct ArrangerScene {
|
||||
/// Name of scene
|
||||
pub(crate) name: Arc<RwLock<String>>,
|
||||
/// Clips in scene, one per track
|
||||
pub(crate) clips: Vec<Option<Arc<RwLock<Phrase>>>>,
|
||||
/// Identifying color of scene
|
||||
pub(crate) color: ItemColor,
|
||||
}
|
||||
|
||||
impl ArrangerSceneApi for ArrangerScene {
|
||||
fn name (&self) -> &Arc<RwLock<String>> {
|
||||
&self.name
|
||||
}
|
||||
fn clips (&self) -> &Vec<Option<Arc<RwLock<Phrase>>>> {
|
||||
&self.clips
|
||||
}
|
||||
fn color (&self) -> ItemColor {
|
||||
self.color
|
||||
}
|
||||
}
|
||||
|
||||
impl HasTracks<ArrangerTrack> for ArrangerTui {
|
||||
fn tracks (&self) -> &Vec<ArrangerTrack> {
|
||||
&self.tracks
|
||||
}
|
||||
fn tracks_mut (&mut self) -> &mut Vec<ArrangerTrack> {
|
||||
&mut self.tracks
|
||||
}
|
||||
}
|
||||
|
||||
impl ArrangerTracksApi<ArrangerTrack> for ArrangerTui {
|
||||
fn track_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
|
||||
-> Usually<&mut ArrangerTrack>
|
||||
{
|
||||
let name = name.map_or_else(||self.track_default_name(), |x|x.to_string());
|
||||
let track = ArrangerTrack {
|
||||
width: name.len() + 2,
|
||||
name: Arc::new(name.into()),
|
||||
color: color.unwrap_or_else(||ItemColor::random()),
|
||||
player: PhrasePlayerModel::from(&self.clock),
|
||||
};
|
||||
self.tracks_mut().push(track);
|
||||
let index = self.tracks().len() - 1;
|
||||
Ok(&mut self.tracks_mut()[index])
|
||||
}
|
||||
fn track_del (&mut self, index: usize) {
|
||||
self.tracks_mut().remove(index);
|
||||
for scene in self.scenes_mut().iter_mut() {
|
||||
scene.clips.remove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArrangerTrack {
|
||||
/// Name of track
|
||||
pub(crate) name: Arc<RwLock<String>>,
|
||||
/// Preferred width of track column
|
||||
pub(crate) width: usize,
|
||||
/// Identifying color of track
|
||||
pub(crate) color: ItemColor,
|
||||
/// MIDI player state
|
||||
pub(crate) player: PhrasePlayerModel,
|
||||
}
|
||||
|
||||
impl HasPlayer for ArrangerTrack {
|
||||
fn player (&self) -> &impl MidiPlayerApi {
|
||||
&self.player
|
||||
}
|
||||
fn player_mut (&mut self) -> &mut impl MidiPlayerApi {
|
||||
&mut self.player
|
||||
}
|
||||
}
|
||||
|
||||
impl ArrangerTrackApi for ArrangerTrack {
|
||||
/// Name of track
|
||||
fn name (&self) -> &Arc<RwLock<String>> {
|
||||
&self.name
|
||||
}
|
||||
/// Preferred width of track column
|
||||
fn width (&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
/// Preferred width of track column
|
||||
fn width_mut (&mut self) -> &mut usize {
|
||||
&mut self.width
|
||||
}
|
||||
/// Identifying color of track
|
||||
fn color (&self) -> ItemColor {
|
||||
self.color
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Copy, Debug)]
|
||||
/// Represents the current user selection in the arranger
|
||||
pub enum ArrangerSelection {
|
||||
/// The whole mix is selected
|
||||
Mix,
|
||||
/// A track is selected.
|
||||
Track(usize),
|
||||
/// A scene is selected.
|
||||
Scene(usize),
|
||||
/// A clip (track × scene) is selected.
|
||||
Clip(usize, usize),
|
||||
}
|
||||
|
||||
/// Focus identification methods
|
||||
impl ArrangerSelection {
|
||||
pub fn description <E: Engine> (
|
||||
&self,
|
||||
tracks: &Vec<ArrangerTrack>,
|
||||
scenes: &Vec<ArrangerScene>,
|
||||
) -> String {
|
||||
format!("Selected: {}", match self {
|
||||
Self::Mix => format!("Everything"),
|
||||
Self::Track(t) => match tracks.get(*t) {
|
||||
Some(track) => format!("T{t}: {}", &track.name.read().unwrap()),
|
||||
None => format!("T??"),
|
||||
},
|
||||
Self::Scene(s) => match scenes.get(*s) {
|
||||
Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()),
|
||||
None => format!("S??"),
|
||||
},
|
||||
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
|
||||
(Some(_), Some(scene)) => match scene.clip(*t) {
|
||||
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
|
||||
None => format!("T{t} S{s}: Empty")
|
||||
},
|
||||
_ => format!("T{t} S{s}: Empty"),
|
||||
}
|
||||
})
|
||||
}
|
||||
pub fn is_mix (&self) -> bool {
|
||||
match self { Self::Mix => true, _ => false }
|
||||
}
|
||||
pub fn is_track (&self) -> bool {
|
||||
match self { Self::Track(_) => true, _ => false }
|
||||
}
|
||||
pub fn is_scene (&self) -> bool {
|
||||
match self { Self::Scene(_) => true, _ => false }
|
||||
}
|
||||
pub fn is_clip (&self) -> bool {
|
||||
match self { Self::Clip(_, _) => true, _ => false }
|
||||
}
|
||||
pub fn track (&self) -> Option<usize> {
|
||||
use ArrangerSelection::*;
|
||||
match self {
|
||||
Clip(t, _) => Some(*t),
|
||||
Track(t) => Some(*t),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
pub fn scene (&self) -> Option<usize> {
|
||||
use ArrangerSelection::*;
|
||||
match self {
|
||||
Clip(_, s) => Some(*s),
|
||||
Scene(s) => Some(*s),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Browses for phrase to import/export
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileBrowser {
|
||||
pub cwd: PathBuf,
|
||||
pub dirs: Vec<(OsString, String)>,
|
||||
pub files: Vec<(OsString, String)>,
|
||||
pub filter: String,
|
||||
pub index: usize,
|
||||
pub scroll: usize,
|
||||
pub size: Measure<Tui>
|
||||
}
|
||||
|
||||
impl FileBrowser {
|
||||
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
|
||||
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
|
||||
let mut dirs = vec![];
|
||||
let mut files = vec![];
|
||||
for entry in std::fs::read_dir(&cwd)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name();
|
||||
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
|
||||
let meta = entry.metadata()?;
|
||||
if meta.is_dir() {
|
||||
dirs.push((name, format!("📁 {decoded}")));
|
||||
} else if meta.is_file() {
|
||||
files.push((name, format!("📄 {decoded}")));
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
cwd,
|
||||
dirs,
|
||||
files,
|
||||
filter: "".to_string(),
|
||||
index: 0,
|
||||
scroll: 0,
|
||||
size: Measure::new(),
|
||||
})
|
||||
}
|
||||
pub fn len (&self) -> usize {
|
||||
self.dirs.len() + self.files.len()
|
||||
}
|
||||
pub fn is_dir (&self) -> bool {
|
||||
self.index < self.dirs.len()
|
||||
}
|
||||
pub fn is_file (&self) -> bool {
|
||||
self.index >= self.dirs.len()
|
||||
}
|
||||
pub fn path (&self) -> PathBuf {
|
||||
self.cwd.join(if self.is_dir() {
|
||||
&self.dirs[self.index].0
|
||||
} else if self.is_file() {
|
||||
&self.files[self.index - self.dirs.len()].0
|
||||
} else {
|
||||
unreachable!()
|
||||
})
|
||||
}
|
||||
pub fn chdir (&self) -> Usually<Self> {
|
||||
Self::new(Some(self.path()))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Contains state for viewing and editing a phrase
|
||||
pub struct PhraseEditorModel {
|
||||
/// Phrase being played
|
||||
pub(crate) phrase: Option<Arc<RwLock<Phrase>>>,
|
||||
/// Length of note that will be inserted, in pulses
|
||||
pub(crate) note_len: usize,
|
||||
/// The full piano roll is rendered to this buffer
|
||||
pub(crate) buffer: BigBuffer,
|
||||
/// Notes currently held at input
|
||||
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
|
||||
/// Notes currently held at output
|
||||
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
|
||||
/// Current position of global playhead
|
||||
pub(crate) now: Arc<Pulse>,
|
||||
/// Width and height of notes area at last render
|
||||
pub(crate) size: Measure<Tui>,
|
||||
|
||||
pub(crate) note_lo: AtomicUsize,
|
||||
pub(crate) note_point: AtomicUsize,
|
||||
|
||||
pub(crate) time_start: AtomicUsize,
|
||||
pub(crate) time_point: AtomicUsize,
|
||||
pub(crate) time_scale: AtomicUsize,
|
||||
|
||||
pub(crate) edit_mode: PhraseEditMode,
|
||||
pub(crate) view_mode: PhraseViewMode,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PhraseEditorModel {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("PhraseEditorModel")
|
||||
.field("note_axis", &format!("{} {}",
|
||||
self.note_lo.load(Ordering::Relaxed),
|
||||
self.note_point.load(Ordering::Relaxed),
|
||||
))
|
||||
.field("time_axis", &format!("{} {} {}",
|
||||
self.time_start.load(Ordering::Relaxed),
|
||||
self.time_point.load(Ordering::Relaxed),
|
||||
self.time_scale.load(Ordering::Relaxed),
|
||||
))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PhraseEditorModel {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
phrase: None,
|
||||
note_len: 24,
|
||||
buffer: Default::default(),
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
now: Pulse::default().into(),
|
||||
size: Measure::new(),
|
||||
edit_mode: PhraseEditMode::Scroll,
|
||||
note_lo: 0.into(),
|
||||
note_point: 0.into(),
|
||||
time_start: 0.into(),
|
||||
time_point: 0.into(),
|
||||
time_scale: 24.into(),
|
||||
view_mode: PhraseViewMode::PianoHorizontal {
|
||||
time_zoom: 24,
|
||||
note_zoom: PhraseViewNoteZoom::N(1)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PhraseEditorModel {
|
||||
/// Put note at current position
|
||||
pub fn put_note (&mut self) {
|
||||
if let Some(phrase) = &self.phrase {
|
||||
let time = self.time_point.load(Ordering::Relaxed);
|
||||
let note = self.note_point.load(Ordering::Relaxed);
|
||||
let mut phrase = phrase.write().unwrap();
|
||||
let key: u7 = u7::from((127 - note) as u8);
|
||||
let vel: u7 = 100.into();
|
||||
let start = time;
|
||||
let end = (start + self.note_len) % phrase.length;
|
||||
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
|
||||
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
|
||||
self.buffer = self.view_mode.draw(&phrase);
|
||||
}
|
||||
}
|
||||
/// Move time cursor forward by current note length
|
||||
pub fn time_cursor_advance (&self) {
|
||||
let point = self.time_point.load(Ordering::Relaxed);
|
||||
let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
|
||||
let forward = |time|(time + self.note_len) % length;
|
||||
self.time_point.store(forward(point), Ordering::Relaxed);
|
||||
}
|
||||
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
|
||||
pub fn show_phrase (&mut self, phrase: Option<Arc<RwLock<Phrase>>>) {
|
||||
if phrase.is_some() {
|
||||
self.buffer = self.view_mode.draw(&*phrase.as_ref().unwrap().read().unwrap());
|
||||
self.phrase = phrase;
|
||||
} else {
|
||||
self.buffer = Default::default();
|
||||
self.phrase = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasEditor {
|
||||
fn editor (&self) -> &PhraseEditorModel;
|
||||
fn editor_focused (&self) -> bool;
|
||||
fn editor_entered (&self) -> bool;
|
||||
}
|
||||
|
||||
impl HasEditor for SequencerTui {
|
||||
fn editor (&self) -> &PhraseEditorModel {
|
||||
&self.editor
|
||||
}
|
||||
fn editor_focused (&self) -> bool {
|
||||
self.focused() == SequencerFocus::PhraseEditor
|
||||
}
|
||||
fn editor_entered (&self) -> bool {
|
||||
self.entered() && self.editor_focused()
|
||||
}
|
||||
}
|
||||
|
||||
impl HasEditor for ArrangerTui {
|
||||
fn editor (&self) -> &PhraseEditorModel {
|
||||
&self.editor
|
||||
}
|
||||
fn editor_focused (&self) -> bool {
|
||||
self.focused() == ArrangerFocus::PhraseEditor
|
||||
}
|
||||
fn editor_entered (&self) -> bool {
|
||||
self.entered() && self.editor_focused()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// 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())
|
||||
}
|
||||
}
|
||||
|
||||
/// Focused field of `PhraseLength`
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum PhraseLengthFocus {
|
||||
/// Editing the number of bars
|
||||
Bar,
|
||||
/// Editing the number of beats
|
||||
Beat,
|
||||
/// Editing the number of ticks
|
||||
Tick,
|
||||
}
|
||||
|
||||
impl PhraseLengthFocus {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PhraseListModel {
|
||||
/// Collection of phrases
|
||||
pub(crate) phrases: Vec<Arc<RwLock<Phrase>>>,
|
||||
/// Selected phrase
|
||||
pub(crate) phrase: AtomicUsize,
|
||||
/// Scroll offset
|
||||
pub(crate) scroll: usize,
|
||||
/// Mode switch
|
||||
pub(crate) mode: Option<PhrasesMode>,
|
||||
}
|
||||
|
||||
impl Default for PhraseListModel {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
phrases: vec![RwLock::new(Phrase::default()).into()],
|
||||
phrase: 0.into(),
|
||||
scroll: 0,
|
||||
mode: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PhraseListModel {
|
||||
pub(crate) fn phrase_index (&self) -> usize {
|
||||
self.phrase.load(Ordering::Relaxed)
|
||||
}
|
||||
pub(crate) fn set_phrase_index (&self, value: usize) {
|
||||
self.phrase.store(value, Ordering::Relaxed);
|
||||
}
|
||||
pub(crate) fn phrases_mode (&self) -> &Option<PhrasesMode> {
|
||||
&self.mode
|
||||
}
|
||||
pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option<PhrasesMode> {
|
||||
&mut self.mode
|
||||
}
|
||||
}
|
||||
|
||||
/// 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),
|
||||
}
|
||||
|
||||
pub trait HasPhraseList: HasPhrases {
|
||||
fn phrases_focused (&self) -> bool;
|
||||
fn phrases_entered (&self) -> bool;
|
||||
fn phrases_mode (&self) -> &Option<PhrasesMode>;
|
||||
fn phrase_index (&self) -> usize;
|
||||
}
|
||||
|
||||
impl HasPhraseList for SequencerTui {
|
||||
fn phrases_focused (&self) -> bool {
|
||||
self.focused() == SequencerFocus::PhraseList
|
||||
}
|
||||
fn phrases_entered (&self) -> bool {
|
||||
self.entered() && self.phrases_focused()
|
||||
}
|
||||
fn phrases_mode (&self) -> &Option<PhrasesMode> {
|
||||
&self.phrases.mode
|
||||
}
|
||||
fn phrase_index (&self) -> usize {
|
||||
self.phrases.phrase.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasPhraseList for ArrangerTui {
|
||||
fn phrases_focused (&self) -> bool {
|
||||
self.focused() == ArrangerFocus::Phrases
|
||||
}
|
||||
fn phrases_entered (&self) -> bool {
|
||||
self.entered() && self.phrases_focused()
|
||||
}
|
||||
fn phrases_mode (&self) -> &Option<PhrasesMode> {
|
||||
&self.phrases.mode
|
||||
}
|
||||
fn phrase_index (&self) -> usize {
|
||||
self.phrases.phrase.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,138 @@
|
|||
use crate::*;
|
||||
|
||||
/// Contains state for viewing and editing a phrase
|
||||
pub struct PhraseEditorModel {
|
||||
/// Phrase being played
|
||||
pub(crate) phrase: Option<Arc<RwLock<Phrase>>>,
|
||||
/// Length of note that will be inserted, in pulses
|
||||
pub(crate) note_len: usize,
|
||||
/// The full piano roll is rendered to this buffer
|
||||
pub(crate) buffer: BigBuffer,
|
||||
/// Notes currently held at input
|
||||
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
|
||||
/// Notes currently held at output
|
||||
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
|
||||
/// Current position of global playhead
|
||||
pub(crate) now: Arc<Pulse>,
|
||||
/// Width and height of notes area at last render
|
||||
pub(crate) size: Measure<Tui>,
|
||||
|
||||
pub(crate) note_lo: AtomicUsize,
|
||||
pub(crate) note_point: AtomicUsize,
|
||||
|
||||
pub(crate) time_start: AtomicUsize,
|
||||
pub(crate) time_point: AtomicUsize,
|
||||
pub(crate) time_scale: AtomicUsize,
|
||||
|
||||
pub(crate) edit_mode: PhraseEditMode,
|
||||
pub(crate) view_mode: PhraseViewMode,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PhraseEditorModel {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("PhraseEditorModel")
|
||||
.field("note_axis", &format!("{} {}",
|
||||
self.note_lo.load(Ordering::Relaxed),
|
||||
self.note_point.load(Ordering::Relaxed),
|
||||
))
|
||||
.field("time_axis", &format!("{} {} {}",
|
||||
self.time_start.load(Ordering::Relaxed),
|
||||
self.time_point.load(Ordering::Relaxed),
|
||||
self.time_scale.load(Ordering::Relaxed),
|
||||
))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PhraseEditorModel {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
phrase: None,
|
||||
note_len: 24,
|
||||
buffer: Default::default(),
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
now: Pulse::default().into(),
|
||||
size: Measure::new(),
|
||||
edit_mode: PhraseEditMode::Scroll,
|
||||
note_lo: 0.into(),
|
||||
note_point: 0.into(),
|
||||
time_start: 0.into(),
|
||||
time_point: 0.into(),
|
||||
time_scale: 24.into(),
|
||||
view_mode: PhraseViewMode::PianoHorizontal {
|
||||
time_zoom: 24,
|
||||
note_zoom: PhraseViewNoteZoom::N(1)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PhraseEditorModel {
|
||||
/// Put note at current position
|
||||
pub fn put_note (&mut self) {
|
||||
if let Some(phrase) = &self.phrase {
|
||||
let time = self.time_point.load(Ordering::Relaxed);
|
||||
let note = self.note_point.load(Ordering::Relaxed);
|
||||
let mut phrase = phrase.write().unwrap();
|
||||
let key: u7 = u7::from((127 - note) as u8);
|
||||
let vel: u7 = 100.into();
|
||||
let start = time;
|
||||
let end = (start + self.note_len) % phrase.length;
|
||||
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
|
||||
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
|
||||
self.buffer = self.view_mode.draw(&phrase);
|
||||
}
|
||||
}
|
||||
/// Move time cursor forward by current note length
|
||||
pub fn time_cursor_advance (&self) {
|
||||
let point = self.time_point.load(Ordering::Relaxed);
|
||||
let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
|
||||
let forward = |time|(time + self.note_len) % length;
|
||||
self.time_point.store(forward(point), Ordering::Relaxed);
|
||||
}
|
||||
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
|
||||
pub fn show_phrase (&mut self, phrase: Option<Arc<RwLock<Phrase>>>) {
|
||||
if phrase.is_some() {
|
||||
self.buffer = self.view_mode.draw(&*phrase.as_ref().unwrap().read().unwrap());
|
||||
self.phrase = phrase;
|
||||
} else {
|
||||
self.buffer = Default::default();
|
||||
self.phrase = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasEditor {
|
||||
fn editor (&self) -> &PhraseEditorModel;
|
||||
fn editor_focused (&self) -> bool;
|
||||
fn editor_entered (&self) -> bool;
|
||||
}
|
||||
|
||||
impl HasEditor for SequencerTui {
|
||||
fn editor (&self) -> &PhraseEditorModel {
|
||||
&self.editor
|
||||
}
|
||||
fn editor_focused (&self) -> bool {
|
||||
self.focused() == SequencerFocus::PhraseEditor
|
||||
}
|
||||
fn editor_entered (&self) -> bool {
|
||||
self.entered() && self.editor_focused()
|
||||
}
|
||||
}
|
||||
|
||||
impl HasEditor for ArrangerTui {
|
||||
fn editor (&self) -> &PhraseEditorModel {
|
||||
&self.editor
|
||||
}
|
||||
fn editor_focused (&self) -> bool {
|
||||
self.focused() == ArrangerFocus::PhraseEditor
|
||||
}
|
||||
fn editor_entered (&self) -> bool {
|
||||
self.entered() && self.editor_focused()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PhraseView<'a> {
|
||||
focused: bool,
|
||||
entered: bool,
|
||||
|
|
@ -473,3 +606,131 @@ impl PhraseViewMode {
|
|||
//////"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8",
|
||||
////];
|
||||
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PhraseCommand {
|
||||
// TODO: 1-9 seek markers that by default start every 8th of the phrase
|
||||
AppendNote,
|
||||
PutNote,
|
||||
SetNoteCursor(usize),
|
||||
SetNoteLength(usize),
|
||||
SetNoteScroll(usize),
|
||||
SetTimeCursor(usize),
|
||||
SetTimeScroll(usize),
|
||||
SetTimeZoom(usize),
|
||||
Show(Option<Arc<RwLock<Phrase>>>),
|
||||
SetEditMode(PhraseEditMode),
|
||||
ToggleDirection,
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhraseEditorModel> for PhraseCommand {
|
||||
fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option<Self> {
|
||||
use PhraseCommand::*;
|
||||
use KeyCode::{Char, Esc, Up, Down, PageUp, PageDown, Left, Right};
|
||||
let note_lo = state.note_lo.load(Ordering::Relaxed);
|
||||
let note_point = state.note_point.load(Ordering::Relaxed);
|
||||
let time_start = state.time_start.load(Ordering::Relaxed);
|
||||
let time_point = state.time_point.load(Ordering::Relaxed);
|
||||
let time_zoom = state.view_mode.time_zoom();
|
||||
let length = state.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
|
||||
Some(match from.event() {
|
||||
key!(Char('`')) => ToggleDirection,
|
||||
key!(Esc) => SetEditMode(PhraseEditMode::Scroll),
|
||||
key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)),
|
||||
key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)),
|
||||
key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)),
|
||||
key!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)),
|
||||
key!(Char('a')) => AppendNote,
|
||||
key!(Char('s')) => PutNote,
|
||||
key!(Char('[')) => SetNoteLength(prev_note_length(state.note_len)),
|
||||
key!(Char(']')) => SetNoteLength(next_note_length(state.note_len)),
|
||||
key!(Char('n')) => { todo!("toggle keys vs notes") },
|
||||
_ => match state.edit_mode {
|
||||
PhraseEditMode::Scroll => match from.event() {
|
||||
key!(Char('e')) => SetEditMode(PhraseEditMode::Note),
|
||||
key!(Up) => SetNoteScroll(note_lo + 1),
|
||||
key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)),
|
||||
key!(PageUp) => SetNoteScroll(note_lo + 3),
|
||||
key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)),
|
||||
key!(Left) => SetTimeScroll(time_start.saturating_sub(1)),
|
||||
key!(Right) => SetTimeScroll(time_start + 1),
|
||||
_ => return None
|
||||
},
|
||||
PhraseEditMode::Note => match from.event() {
|
||||
key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll),
|
||||
key!(Up) => SetNoteCursor(note_point + 1),
|
||||
key!(Down) => SetNoteCursor(note_point.saturating_sub(1)),
|
||||
key!(PageUp) => SetNoteCursor(note_point + 3),
|
||||
key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)),
|
||||
key!(Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)),
|
||||
key!(Right) => SetTimeCursor((time_point + time_zoom) % length),
|
||||
_ => return None
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<PhraseEditorModel> for PhraseCommand {
|
||||
fn execute (self, state: &mut PhraseEditorModel) -> Perhaps<Self> {
|
||||
use PhraseCommand::*;
|
||||
Ok(match self {
|
||||
Show(phrase) => {
|
||||
state.show_phrase(phrase);
|
||||
None
|
||||
},
|
||||
ToggleDirection => {
|
||||
todo!()
|
||||
},
|
||||
SetEditMode(mode) => {
|
||||
state.edit_mode = mode;
|
||||
None
|
||||
}
|
||||
AppendNote => {
|
||||
state.put_note();
|
||||
state.time_cursor_advance();
|
||||
None
|
||||
},
|
||||
PutNote => {
|
||||
state.put_note();
|
||||
None
|
||||
},
|
||||
SetTimeCursor(time) => {
|
||||
state.time_point.store(time, Ordering::Relaxed);
|
||||
None
|
||||
},
|
||||
SetTimeScroll(time) => {
|
||||
state.time_start.store(time, Ordering::Relaxed);
|
||||
None
|
||||
},
|
||||
SetTimeZoom(zoom) => {
|
||||
state.view_mode.set_time_zoom(zoom);
|
||||
state.show_phrase(state.phrase.clone());
|
||||
None
|
||||
},
|
||||
SetNoteScroll(note) => {
|
||||
state.note_lo.store(note, Ordering::Relaxed);
|
||||
None
|
||||
},
|
||||
SetNoteLength(time) => {
|
||||
state.note_len = time;
|
||||
None
|
||||
},
|
||||
SetNoteCursor(note) => {
|
||||
let note = 127.min(note);
|
||||
let start = state.note_lo.load(Ordering::Relaxed);
|
||||
state.note_point.store(note, Ordering::Relaxed);
|
||||
if note < start {
|
||||
state.note_lo.store(note, Ordering::Relaxed);
|
||||
}
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum PhraseEditMode {
|
||||
Note,
|
||||
Scroll,
|
||||
}
|
||||
129
crates/tek/src/tui/phrase_length.rs
Normal file
129
crates/tek/src/tui/phrase_length.rs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
use crate::*;
|
||||
use super::phrase_list::{PhraseListModel, PhrasesMode};
|
||||
use PhraseLengthFocus::*;
|
||||
use PhraseLengthCommand::*;
|
||||
|
||||
/// 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())
|
||||
}
|
||||
}
|
||||
|
||||
/// Focused field of `PhraseLength`
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum PhraseLengthFocus {
|
||||
/// Editing the number of bars
|
||||
Bar,
|
||||
/// Editing the number of beats
|
||||
Beat,
|
||||
/// Editing the number of ticks
|
||||
Tick,
|
||||
}
|
||||
|
||||
impl PhraseLengthFocus {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render!(|self: PhraseLength|{
|
||||
let bars = ||self.bars_string();
|
||||
let beats = ||self.beats_string();
|
||||
let ticks = ||self.ticks_string();
|
||||
row!(move|add|match self.focus {
|
||||
None =>
|
||||
add(&row!([" ", bars(), "B", beats(), "b", ticks(), "T"])),
|
||||
Some(PhraseLengthFocus::Bar) =>
|
||||
add(&row!(["[", bars(), "]", beats(), "b", ticks(), "T"])),
|
||||
Some(PhraseLengthFocus::Beat) =>
|
||||
add(&row!([" ", bars(), "[", beats(), "]", ticks(), "T"])),
|
||||
Some(PhraseLengthFocus::Tick) =>
|
||||
add(&row!([" ", bars(), "B", beats(), "[", ticks(), "]"])),
|
||||
})
|
||||
});
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum PhraseLengthCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Set(usize),
|
||||
Next,
|
||||
Prev,
|
||||
Inc,
|
||||
Dec,
|
||||
}
|
||||
|
||||
impl Command<PhraseListModel> for PhraseLengthCommand {
|
||||
fn execute (self, state: &mut PhraseListModel) -> Perhaps<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() },
|
||||
Inc => match focus {
|
||||
Bar => { *length += 4 * PPQ },
|
||||
Beat => { *length += PPQ },
|
||||
Tick => { *length += 1 },
|
||||
},
|
||||
Dec => match focus {
|
||||
Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||
Beat => { *length = length.saturating_sub(PPQ) },
|
||||
Tick => { *length = length.saturating_sub(1) },
|
||||
},
|
||||
Set(length) => {
|
||||
let mut phrase = state.phrases()[phrase].write().unwrap();
|
||||
let old_length = phrase.length;
|
||||
phrase.length = length;
|
||||
std::mem::drop(phrase);
|
||||
*state.phrases_mode_mut() = None;
|
||||
return Ok(Some(Self::Set(old_length)))
|
||||
},
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
303
crates/tek/src/tui/phrase_list.rs
Normal file
303
crates/tek/src/tui/phrase_list.rs
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
use super::*;
|
||||
use crate::{
|
||||
*,
|
||||
api::PhrasePoolCommand as Pool,
|
||||
tui::{
|
||||
phrase_rename::PhraseRenameCommand as Rename,
|
||||
phrase_length::PhraseLengthCommand as Length,
|
||||
file_browser::FileBrowserCommand as Browse,
|
||||
}
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PhraseListModel {
|
||||
/// Collection of phrases
|
||||
pub(crate) phrases: Vec<Arc<RwLock<Phrase>>>,
|
||||
/// Selected phrase
|
||||
pub(crate) phrase: AtomicUsize,
|
||||
/// Scroll offset
|
||||
pub(crate) scroll: usize,
|
||||
/// Mode switch
|
||||
pub(crate) mode: Option<PhrasesMode>,
|
||||
}
|
||||
|
||||
impl Default for PhraseListModel {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
phrases: vec![RwLock::new(Phrase::default()).into()],
|
||||
phrase: 0.into(),
|
||||
scroll: 0,
|
||||
mode: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PhraseListModel {
|
||||
pub(crate) fn phrase_index (&self) -> usize {
|
||||
self.phrase.load(Ordering::Relaxed)
|
||||
}
|
||||
pub(crate) fn set_phrase_index (&self, value: usize) {
|
||||
self.phrase.store(value, Ordering::Relaxed);
|
||||
}
|
||||
pub(crate) fn phrases_mode (&self) -> &Option<PhrasesMode> {
|
||||
&self.mode
|
||||
}
|
||||
pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option<PhrasesMode> {
|
||||
&mut self.mode
|
||||
}
|
||||
}
|
||||
|
||||
/// 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),
|
||||
}
|
||||
|
||||
pub trait HasPhraseList: HasPhrases {
|
||||
fn phrases_focused (&self) -> bool;
|
||||
fn phrases_entered (&self) -> bool;
|
||||
fn phrases_mode (&self) -> &Option<PhrasesMode>;
|
||||
fn phrase_index (&self) -> usize;
|
||||
}
|
||||
|
||||
impl HasPhraseList for SequencerTui {
|
||||
fn phrases_focused (&self) -> bool {
|
||||
self.focused() == SequencerFocus::PhraseList
|
||||
}
|
||||
fn phrases_entered (&self) -> bool {
|
||||
self.entered() && self.phrases_focused()
|
||||
}
|
||||
fn phrases_mode (&self) -> &Option<PhrasesMode> {
|
||||
&self.phrases.mode
|
||||
}
|
||||
fn phrase_index (&self) -> usize {
|
||||
self.phrases.phrase.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasPhraseList for ArrangerTui {
|
||||
fn phrases_focused (&self) -> bool {
|
||||
self.focused() == ArrangerFocus::Phrases
|
||||
}
|
||||
fn phrases_entered (&self) -> bool {
|
||||
self.entered() && self.phrases_focused()
|
||||
}
|
||||
fn phrases_mode (&self) -> &Option<PhrasesMode> {
|
||||
&self.phrases.mode
|
||||
}
|
||||
fn phrase_index (&self) -> usize {
|
||||
self.phrases.phrase.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PhraseListView<'a> {
|
||||
pub(crate) title: &'static str,
|
||||
pub(crate) focused: bool,
|
||||
pub(crate) entered: bool,
|
||||
pub(crate) phrases: &'a Vec<Arc<RwLock<Phrase>>>,
|
||||
pub(crate) index: usize,
|
||||
pub(crate) mode: &'a Option<PhrasesMode>
|
||||
}
|
||||
|
||||
impl<'a, T: HasPhraseList> From<&'a T> for PhraseListView<'a> {
|
||||
fn from (state: &'a T) -> Self {
|
||||
Self {
|
||||
title: "Phrases",
|
||||
focused: state.phrases_focused(),
|
||||
entered: state.phrases_entered(),
|
||||
phrases: state.phrases(),
|
||||
index: state.phrase_index(),
|
||||
mode: state.phrases_mode(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Display phrases always in order of appearance
|
||||
render!(|self: PhraseListView<'a>|{
|
||||
let Self { title, focused, entered, phrases, index, mode } = self;
|
||||
let border_color = if *focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)};
|
||||
let title_color = if *focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)};
|
||||
let upper_left = format!("[{}] {title}", if *entered {"■"} else {" "});
|
||||
let upper_right = format!("({})", phrases.len());
|
||||
lay!([
|
||||
Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color))
|
||||
.wrap(Tui::bg(Color::Rgb(28, 35, 25), Tui::fill_xy(col!(move|add|match mode {
|
||||
Some(PhrasesMode::Import(_, ref browser)) => {
|
||||
add(browser)
|
||||
},
|
||||
Some(PhrasesMode::Export(_, ref browser)) => {
|
||||
add(browser)
|
||||
},
|
||||
_ => {
|
||||
for (i, phrase) in phrases.iter().enumerate() {
|
||||
add(&lay!(|add|{
|
||||
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
|
||||
let mut length = PhraseLength::new(length, None);
|
||||
if let Some(PhrasesMode::Length(phrase, new_length, focus)) = mode {
|
||||
if *focused && i == *phrase {
|
||||
length.pulses = *new_length;
|
||||
length.focus = Some(*focus);
|
||||
}
|
||||
}
|
||||
let length = Tui::fill_x(Tui::at_e(length));
|
||||
let row1 = Tui::fill_x(lay!([Tui::fill_x(Tui::at_w(format!(" {i}"))), length]));
|
||||
let mut row2 = format!(" {name}");
|
||||
if let Some(PhrasesMode::Rename(phrase, _)) = mode {
|
||||
if *focused && i == *phrase {
|
||||
row2 = format!("{row2}▄");
|
||||
}
|
||||
};
|
||||
let row2 = Tui::bold(true, row2);
|
||||
add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([row1, row2]))))?;
|
||||
if *entered && i == *index {
|
||||
add(&CORNERS)?;
|
||||
}
|
||||
Ok(())
|
||||
}))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
})))),
|
||||
Tui::fill_xy(Tui::at_nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))),
|
||||
Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))),
|
||||
])
|
||||
});
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum PhrasesCommand {
|
||||
Select(usize),
|
||||
Phrase(Pool),
|
||||
Rename(Rename),
|
||||
Length(Length),
|
||||
Import(Browse),
|
||||
Export(Browse),
|
||||
}
|
||||
|
||||
impl Command<PhraseListModel> for PhrasesCommand {
|
||||
fn execute (self, state: &mut PhraseListModel) -> Perhaps<Self> {
|
||||
use PhrasesCommand::*;
|
||||
Ok(match self {
|
||||
Phrase(command) => command.execute(state)?.map(Phrase),
|
||||
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)?)
|
||||
);
|
||||
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)?)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Export)
|
||||
},
|
||||
Select(phrase) => {
|
||||
state.set_phrase_index(phrase);
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HasPhrases for PhraseListModel {
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
||||
&self.phrases
|
||||
}
|
||||
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
|
||||
&mut self.phrases
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhraseListModel> for PhrasesCommand {
|
||||
fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option<Self> {
|
||||
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 (state: &PhraseListModel, input: &TuiInput) -> Option<PhrasesCommand> {
|
||||
use KeyCode::{Up, Down, Delete, Char};
|
||||
use PhrasesCommand as Cmd;
|
||||
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!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())),
|
||||
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(',')) => 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
|
||||
})
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
render!(|self: FileBrowser|{
|
||||
Stack::down(|add|{
|
||||
let mut i = 0;
|
||||
for (_, name) in self.dirs.iter() {
|
||||
if i >= self.scroll {
|
||||
add(&Tui::bold(i == self.index, name.as_str()))?;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
for (_, name) in self.files.iter() {
|
||||
if i >= self.scroll {
|
||||
add(&Tui::bold(i == self.index, name.as_str()))?;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
add(&format!("{}/{i}", self.index))?;
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
render!(|self: PhraseLength|{
|
||||
let bars = ||self.bars_string();
|
||||
let beats = ||self.beats_string();
|
||||
let ticks = ||self.ticks_string();
|
||||
row!(move|add|match self.focus {
|
||||
None =>
|
||||
add(&row!([" ", bars(), "B", beats(), "b", ticks(), "T"])),
|
||||
Some(PhraseLengthFocus::Bar) =>
|
||||
add(&row!(["[", bars(), "]", beats(), "b", ticks(), "T"])),
|
||||
Some(PhraseLengthFocus::Beat) =>
|
||||
add(&row!([" ", bars(), "[", beats(), "]", ticks(), "T"])),
|
||||
Some(PhraseLengthFocus::Tick) =>
|
||||
add(&row!([" ", bars(), "B", beats(), "[", ticks(), "]"])),
|
||||
})
|
||||
});
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct PhraseListView<'a> {
|
||||
pub(crate) title: &'static str,
|
||||
pub(crate) focused: bool,
|
||||
pub(crate) entered: bool,
|
||||
pub(crate) phrases: &'a Vec<Arc<RwLock<Phrase>>>,
|
||||
pub(crate) index: usize,
|
||||
pub(crate) mode: &'a Option<PhrasesMode>
|
||||
}
|
||||
|
||||
impl<'a, T: HasPhraseList> From<&'a T> for PhraseListView<'a> {
|
||||
fn from (state: &'a T) -> Self {
|
||||
Self {
|
||||
title: "Phrases",
|
||||
focused: state.phrases_focused(),
|
||||
entered: state.phrases_entered(),
|
||||
phrases: state.phrases(),
|
||||
index: state.phrase_index(),
|
||||
mode: state.phrases_mode(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Display phrases always in order of appearance
|
||||
render!(|self: PhraseListView<'a>|{
|
||||
let Self { title, focused, entered, phrases, index, mode } = self;
|
||||
let border_color = if *focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)};
|
||||
let title_color = if *focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)};
|
||||
let upper_left = format!("[{}] {title}", if *entered {"■"} else {" "});
|
||||
let upper_right = format!("({})", phrases.len());
|
||||
lay!([
|
||||
Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color))
|
||||
.wrap(Tui::bg(Color::Rgb(28, 35, 25), Tui::fill_xy(col!(move|add|match mode {
|
||||
Some(PhrasesMode::Import(_, ref browser)) => {
|
||||
add(browser)
|
||||
},
|
||||
Some(PhrasesMode::Export(_, ref browser)) => {
|
||||
add(browser)
|
||||
},
|
||||
_ => {
|
||||
for (i, phrase) in phrases.iter().enumerate() {
|
||||
add(&lay!(|add|{
|
||||
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
|
||||
let mut length = PhraseLength::new(length, None);
|
||||
if let Some(PhrasesMode::Length(phrase, new_length, focus)) = mode {
|
||||
if *focused && i == *phrase {
|
||||
length.pulses = *new_length;
|
||||
length.focus = Some(*focus);
|
||||
}
|
||||
}
|
||||
let length = Tui::fill_x(Tui::at_e(length));
|
||||
let row1 = Tui::fill_x(lay!([Tui::fill_x(Tui::at_w(format!(" {i}"))), length]));
|
||||
let mut row2 = format!(" {name}");
|
||||
if let Some(PhrasesMode::Rename(phrase, _)) = mode {
|
||||
if *focused && i == *phrase {
|
||||
row2 = format!("{row2}▄");
|
||||
}
|
||||
};
|
||||
let row2 = Tui::bold(true, row2);
|
||||
add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([row1, row2]))))?;
|
||||
if *entered && i == *index {
|
||||
add(&CORNERS)?;
|
||||
}
|
||||
Ok(())
|
||||
}))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
})))),
|
||||
Tui::fill_xy(Tui::at_nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))),
|
||||
Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))),
|
||||
])
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue