separate control layer

This commit is contained in:
🪞👃🪞 2024-11-25 18:16:14 +01:00
parent 416acd9f7b
commit 9319315595
29 changed files with 1647 additions and 1641 deletions

View file

@ -9,23 +9,27 @@ pub(crate) use std::path::PathBuf;
pub(crate) use std::ffi::OsString;
pub(crate) use std::fs::read_dir;
use std::fmt::Debug;
submod! {
tui_apps
tui_command
tui_control
tui_debug
tui_focus
tui_handle
tui_init
tui_input
tui_impls
tui_jack
tui_menu
tui_select
tui_status
tui_theme
tui_app_arranger
tui_app_sequencer
tui_app_transport
tui_jack_transport
tui_jack_sequencer
tui_jack_arranger
tui_control_arranger
tui_control_file_browser
tui_control_phrase_editor
tui_control_phrase_length
tui_control_phrase_list
tui_control_phrase_rename
tui_control_sequencer
tui_control_transport
tui_model_arranger
tui_model_clock
@ -43,3 +47,196 @@ submod! {
tui_view_sequencer
tui_view_transport
}
pub fn to_focus_command (input: &TuiInput) -> Option<FocusCommand> {
use KeyCode::{Tab, BackTab, Up, Down, Left, Right, Enter, Esc};
Some(match input.event() {
key!(Tab) => FocusCommand::Next,
key!(Shift-Tab) => FocusCommand::Prev,
key!(BackTab) => FocusCommand::Prev,
key!(Shift-BackTab) => FocusCommand::Prev,
key!(Up) => FocusCommand::Up,
key!(Down) => FocusCommand::Down,
key!(Left) => FocusCommand::Left,
key!(Right) => FocusCommand::Right,
key!(Enter) => FocusCommand::Enter,
key!(Esc) => FocusCommand::Exit,
_ => return None
})
}
pub struct TuiTheme;
impl TuiTheme {
pub fn border_bg () -> Color {
Color::Rgb(40, 50, 30)
}
pub fn border_fg (focused: bool) -> Color {
if focused { Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) }
}
pub fn title_fg (focused: bool) -> Color {
if focused { Color::Rgb(150, 160, 90) } else { Color::Rgb(120, 130, 100) }
}
pub fn separator_fg (_: bool) -> Color {
Color::Rgb(0, 0, 0)
}
pub const fn hotkey_fg () -> Color {
Color::Rgb(255, 255, 0)
}
pub fn mode_bg () -> Color {
Color::Rgb(150, 160, 90)
}
pub fn mode_fg () -> Color {
Color::Rgb(255, 255, 255)
}
pub fn status_bar_bg () -> Color {
Color::Rgb(28, 35, 25)
}
}
macro_rules! impl_clock_api {
($Struct:ident $(:: $field:ident)*) => {
impl ClockApi for $Struct {
fn quant (&self) -> &Arc<Quantize> {
&self$(.$field)*.quant
}
fn sync (&self) -> &Arc<LaunchSync> {
&self$(.$field)*.sync
}
fn current (&self) -> &Arc<Instant> {
&self$(.$field)*.current
}
fn transport_handle (&self) -> &Arc<Transport> {
&self$(.$field)*.transport
}
fn transport_state (&self) -> &Arc<RwLock<Option<TransportState>>> {
&self$(.$field)*.playing
}
fn transport_offset (&self) -> &Arc<RwLock<Option<(usize, usize)>>> {
&self$(.$field)*.started
}
}
}
}
macro_rules! impl_midi_player {
($Struct:ident $(:: $field:ident)*) => {
impl HasPhrase for $Struct {
fn reset (&self) -> bool {
self$(.$field)*.reset
}
fn reset_mut (&mut self) -> &mut bool {
&mut self$(.$field)*.reset
}
fn play_phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
&self$(.$field)*.play_phrase
}
fn play_phrase_mut (&mut self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
&mut self$(.$field)*.play_phrase
}
fn next_phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
&self$(.$field)*.next_phrase
}
fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
&mut self$(.$field)*.next_phrase
}
}
impl MidiInputApi for $Struct {
fn midi_ins (&self) -> &Vec<Port<jack::MidiIn>> {
&self$(.$field)*.midi_ins
}
fn midi_ins_mut (&mut self) -> &mut Vec<Port<jack::MidiIn>> {
&mut self$(.$field)*.midi_ins
}
fn recording (&self) -> bool {
self$(.$field)*.recording
}
fn recording_mut (&mut self) -> &mut bool {
&mut self$(.$field)*.recording
}
fn monitoring (&self) -> bool {
self$(.$field)*.monitoring
}
fn monitoring_mut (&mut self) -> &mut bool {
&mut self$(.$field)*.monitoring
}
fn overdub (&self) -> bool {
self$(.$field)*.overdub
}
fn overdub_mut (&mut self) -> &mut bool {
&mut self$(.$field)*.overdub
}
fn notes_in (&self) -> &Arc<RwLock<[bool; 128]>> {
&self$(.$field)*.notes_in
}
}
impl MidiOutputApi for $Struct {
fn midi_outs (&self) -> &Vec<Port<jack::MidiOut>> {
&self$(.$field)*.midi_outs
}
fn midi_outs_mut (&mut self) -> &mut Vec<Port<jack::MidiOut>> {
&mut self$(.$field)*.midi_outs
}
fn midi_note (&mut self) -> &mut Vec<u8> {
&mut self$(.$field)*.note_buf
}
fn notes_out (&self) -> &Arc<RwLock<[bool; 128]>> {
&self$(.$field)*.notes_in
}
}
impl MidiPlayerApi for $Struct {}
}
}
impl_clock_api!(TransportTui::clock);
impl_clock_api!(SequencerTui::clock);
impl_clock_api!(ArrangerTui::clock);
impl_clock_api!(PhrasePlayerModel::clock);
impl_clock_api!(ArrangerTrack::player::clock);
impl_midi_player!(SequencerTui::player);
impl_midi_player!(ArrangerTrack::player);
impl_midi_player!(PhrasePlayerModel);
use std::fmt::{Debug, Formatter, Error};
type DebugResult = std::result::Result<(), Error>;
impl Debug for ClockModel {
fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult {
f.debug_struct("editor")
.field("playing", &self.playing)
.field("started", &self.started)
.field("current", &self.current)
.field("quant", &self.quant)
.field("sync", &self.sync)
.finish()
}
}
impl Debug for TransportTui {
fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult {
f.debug_struct("Measure")
.field("jack", &self.jack)
.field("size", &self.size)
.field("cursor", &self.cursor)
.finish()
}
}
impl Debug for PhraseEditorModel {
fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult {
f.debug_struct("editor")
.field("note_axis", &self.time_axis)
.field("time_axis", &self.note_axis)
.finish()
}
}
impl Debug for PhrasePlayerModel {
fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult {
f.debug_struct("editor")
.field("clock", &self.clock)
.field("play_phrase", &self.play_phrase)
.field("next_phrase", &self.next_phrase)
.finish()
}
}

View file

@ -0,0 +1,68 @@
use crate::*;
/// Root view for standalone `tek_arranger`
pub struct ArrangerTui {
pub jack: Arc<RwLock<JackClient>>,
pub clock: ClockModel,
pub phrases: PhrasesModel,
pub tracks: Vec<ArrangerTrack>,
pub scenes: Vec<ArrangerScene>,
pub name: Arc<RwLock<String>>,
pub splits: [u16;2],
pub selected: ArrangerSelection,
pub mode: ArrangerMode,
pub color: ItemColor,
pub entered: bool,
pub size: Measure<Tui>,
pub cursor: (usize, usize),
pub menu_bar: Option<MenuBar<Tui, Self, ArrangerCommand>>,
pub status_bar: Option<ArrangerStatus>,
pub history: Vec<ArrangerCommand>,
pub note_buf: Vec<u8>,
pub midi_buf: Vec<Vec<Vec<u8>>>,
pub editor: PhraseEditorModel,
pub focus: FocusState<AppFocus<ArrangerFocus>>,
pub perf: PerfModel,
}
impl TryFrom<&Arc<RwLock<JackClient>>> for ArrangerTui {
type Error = Box<dyn std::error::Error>;
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
Ok(Self {
jack: jack.clone(),
clock: ClockModel::from(&Arc::new(jack.read().unwrap().transport())),
phrases: PhrasesModel::default(),
editor: PhraseEditorModel::default(),
selected: ArrangerSelection::Clip(0, 0),
scenes: vec![],
tracks: vec![],
color: Color::Rgb(28, 35, 25).into(),
history: vec![],
mode: ArrangerMode::Vertical(2),
name: Arc::new(RwLock::new(String::new())),
size: Measure::new(),
cursor: (0, 0),
splits: [20, 20],
entered: false,
menu_bar: None,
status_bar: None,
midi_buf: vec![vec![];65536],
note_buf: vec![],
focus: FocusState::Entered(AppFocus::Content(ArrangerFocus::Transport(TransportFocus::Bpm))),
perf: PerfModel::default(),
})
}
}
/// Sections in the arranger app that may be focused
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum ArrangerFocus {
/// The transport (toolbar) is focused
Transport(TransportFocus),
/// The arrangement (grid) is focused
Arranger,
/// The phrase list (pool) is focused
Phrases,
/// The phrase editor (sequencer) is focused
PhraseEditor,
}

View file

@ -0,0 +1,51 @@
use crate::*;
/// Root view for standalone `tek_sequencer`.
pub struct SequencerTui {
pub jack: Arc<RwLock<JackClient>>,
pub clock: ClockModel,
pub phrases: PhrasesModel,
pub player: PhrasePlayerModel,
pub editor: PhraseEditorModel,
pub size: Measure<Tui>,
pub cursor: (usize, usize),
pub split: u16,
pub entered: bool,
pub note_buf: Vec<u8>,
pub midi_buf: Vec<Vec<Vec<u8>>>,
pub focus: FocusState<AppFocus<SequencerFocus>>,
pub perf: PerfModel,
}
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(&Arc::new(jack.read().unwrap().transport()));
Ok(Self {
jack: jack.clone(),
phrases: PhrasesModel::default(),
player: PhrasePlayerModel::from(&clock),
editor: PhraseEditorModel::default(),
size: Measure::new(),
cursor: (0, 0),
entered: false,
split: 20,
midi_buf: vec![vec![];65536],
note_buf: vec![],
clock,
focus: FocusState::Entered(AppFocus::Content(SequencerFocus::Transport(TransportFocus::Bpm))),
perf: PerfModel::default(),
})
}
}
/// Sections in the sequencer app that may be focused
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum SequencerFocus {
/// The transport (toolbar) is focused
Transport(TransportFocus),
/// The phrase list (pool) is focused
Phrases,
/// The phrase editor (sequencer) is focused
PhraseEditor,
}

View file

@ -0,0 +1,56 @@
use crate::*;
/// Stores and displays time-related info.
pub struct TransportTui {
pub jack: Arc<RwLock<JackClient>>,
pub clock: ClockModel,
pub size: Measure<Tui>,
pub cursor: (usize, usize),
pub focus: FocusState<AppFocus<TransportFocus>>,
}
/// Create app state from JACK handle.
impl TryFrom<&Arc<RwLock<JackClient>>> for TransportTui {
type Error = Box<dyn std::error::Error>;
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
Ok(Self {
jack: jack.clone(),
clock: ClockModel::from(&Arc::new(jack.read().unwrap().transport())),
size: Measure::new(),
cursor: (0, 0),
focus: FocusState::Entered(AppFocus::Content(TransportFocus::Bpm))
})
}
}
/// Which item of the transport toolbar is focused
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransportFocus {
Bpm,
Sync,
PlayPause,
Clock,
Quant,
}
impl FocusWrap<TransportFocus> for TransportFocus {
fn wrap <'a, W: Widget<Engine = Tui>> (self, focus: TransportFocus, content: &'a W)
-> impl Widget<Engine = Tui> + 'a
{
let focused = focus == self;
let corners = focused.then_some(CORNERS);
let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50)));
lay!(corners, highlight, *content)
}
}
impl FocusWrap<TransportFocus> for Option<TransportFocus> {
fn wrap <'a, W: Widget<Engine = Tui>> (self, focus: TransportFocus, content: &'a W)
-> impl Widget<Engine = Tui> + 'a
{
let focused = Some(focus) == self;
let corners = focused.then_some(CORNERS);
let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50)));
lay!(corners, highlight, *content)
}
}

View file

@ -1,52 +0,0 @@
use crate::*;
/// Stores and displays time-related info.
pub struct TransportTui {
pub jack: Arc<RwLock<JackClient>>,
pub clock: ClockModel,
pub size: Measure<Tui>,
pub cursor: (usize, usize),
pub focus: FocusState<AppFocus<TransportFocus>>,
}
/// Root view for standalone `tek_sequencer`.
pub struct SequencerTui {
pub jack: Arc<RwLock<JackClient>>,
pub clock: ClockModel,
pub phrases: PhrasesModel,
pub player: PhrasePlayerModel,
pub editor: PhraseEditorModel,
pub size: Measure<Tui>,
pub cursor: (usize, usize),
pub split: u16,
pub entered: bool,
pub note_buf: Vec<u8>,
pub midi_buf: Vec<Vec<Vec<u8>>>,
pub focus: FocusState<AppFocus<SequencerFocus>>,
pub perf: PerfModel,
}
/// Root view for standalone `tek_arranger`
pub struct ArrangerTui {
pub jack: Arc<RwLock<JackClient>>,
pub clock: ClockModel,
pub phrases: PhrasesModel,
pub tracks: Vec<ArrangerTrack>,
pub scenes: Vec<ArrangerScene>,
pub name: Arc<RwLock<String>>,
pub splits: [u16;2],
pub selected: ArrangerSelection,
pub mode: ArrangerMode,
pub color: ItemColor,
pub entered: bool,
pub size: Measure<Tui>,
pub cursor: (usize, usize),
pub menu_bar: Option<MenuBar<Tui, Self, ArrangerCommand>>,
pub status_bar: Option<ArrangerStatus>,
pub history: Vec<ArrangerCommand>,
pub note_buf: Vec<u8>,
pub midi_buf: Vec<Vec<Vec<u8>>>,
pub editor: PhraseEditorModel,
pub focus: FocusState<AppFocus<ArrangerFocus>>,
pub perf: PerfModel,
}

View file

@ -1,370 +0,0 @@
use crate::*;
#[derive(Clone, Debug, PartialEq)]
pub enum TransportCommand {
Focus(FocusCommand),
Clock(ClockCommand),
}
impl<T: TransportControl> Command<T> for TransportCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use TransportCommand::{Focus, Clock};
use FocusCommand::{Next, Prev};
use ClockCommand::{SetBpm, SetQuant, SetSync};
Ok(match self {
Focus(cmd) => cmd.execute(state)?.map(Focus),
Clock(SetBpm(bpm)) => Some(Clock(SetBpm(state.bpm().set(bpm)))),
Clock(SetQuant(quant)) => Some(Clock(SetQuant(state.quant().set(quant)))),
Clock(SetSync(sync)) => Some(Clock(SetSync(state.sync().set(sync)))),
_ => return Ok(None)
})
}
}
#[derive(Clone, Debug)]
pub enum SequencerCommand {
Focus(FocusCommand),
Undo,
Redo,
Clear,
Clock(ClockCommand),
Phrases(PhrasesCommand),
Editor(PhraseCommand),
}
impl Command<SequencerTui> for SequencerCommand {
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
use SequencerCommand::*;
Ok(match self {
Focus(cmd) => cmd.execute(state)?.map(Focus),
Phrases(cmd) => cmd.execute(state)?.map(Phrases),
Editor(cmd) => cmd.execute(state)?.map(Editor),
Clock(cmd) => cmd.execute(state)?.map(Clock),
Undo => { todo!() },
Redo => { todo!() },
Clear => { todo!() },
})
}
}
#[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(state)?.map(Phrases),
Editor(cmd) => cmd.execute(state)?.map(Editor),
Clock(cmd) => cmd.execute(state)?.map(Clock),
Zoom(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)
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum PhrasesCommand {
Select(usize),
Phrase(PhrasePoolCommand),
Rename(PhraseRenameCommand),
Length(PhraseLengthCommand),
Import(FileBrowserCommand),
Export(FileBrowserCommand),
}
impl<T: PhrasesControl> Command<T> for PhrasesCommand {
fn execute (self, state: &mut T) -> 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
},
})
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PhraseLengthCommand {
Begin,
Cancel,
Set(usize),
Next,
Prev,
Inc,
Dec,
}
impl<T: PhrasesControl> Command<T> for PhraseLengthCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use PhraseLengthFocus::*;
use PhraseLengthCommand::*;
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)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum PhraseRenameCommand {
Begin,
Cancel,
Confirm,
Set(String),
}
impl<T: PhrasesControl> Command<T> for PhraseRenameCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use PhraseRenameCommand::*;
match state.phrases_mode_mut().clone() {
Some(PhrasesMode::Rename(phrase, ref mut old_name)) => match self {
Set(s) => {
state.phrases()[phrase].write().unwrap().name = s.into();
return Ok(Some(Self::Set(old_name.clone())))
},
Confirm => {
let old_name = old_name.clone();
*state.phrases_mode_mut() = None;
return Ok(Some(Self::Set(old_name)))
},
Cancel => {
state.phrases()[phrase].write().unwrap().name = old_name.clone();
},
_ => unreachable!()
},
_ => unreachable!()
};
Ok(None)
}
}
/// Commands supported by [FileBrowser]
#[derive(Debug, Clone, PartialEq)]
pub enum FileBrowserCommand {
Begin,
Cancel,
Confirm,
Select(usize),
Chdir(PathBuf),
Filter(String),
}
impl<T: PhrasesControl> Command<T> for FileBrowserCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use FileBrowserCommand::*;
use PhrasesMode::{Import, Export};
let mode = state.phrases_mode_mut();
match mode {
Some(Import(index, ref mut browser)) => match self {
Cancel => {
*mode = None;
},
Chdir(cwd) => {
*mode = Some(Import(*index, FileBrowser::new(Some(cwd))?));
},
Select(index) => {
browser.index = index;
},
Confirm => {
if browser.is_file() {
let index = *index;
let path = browser.path();
*mode = None;
PhrasePoolCommand::Import(index, path).execute(state)?;
} else if browser.is_dir() {
*mode = Some(Import(*index, browser.chdir()?));
}
},
_ => todo!(),
_ => unreachable!()
},
Some(PhrasesMode::Export(index, ref mut browser)) => match self {
Cancel => {
*mode = None;
},
Chdir(cwd) => {
*mode = Some(PhrasesMode::Export(*index, FileBrowser::new(Some(cwd))?));
},
Select(index) => {
browser.index = index;
},
_ => unreachable!()
},
_ => unreachable!(),
};
Ok(None)
}
}
#[derive(Clone, Debug)]
pub enum PhraseCommand {
// TODO: 1-9 seek markers that by default start every 8th of the phrase
ToggleDirection,
EnterEditMode,
ExitEditMode,
NoteAppend,
NoteSet,
NoteCursorSet(Option<usize>),
NoteLengthSet(usize),
NoteScrollSet(usize),
TimeCursorSet(Option<usize>),
TimeScrollSet(usize),
TimeZoomSet(usize),
Show(Option<Arc<RwLock<Phrase>>>),
}
impl<T: PhraseEditorControl + HasEnter> Command<T> for PhraseCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use PhraseCommand::*;
Ok(match self {
Show(phrase) => {
state.edit_phrase(phrase);
None
},
ToggleDirection => { todo!() },
EnterEditMode => {
state.focus_enter();
None
},
ExitEditMode => {
state.focus_exit();
None
},
NoteAppend => {
if state.phrase_editor_entered() {
state.put_note();
state.time_cursor_advance();
}
None
},
NoteSet => { if state.phrase_editor_entered() { state.put_note(); } None },
TimeCursorSet(time) => { state.time_axis().write().unwrap().point_set(time); None },
TimeScrollSet(time) => { state.time_axis().write().unwrap().start_set(time); None },
TimeZoomSet(zoom) => { state.time_axis().write().unwrap().scale_set(zoom); None },
NoteScrollSet(note) => { state.note_axis().write().unwrap().start_set(note); None },
NoteLengthSet(time) => { *state.note_len_mut() = time; None },
NoteCursorSet(note) => {
let mut axis = state.note_axis().write().unwrap();
axis.point_set(note);
if let Some(point) = axis.point {
if point > 73 {
axis.point = Some(73);
}
if point < axis.start {
axis.start = (point / 2) * 2;
}
}
None
},
_ => unreachable!()
})
}
}

View file

@ -1,131 +0,0 @@
use crate::*;
pub trait TransportControl: ClockApi + FocusGrid + HasEnter {
fn transport_focused (&self) -> Option<TransportFocus>;
}
pub trait SequencerControl: TransportControl {}
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 TransportControl for TransportTui {
fn transport_focused (&self) -> Option<TransportFocus> {
if let AppFocus::Content(focus) = self.focus.inner() {
Some(focus)
} else {
None
}
}
}
impl TransportControl for SequencerTui {
fn transport_focused (&self) -> Option<TransportFocus> {
if let AppFocus::Content(SequencerFocus::Transport(focus)) = self.focus.inner() {
Some(focus)
} else {
None
}
}
}
impl TransportControl for ArrangerTui {
fn transport_focused (&self) -> Option<TransportFocus> {
if let AppFocus::Content(ArrangerFocus::Transport(focus)) = self.focus.inner() {
Some(focus)
} else {
None
}
}
}
impl SequencerControl for SequencerTui {}
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.enqueue_next(phrase.as_ref());
}
}
if self.is_stopped() {
self.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].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();
}
}
}
}
}
pub trait PhrasesControl: HasPhrases {
fn phrase_index (&self) -> usize;
fn set_phrase_index (&self, index: usize);
fn phrases_mode (&self) -> &Option<PhrasesMode>;
fn phrases_mode_mut (&mut self) -> &mut Option<PhrasesMode>;
fn index_of (&self, phrase: &Phrase) -> Option<usize> {
for i in 0..self.phrases().len() {
if *self.phrases()[i].read().unwrap() == *phrase { return Some(i) }
}
return None
}
}
pub trait PhraseEditorControl: HasFocus {
fn edit_phrase (&mut self, phrase: Option<Arc<RwLock<Phrase>>>);
fn phrase_to_edit (&self) -> Option<Arc<RwLock<Phrase>>>;
fn phrase_editing (&self) -> &Option<Arc<RwLock<Phrase>>>;
fn phrase_editor_entered (&self) -> bool;
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>>;
fn note_axis (&self) -> &RwLock<FixedAxis<usize>>;
fn note_len (&self) -> usize;
fn note_len_mut (&mut self) -> &mut usize;
fn put_note (&mut self);
fn time_cursor_advance (&self) {
let point = self.time_axis().read().unwrap().point;
let length = self.phrase_editing().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
let forward = |time|(time + self.note_len()) % length;
self.time_axis().write().unwrap().point = point.map(forward);
}
}

View file

@ -0,0 +1,260 @@
use crate::*;
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(state)?.map(Phrases),
Editor(cmd) => cmd.execute(state)?.map(Editor),
Clock(cmd) => cmd.execute(state)?.map(Clock),
Zoom(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.enqueue_next(phrase.as_ref());
}
}
if self.is_stopped() {
self.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].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(state.phrase_to_edit().clone())),
_ => match state.focused() {
AppFocus::Menu => { todo!() },
AppFocus::Content(focused) => match focused {
ArrangerFocus::Transport(_) => {
use TransportCommand::{Clock, Focus};
match TransportCommand::input_to_command(state, input)? {
Clock(_) => { todo!() },
Focus(command) => Cmd::Focus(command)
}
},
ArrangerFocus::PhraseEditor => {
Cmd::Editor(PhraseCommand::input_to_command(state, input)?)
},
ArrangerFocus::Phrases => {
Cmd::Phrases(PhrasesCommand::input_to_command(state, 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
})
}

View file

@ -0,0 +1,116 @@
use crate::*;
/// Commands supported by [FileBrowser]
#[derive(Debug, Clone, PartialEq)]
pub enum FileBrowserCommand {
Begin,
Cancel,
Confirm,
Select(usize),
Chdir(PathBuf),
Filter(String),
}
impl<T: PhrasesControl> Command<T> for FileBrowserCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use FileBrowserCommand::*;
use PhrasesMode::{Import, Export};
let mode = state.phrases_mode_mut();
match mode {
Some(Import(index, ref mut browser)) => match self {
Cancel => {
*mode = None;
},
Chdir(cwd) => {
*mode = Some(Import(*index, FileBrowser::new(Some(cwd))?));
},
Select(index) => {
browser.index = index;
},
Confirm => {
if browser.is_file() {
let index = *index;
let path = browser.path();
*mode = None;
PhrasePoolCommand::Import(index, path).execute(state)?;
} else if browser.is_dir() {
*mode = Some(Import(*index, browser.chdir()?));
}
},
_ => todo!(),
_ => unreachable!()
},
Some(PhrasesMode::Export(index, ref mut browser)) => match self {
Cancel => {
*mode = None;
},
Chdir(cwd) => {
*mode = Some(PhrasesMode::Export(*index, FileBrowser::new(Some(cwd))?));
},
Select(index) => {
browser.index = index;
},
_ => unreachable!()
},
_ => unreachable!(),
};
Ok(None)
}
}
impl<T: PhrasesControl> InputToCommand<Tui, T> for FileBrowserCommand {
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
use KeyCode::{Up, Down, Right, Left, Enter, Esc, Char, Backspace};
use FileBrowserCommand::*;
if let Some(PhrasesMode::Import(index, browser)) = state.phrases_mode() {
Some(match from.event() {
key!(Up) => Select(
browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1))
),
key!(Down) => Select(
browser.index.saturating_add(1) % browser.len()
),
key!(Right) => Chdir(browser.cwd.clone()),
key!(Left) => Chdir(browser.cwd.clone()),
key!(Enter) => Confirm,
key!(Char(c)) => { todo!() },
key!(Backspace) => { todo!() },
key!(Esc) => Self::Cancel,
_ => return None
})
} else if let Some(PhrasesMode::Export(index, browser)) = state.phrases_mode() {
Some(match from.event() {
key!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())),
key!(Down) => Select(browser.index.saturating_add(1) % browser.len()),
key!(Right) => Chdir(browser.cwd.clone()),
key!(Left) => Chdir(browser.cwd.clone()),
key!(Enter) => Confirm,
key!(Char(c)) => { todo!() },
key!(Backspace) => { todo!() },
key!(Esc) => Self::Cancel,
_ => return None
})
} else {
unreachable!()
}
}
}
impl<T: PhrasesControl> InputToCommand<Tui, T> for PhraseLengthCommand {
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
use KeyCode::{Up, Down, Right, Left, Enter, Esc};
if let Some(PhrasesMode::Length(_, length, _)) = state.phrases_mode() {
Some(match from.event() {
key!(Up) => Self::Inc,
key!(Down) => Self::Dec,
key!(Right) => Self::Next,
key!(Left) => Self::Prev,
key!(Enter) => Self::Set(*length),
key!(Esc) => Self::Cancel,
_ => return None
})
} else {
unreachable!()
}
}
}

View file

@ -0,0 +1,183 @@
use crate::*;
#[derive(Clone, Debug)]
pub enum PhraseCommand {
// TODO: 1-9 seek markers that by default start every 8th of the phrase
ToggleDirection,
EnterEditMode,
ExitEditMode,
NoteAppend,
NoteSet,
NoteCursorSet(Option<usize>),
NoteLengthSet(usize),
NoteScrollSet(usize),
TimeCursorSet(Option<usize>),
TimeScrollSet(usize),
TimeZoomSet(usize),
Show(Option<Arc<RwLock<Phrase>>>),
}
impl<T: PhraseEditorControl + HasEnter> InputToCommand<Tui, T> for PhraseCommand {
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
use PhraseCommand::*;
use KeyCode::{Char, Enter, Esc, Up, Down, PageUp, PageDown, Left, Right};
Some(match from.event() {
key!(Char('`')) => ToggleDirection,
key!(Enter) => EnterEditMode,
key!(Esc) => ExitEditMode,
key!(Char('a')) => NoteAppend,
key!(Char('s')) => NoteSet,
key!(Char('[')) => NoteLengthSet(prev_note_length(state.note_len())),
key!(Char(']')) => NoteLengthSet(next_note_length(state.note_len())),
key!(Char('-')) => TimeZoomSet(next_note_length(state.time_axis().read().unwrap().scale)),
key!(Char('_')) => TimeZoomSet(next_note_length(state.time_axis().read().unwrap().scale)),
key!(Char('=')) => TimeZoomSet(prev_note_length(state.time_axis().read().unwrap().scale)),
key!(Char('+')) => TimeZoomSet(prev_note_length(state.time_axis().read().unwrap().scale)),
key!(Up) => match state.phrase_editor_entered() {
true => NoteCursorSet(state.note_axis().write().unwrap().point_plus(1)),
false => NoteScrollSet(state.note_axis().write().unwrap().start_plus(1)),
},
key!(Down) => match state.phrase_editor_entered() {
true => NoteCursorSet(state.note_axis().write().unwrap().point_minus(1)),
false => NoteScrollSet(state.note_axis().write().unwrap().start_minus(1)),
},
key!(PageUp) => match state.phrase_editor_entered() {
true => NoteCursorSet(state.note_axis().write().unwrap().point_plus(3)),
false => NoteScrollSet(state.note_axis().write().unwrap().start_plus(3)),
},
key!(PageDown) => match state.phrase_editor_entered() {
true => NoteCursorSet(state.note_axis().write().unwrap().point_minus(3)),
false => NoteScrollSet(state.note_axis().write().unwrap().start_minus(3)),
},
key!(Left) => match state.phrase_editor_entered() {
true => TimeCursorSet(state.note_axis().write().unwrap().point_minus(1)),
false => TimeScrollSet(state.note_axis().write().unwrap().start_minus(1)),
},
key!(Right) => match state.phrase_editor_entered() {
true => TimeCursorSet(state.note_axis().write().unwrap().point_plus(1)),
false => TimeScrollSet(state.note_axis().write().unwrap().start_plus(1)),
},
_ => return None
})
}
}
impl<T: PhraseEditorControl + HasEnter> Command<T> for PhraseCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use PhraseCommand::*;
Ok(match self {
Show(phrase) => {
state.edit_phrase(phrase);
None
},
ToggleDirection => { todo!() },
EnterEditMode => {
state.focus_enter();
None
},
ExitEditMode => {
state.focus_exit();
None
},
NoteAppend => {
if state.phrase_editor_entered() {
state.put_note();
state.time_cursor_advance();
}
None
},
NoteSet => { if state.phrase_editor_entered() { state.put_note(); } None },
TimeCursorSet(time) => { state.time_axis().write().unwrap().point_set(time); None },
TimeScrollSet(time) => { state.time_axis().write().unwrap().start_set(time); None },
TimeZoomSet(zoom) => { state.time_axis().write().unwrap().scale_set(zoom); None },
NoteScrollSet(note) => { state.note_axis().write().unwrap().start_set(note); None },
NoteLengthSet(time) => { *state.note_len_mut() = time; None },
NoteCursorSet(note) => {
let mut axis = state.note_axis().write().unwrap();
axis.point_set(note);
if let Some(point) = axis.point {
if point > 73 {
axis.point = Some(73);
}
if point < axis.start {
axis.start = (point / 2) * 2;
}
}
None
},
_ => unreachable!()
})
}
}
pub trait PhraseEditorControl: HasFocus {
fn edit_phrase (&mut self, phrase: Option<Arc<RwLock<Phrase>>>);
fn phrase_to_edit (&self) -> Option<Arc<RwLock<Phrase>>>;
fn phrase_editing (&self) -> &Option<Arc<RwLock<Phrase>>>;
fn phrase_editor_entered (&self) -> bool;
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>>;
fn note_axis (&self) -> &RwLock<FixedAxis<usize>>;
fn note_len (&self) -> usize;
fn note_len_mut (&mut self) -> &mut usize;
fn put_note (&mut self);
fn time_cursor_advance (&self) {
let point = self.time_axis().read().unwrap().point;
let length = self.phrase_editing().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
let forward = |time|(time + self.note_len()) % length;
self.time_axis().write().unwrap().point = point.map(forward);
}
}
macro_rules! impl_phrase_editor_control {
(
$Struct:ident $(:: $field:ident)*
[$Focus:expr]
[$self1:ident: $phrase_to_edit:expr]
[$self2:ident, $phrase:ident: $edit_phrase:expr]
) => {
impl PhraseEditorControl for $Struct {
fn phrase_to_edit (&$self1) -> Option<Arc<RwLock<Phrase>>> {
$phrase_to_edit
}
fn edit_phrase (&mut $self2, $phrase: Option<Arc<RwLock<Phrase>>>) {
$edit_phrase
//self.editor.show(self.selected_phrase().as_ref());
//state.editor.phrase = phrase.clone();
//state.focus(ArrangerFocus::PhraseEditor);
//state.focus_enter();
//todo!("edit_phrase")
}
fn phrase_editing (&self) -> &Option<Arc<RwLock<Phrase>>> {
todo!("phrase_editing")
}
fn phrase_editor_entered (&self) -> bool {
self.entered && self.focused() == $Focus
}
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
&self.editor.time_axis
}
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
&self.editor.note_axis
}
fn note_len (&self) -> usize {
self.editor.note_len
}
fn note_len_mut (&mut self) -> &mut usize {
&mut self.editor.note_len
}
fn put_note (&mut self) {
todo!("put_note")
}
}
}
}
impl_phrase_editor_control!(SequencerTui
[AppFocus::Content(SequencerFocus::PhraseEditor)]
[self: Some(self.phrases.phrases[self.phrases.phrase.load(Ordering::Relaxed)].clone())]
[self, phrase: self.editor.show(phrase)]
);
impl_phrase_editor_control!(ArrangerTui
[AppFocus::Content(ArrangerFocus::PhraseEditor)]
[self: todo!()]
[self, phrase: todo!()]
);

View file

@ -0,0 +1,47 @@
use crate::*;
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PhraseLengthCommand {
Begin,
Cancel,
Set(usize),
Next,
Prev,
Inc,
Dec,
}
impl<T: PhrasesControl> Command<T> for PhraseLengthCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use PhraseLengthFocus::*;
use PhraseLengthCommand::*;
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)
}
}

View file

@ -0,0 +1,181 @@
use crate::*;
#[derive(Clone, PartialEq, Debug)]
pub enum PhrasesCommand {
Select(usize),
Phrase(PhrasePoolCommand),
Rename(PhraseRenameCommand),
Length(PhraseLengthCommand),
Import(FileBrowserCommand),
Export(FileBrowserCommand),
}
impl<T: PhrasesControl> Command<T> for PhrasesCommand {
fn execute (self, state: &mut T) -> 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
},
})
}
}
pub trait PhrasesControl: HasPhrases {
fn phrase_index (&self) -> usize;
fn set_phrase_index (&self, index: usize);
fn phrases_mode (&self) -> &Option<PhrasesMode>;
fn phrases_mode_mut (&mut self) -> &mut Option<PhrasesMode>;
fn index_of (&self, phrase: &Phrase) -> Option<usize> {
for i in 0..self.phrases().len() {
if *self.phrases()[i].read().unwrap() == *phrase { return Some(i) }
}
return None
}
}
macro_rules! impl_phrases_control {
($Struct:ident $(:: $field:ident)*) => {
impl PhrasesControl for $Struct {
fn phrase_index (&self) -> usize {
self.phrases.phrase.load(Ordering::Relaxed)
}
fn set_phrase_index (&self, value: usize) {
self.phrases.phrase.store(value, Ordering::Relaxed);
}
fn phrases_mode (&self) -> &Option<PhrasesMode> {
&self.phrases.mode
}
fn phrases_mode_mut (&mut self) -> &mut Option<PhrasesMode> {
&mut self.phrases.mode
}
}
}
}
impl_phrases_control!(SequencerTui);
impl_phrases_control!(ArrangerTui);
macro_rules! impl_has_phrases {
($Struct:ident $(:: $field:ident)*) => {
impl HasPhrases for $Struct {
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
&self$(.$field)*.phrases
}
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
&mut self$(.$field)*.phrases
}
}
}
}
impl_has_phrases!(PhrasesModel);
impl_has_phrases!(SequencerTui::phrases);
impl_has_phrases!(ArrangerTui::phrases);
impl<T: PhrasesControl> InputToCommand<Tui, T> for PhrasesCommand {
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
use PhraseRenameCommand as Rename;
use PhraseLengthCommand as Length;
use FileBrowserCommand as Browse;
Some(match state.phrases_mode() {
Some(PhrasesMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?),
Some(PhrasesMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?),
Some(PhrasesMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?),
Some(PhrasesMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?),
_ => to_phrases_command(state, input)?
})
}
}
fn to_phrases_command <T: PhrasesControl> (state: &T, input: &TuiInput) -> Option<PhrasesCommand> {
use KeyCode::{Up, Down, Delete, Char};
use PhrasesCommand as Cmd;
use PhrasePoolCommand as Pool;
use PhraseRenameCommand as Rename;
use PhraseLengthCommand as Length;
use FileBrowserCommand as Browse;
let index = state.phrase_index();
let count = state.phrases().len();
Some(match input.event() {
key!(Char('n')) => Cmd::Rename(Rename::Begin),
key!(Char('t')) => Cmd::Length(Length::Begin),
key!(Char('m')) => Cmd::Import(Browse::Begin),
key!(Char('x')) => Cmd::Export(Browse::Begin),
key!(Up) => Cmd::Select(index.overflowing_sub(1).0.min(state.phrases().len() - 1)),
key!(Down) => Cmd::Select(index.saturating_add(1) % state.phrases().len()),
key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())),
key!(Char(',')) => if index > 1 {
state.set_phrase_index(state.phrase_index().saturating_sub(1));
Cmd::Phrase(Pool::Swap(index - 1, index))
} else {
return None
},
key!(Char('.')) => if index < count.saturating_sub(1) {
state.set_phrase_index(state.phrase_index() + 1);
Cmd::Phrase(Pool::Swap(index + 1, index))
} else {
return None
},
key!(Delete) => if index > 0 {
state.set_phrase_index(index.min(count.saturating_sub(1)));
Cmd::Phrase(Pool::Delete(index))
} else {
return None
},
key!(Char('a')) => Cmd::Phrase(Pool::Add(
count, Phrase::new(
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
)
)),
key!(Char('i')) => Cmd::Phrase(Pool::Add(
index + 1, Phrase::new(
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
)
)),
key!(Char('d')) => {
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
Cmd::Phrase(Pool::Add(index + 1, phrase))
},
_ => return None
})
}

View file

@ -0,0 +1,60 @@
use crate::*;
#[derive(Clone, Debug, PartialEq)]
pub enum PhraseRenameCommand {
Begin,
Cancel,
Confirm,
Set(String),
}
impl<T: PhrasesControl> Command<T> for PhraseRenameCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use PhraseRenameCommand::*;
match state.phrases_mode_mut().clone() {
Some(PhrasesMode::Rename(phrase, ref mut old_name)) => match self {
Set(s) => {
state.phrases()[phrase].write().unwrap().name = s.into();
return Ok(Some(Self::Set(old_name.clone())))
},
Confirm => {
let old_name = old_name.clone();
*state.phrases_mode_mut() = None;
return Ok(Some(Self::Set(old_name)))
},
Cancel => {
state.phrases()[phrase].write().unwrap().name = old_name.clone();
},
_ => unreachable!()
},
_ => unreachable!()
};
Ok(None)
}
}
impl<T: PhrasesControl> InputToCommand<Tui, T> for PhraseRenameCommand {
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
use KeyCode::{Char, Backspace, Enter, Esc};
if let Some(PhrasesMode::Rename(_, ref old_name)) = state.phrases_mode() {
Some(match from.event() {
key!(Char(c)) => {
let mut new_name = old_name.clone();
new_name.push(*c);
Self::Set(new_name)
},
key!(Backspace) => {
let mut new_name = old_name.clone();
new_name.pop();
Self::Set(new_name)
},
key!(Enter) => Self::Confirm,
key!(Esc) => Self::Cancel,
_ => return None
})
} else {
unreachable!()
}
}
}

View file

@ -0,0 +1,88 @@
use crate::*;
impl Handle<Tui> for SequencerTui {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
SequencerCommand::execute_with_state(self, i)
}
}
pub trait SequencerControl: TransportControl {}
impl SequencerControl for SequencerTui {}
#[derive(Clone, Debug)]
pub enum SequencerCommand {
Focus(FocusCommand),
Undo,
Redo,
Clear,
Clock(ClockCommand),
Phrases(PhrasesCommand),
Editor(PhraseCommand),
}
impl Command<SequencerTui> for SequencerCommand {
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
use SequencerCommand::*;
Ok(match self {
Focus(cmd) => cmd.execute(state)?.map(Focus),
Phrases(cmd) => cmd.execute(state)?.map(Phrases),
Editor(cmd) => cmd.execute(state)?.map(Editor),
Clock(cmd) => cmd.execute(state)?.map(Clock),
Undo => { todo!() },
Redo => { todo!() },
Clear => { todo!() },
})
}
}
impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
to_sequencer_command(state, input)
.or_else(||to_focus_command(input).map(SequencerCommand::Focus))
}
}
pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option<SequencerCommand> {
use SequencerCommand::*;
use KeyCode::Char;
if !state.entered() {
return None
}
Some(match input.event() {
key!(Char('e')) => Editor(
PhraseCommand::Show(state.phrase_to_edit().clone())
),
key!(Char(' ')) => Clock(
if let Some(TransportState::Stopped) = *state.clock.playing.read().unwrap() {
ClockCommand::Play(None)
} else {
ClockCommand::Pause(None)
}
),
key!(Shift-Char(' ')) => Clock(
if let Some(TransportState::Stopped) = *state.clock.playing.read().unwrap() {
ClockCommand::Play(Some(0))
} else {
ClockCommand::Pause(Some(0))
}
),
_ => match state.focused() {
AppFocus::Menu => { todo!() },
AppFocus::Content(focused) => match focused {
SequencerFocus::Transport(_) => {
match TransportCommand::input_to_command(state, input)? {
TransportCommand::Clock(_) => { todo!() },
TransportCommand::Focus(command) => Focus(command),
}
},
SequencerFocus::Phrases => Phrases(
PhrasesCommand::input_to_command(state, input)?
),
SequencerFocus::PhraseEditor => Editor(
PhraseCommand::input_to_command(state, input)?
),
}
}
})
}

View file

@ -0,0 +1,119 @@
use crate::*;
impl Handle<Tui> for TransportTui {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
TransportCommand::execute_with_state(self, from)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum TransportCommand {
Focus(FocusCommand),
Clock(ClockCommand),
}
impl<T: TransportControl> Command<T> for TransportCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use TransportCommand::{Focus, Clock};
use FocusCommand::{Next, Prev};
use ClockCommand::{SetBpm, SetQuant, SetSync};
Ok(match self {
Focus(cmd) => cmd.execute(state)?.map(Focus),
Clock(SetBpm(bpm)) => Some(Clock(SetBpm(state.bpm().set(bpm)))),
Clock(SetQuant(quant)) => Some(Clock(SetQuant(state.quant().set(quant)))),
Clock(SetSync(sync)) => Some(Clock(SetSync(state.sync().set(sync)))),
_ => return Ok(None)
})
}
}
pub trait TransportControl: ClockApi + FocusGrid + HasEnter {
fn transport_focused (&self) -> Option<TransportFocus>;
}
impl TransportControl for TransportTui {
fn transport_focused (&self) -> Option<TransportFocus> {
if let AppFocus::Content(focus) = self.focus.inner() {
Some(focus)
} else {
None
}
}
}
impl TransportControl for SequencerTui {
fn transport_focused (&self) -> Option<TransportFocus> {
if let AppFocus::Content(SequencerFocus::Transport(focus)) = self.focus.inner() {
Some(focus)
} else {
None
}
}
}
impl TransportControl for ArrangerTui {
fn transport_focused (&self) -> Option<TransportFocus> {
if let AppFocus::Content(ArrangerFocus::Transport(focus)) = self.focus.inner() {
Some(focus)
} else {
None
}
}
}
impl<T: TransportControl> InputToCommand<Tui, T> for TransportCommand {
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
to_transport_command(state, input)
.or_else(||to_focus_command(input).map(TransportCommand::Focus))
}
}
pub fn to_transport_command <T> (state: &T, input: &TuiInput) -> Option<TransportCommand>
where
T: TransportControl
{
use ClockCommand::{SetBpm, SetQuant, SetSync};
use TransportCommand::{Focus, Clock};
use KeyCode::{Enter, Left, Right, Char};
Some(match input.event() {
key!(Left) => Focus(FocusCommand::Prev),
key!(Right) => Focus(FocusCommand::Next),
key!(Char(' ')) => todo!("toolbar space"),
key!(Shift-Char(' ')) => todo!("toolbar shift-space"),
_ => match state.transport_focused().unwrap() {
TransportFocus::Bpm => match input.event() {
key!(Char(',')) => Clock(SetBpm(state.bpm().get() - 1.0)),
key!(Char('.')) => Clock(SetBpm(state.bpm().get() + 1.0)),
key!(Char('<')) => Clock(SetBpm(state.bpm().get() - 0.001)),
key!(Char('>')) => Clock(SetBpm(state.bpm().get() + 0.001)),
_ => return None,
},
TransportFocus::Quant => match input.event() {
key!(Char(',')) => Clock(SetQuant(state.quant().prev())),
key!(Char('.')) => Clock(SetQuant(state.quant().next())),
key!(Char('<')) => Clock(SetQuant(state.quant().prev())),
key!(Char('>')) => Clock(SetQuant(state.quant().next())),
_ => return None,
},
TransportFocus::Sync => match input.event() {
key!(Char(',')) => Clock(SetSync(state.sync().prev())),
key!(Char('.')) => Clock(SetSync(state.sync().next())),
key!(Char('<')) => Clock(SetSync(state.sync().prev())),
key!(Char('>')) => Clock(SetSync(state.sync().next())),
_ => return None,
},
TransportFocus::Clock => match input.event() {
key!(Char(',')) => todo!("transport seek bar"),
key!(Char('.')) => todo!("transport seek bar"),
key!(Char('<')) => todo!("transport seek beat"),
key!(Char('>')) => todo!("transport seek beat"),
_ => return None,
},
TransportFocus::PlayPause => match input.event() {
key!(Enter) => todo!("transport play toggle"),
key!(Shift-Enter) => todo!("transport shift-play toggle"),
_ => return None,
},
}
})
}

View file

@ -1,47 +0,0 @@
// Not all fields are included here. Add as needed.
use crate::*;
use std::fmt::{Debug, Formatter, Error};
type DebugResult = std::result::Result<(), Error>;
impl Debug for ClockModel {
fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult {
f.debug_struct("editor")
.field("playing", &self.playing)
.field("started", &self.started)
.field("current", &self.current)
.field("quant", &self.quant)
.field("sync", &self.sync)
.finish()
}
}
impl Debug for TransportTui {
fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult {
f.debug_struct("Measure")
.field("jack", &self.jack)
.field("size", &self.size)
.field("cursor", &self.cursor)
.finish()
}
}
impl Debug for PhraseEditorModel {
fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult {
f.debug_struct("editor")
.field("note_axis", &self.time_axis)
.field("time_axis", &self.note_axis)
.finish()
}
}
impl Debug for PhrasePlayerModel {
fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult {
f.debug_struct("editor")
.field("clock", &self.clock)
.field("play_phrase", &self.play_phrase)
.field("next_phrase", &self.next_phrase)
.finish()
}
}

View file

@ -8,67 +8,11 @@ pub enum AppFocus<T: Copy + Debug + PartialEq> {
Content(T)
}
/// Which item of the transport toolbar is focused
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransportFocus {
Bpm,
Sync,
PlayPause,
Clock,
Quant,
}
/// Sections in the sequencer app that may be focused
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum SequencerFocus {
/// The transport (toolbar) is focused
Transport(TransportFocus),
/// The phrase list (pool) is focused
Phrases,
/// The phrase editor (sequencer) is focused
PhraseEditor,
}
/// Sections in the arranger app that may be focused
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum ArrangerFocus {
/// The transport (toolbar) is focused
Transport(TransportFocus),
/// The arrangement (grid) is focused
Arranger,
/// The phrase list (pool) is focused
Phrases,
/// The phrase editor (sequencer) is focused
PhraseEditor,
}
pub trait FocusWrap<T> {
fn wrap <'a, W: Widget<Engine = Tui>> (self, focus: T, content: &'a W)
-> impl Widget<Engine = Tui> + 'a;
}
impl FocusWrap<TransportFocus> for TransportFocus {
fn wrap <'a, W: Widget<Engine = Tui>> (self, focus: TransportFocus, content: &'a W)
-> impl Widget<Engine = Tui> + 'a
{
let focused = focus == self;
let corners = focused.then_some(CORNERS);
let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50)));
lay!(corners, highlight, *content)
}
}
impl FocusWrap<TransportFocus> for Option<TransportFocus> {
fn wrap <'a, W: Widget<Engine = Tui>> (self, focus: TransportFocus, content: &'a W)
-> impl Widget<Engine = Tui> + 'a
{
let focused = Some(focus) == self;
let corners = focused.then_some(CORNERS);
let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50)));
lay!(corners, highlight, *content)
}
}
macro_rules! impl_focus {
($Struct:ident $Focus:ident $Grid:expr) => {
impl HasFocus for $Struct {
@ -175,31 +119,3 @@ impl_focus!(ArrangerTui ArrangerFocus [
Content(PhraseEditor),
],
]);
/// 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,
}
}
}

View file

@ -1,27 +0,0 @@
use crate::*;
impl Handle<Tui> for TransportTui {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
TransportCommand::execute_with_state(self, from)
}
}
impl Handle<Tui> for SequencerTui {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
SequencerCommand::execute_with_state(self, i)
}
}
impl Handle<Tui> for ArrangerTui {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
ArrangerCommand::execute_with_state(self, i)
}
}
//impl Handle<Tui> for PhrasesModel {
//fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
//PhrasesCommand::execute_with_state(self, from)
//}
//}
//impl Handle<Tui> for PhraseEditorModel {
//fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
//PhraseCommand::execute_with_state(self, from)
//}
//}

View file

@ -1,208 +1 @@
use crate::*;
macro_rules! impl_jack_api {
($Struct:ident $(:: $field:ident)*) => {
impl JackApi for $Struct {
fn jack (&self) -> &Arc<RwLock<JackClient>> {
&self$(.$field)*
}
}
}
}
macro_rules! impl_clock_api {
($Struct:ident $(:: $field:ident)*) => {
impl ClockApi for $Struct {
fn quant (&self) -> &Arc<Quantize> {
&self$(.$field)*.quant
}
fn sync (&self) -> &Arc<LaunchSync> {
&self$(.$field)*.sync
}
fn current (&self) -> &Arc<Instant> {
&self$(.$field)*.current
}
fn transport_handle (&self) -> &Arc<Transport> {
&self$(.$field)*.transport
}
fn transport_state (&self) -> &Arc<RwLock<Option<TransportState>>> {
&self$(.$field)*.playing
}
fn transport_offset (&self) -> &Arc<RwLock<Option<(usize, usize)>>> {
&self$(.$field)*.started
}
}
}
}
macro_rules! impl_has_phrases {
($Struct:ident $(:: $field:ident)*) => {
impl HasPhrases for $Struct {
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
&self$(.$field)*.phrases
}
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
&mut self$(.$field)*.phrases
}
}
}
}
macro_rules! impl_midi_player {
($Struct:ident $(:: $field:ident)*) => {
impl HasPhrase for $Struct {
fn reset (&self) -> bool {
self$(.$field)*.reset
}
fn reset_mut (&mut self) -> &mut bool {
&mut self$(.$field)*.reset
}
fn play_phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
&self$(.$field)*.play_phrase
}
fn play_phrase_mut (&mut self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
&mut self$(.$field)*.play_phrase
}
fn next_phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
&self$(.$field)*.next_phrase
}
fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
&mut self$(.$field)*.next_phrase
}
}
impl MidiInputApi for $Struct {
fn midi_ins (&self) -> &Vec<Port<jack::MidiIn>> {
&self$(.$field)*.midi_ins
}
fn midi_ins_mut (&mut self) -> &mut Vec<Port<jack::MidiIn>> {
&mut self$(.$field)*.midi_ins
}
fn recording (&self) -> bool {
self$(.$field)*.recording
}
fn recording_mut (&mut self) -> &mut bool {
&mut self$(.$field)*.recording
}
fn monitoring (&self) -> bool {
self$(.$field)*.monitoring
}
fn monitoring_mut (&mut self) -> &mut bool {
&mut self$(.$field)*.monitoring
}
fn overdub (&self) -> bool {
self$(.$field)*.overdub
}
fn overdub_mut (&mut self) -> &mut bool {
&mut self$(.$field)*.overdub
}
fn notes_in (&self) -> &Arc<RwLock<[bool; 128]>> {
&self$(.$field)*.notes_in
}
}
impl MidiOutputApi for $Struct {
fn midi_outs (&self) -> &Vec<Port<jack::MidiOut>> {
&self$(.$field)*.midi_outs
}
fn midi_outs_mut (&mut self) -> &mut Vec<Port<jack::MidiOut>> {
&mut self$(.$field)*.midi_outs
}
fn midi_note (&mut self) -> &mut Vec<u8> {
&mut self$(.$field)*.note_buf
}
fn notes_out (&self) -> &Arc<RwLock<[bool; 128]>> {
&self$(.$field)*.notes_in
}
}
impl MidiPlayerApi for $Struct {}
}
}
macro_rules! impl_phrases_control {
($Struct:ident $(:: $field:ident)*) => {
impl PhrasesControl for $Struct {
fn phrase_index (&self) -> usize {
self.phrases.phrase.load(Ordering::Relaxed)
}
fn set_phrase_index (&self, value: usize) {
self.phrases.phrase.store(value, Ordering::Relaxed);
}
fn phrases_mode (&self) -> &Option<PhrasesMode> {
&self.phrases.mode
}
fn phrases_mode_mut (&mut self) -> &mut Option<PhrasesMode> {
&mut self.phrases.mode
}
}
}
}
macro_rules! impl_phrase_editor_control {
(
$Struct:ident $(:: $field:ident)*
[$Focus:expr]
[$self1:ident: $phrase_to_edit:expr]
[$self2:ident, $phrase:ident: $edit_phrase:expr]
) => {
impl PhraseEditorControl for $Struct {
fn phrase_to_edit (&$self1) -> Option<Arc<RwLock<Phrase>>> {
$phrase_to_edit
}
fn edit_phrase (&mut $self2, $phrase: Option<Arc<RwLock<Phrase>>>) {
$edit_phrase
//self.editor.show(self.selected_phrase().as_ref());
//state.editor.phrase = phrase.clone();
//state.focus(ArrangerFocus::PhraseEditor);
//state.focus_enter();
//todo!("edit_phrase")
}
fn phrase_editing (&self) -> &Option<Arc<RwLock<Phrase>>> {
todo!("phrase_editing")
}
fn phrase_editor_entered (&self) -> bool {
self.entered && self.focused() == $Focus
}
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
&self.editor.time_axis
}
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
&self.editor.note_axis
}
fn note_len (&self) -> usize {
self.editor.note_len
}
fn note_len_mut (&mut self) -> &mut usize {
&mut self.editor.note_len
}
fn put_note (&mut self) {
todo!("put_note")
}
}
}
}
impl_jack_api!(TransportTui::jack);
impl_jack_api!(SequencerTui::jack);
impl_jack_api!(ArrangerTui::jack);
impl_clock_api!(TransportTui::clock);
impl_clock_api!(SequencerTui::clock);
impl_clock_api!(ArrangerTui::clock);
impl_clock_api!(PhrasePlayerModel::clock);
impl_clock_api!(ArrangerTrack::player::clock);
impl_has_phrases!(PhrasesModel);
impl_has_phrases!(SequencerTui::phrases);
impl_has_phrases!(ArrangerTui::phrases);
impl_midi_player!(SequencerTui::player);
impl_midi_player!(ArrangerTrack::player);
impl_midi_player!(PhrasePlayerModel);
impl_phrases_control!(SequencerTui);
impl_phrases_control!(ArrangerTui);
impl_phrase_editor_control!(SequencerTui
[AppFocus::Content(SequencerFocus::PhraseEditor)]
[self: Some(self.phrases.phrases[self.phrases.phrase.load(Ordering::Relaxed)].clone())]
[self, phrase: self.editor.show(phrase)]
);
impl_phrase_editor_control!(ArrangerTui
[AppFocus::Content(ArrangerFocus::PhraseEditor)]
[self: todo!()]
[self, phrase: todo!()]
);

View file

@ -1,66 +0,0 @@
use crate::*;
/// Create app state from JACK handle.
impl TryFrom<&Arc<RwLock<JackClient>>> for TransportTui {
type Error = Box<dyn std::error::Error>;
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
Ok(Self {
jack: jack.clone(),
clock: ClockModel::from(&Arc::new(jack.read().unwrap().transport())),
size: Measure::new(),
cursor: (0, 0),
focus: FocusState::Entered(AppFocus::Content(TransportFocus::Bpm))
})
}
}
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(&Arc::new(jack.read().unwrap().transport()));
Ok(Self {
jack: jack.clone(),
phrases: PhrasesModel::default(),
player: PhrasePlayerModel::from(&clock),
editor: PhraseEditorModel::default(),
size: Measure::new(),
cursor: (0, 0),
entered: false,
split: 20,
midi_buf: vec![vec![];65536],
note_buf: vec![],
clock,
focus: FocusState::Entered(AppFocus::Content(SequencerFocus::Transport(TransportFocus::Bpm))),
perf: PerfModel::default(),
})
}
}
impl TryFrom<&Arc<RwLock<JackClient>>> for ArrangerTui {
type Error = Box<dyn std::error::Error>;
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
Ok(Self {
jack: jack.clone(),
clock: ClockModel::from(&Arc::new(jack.read().unwrap().transport())),
phrases: PhrasesModel::default(),
editor: PhraseEditorModel::default(),
selected: ArrangerSelection::Clip(0, 0),
scenes: vec![],
tracks: vec![],
color: Color::Rgb(28, 35, 25).into(),
history: vec![],
mode: ArrangerMode::Vertical(2),
name: Arc::new(RwLock::new(String::new())),
size: Measure::new(),
cursor: (0, 0),
splits: [20, 20],
entered: false,
menu_bar: None,
status_bar: None,
midi_buf: vec![vec![];65536],
note_buf: vec![],
focus: FocusState::Entered(AppFocus::Content(ArrangerFocus::Transport(TransportFocus::Bpm))),
perf: PerfModel::default(),
})
}
}

View file

@ -1,455 +0,0 @@
use crate::*;
impl<T: TransportControl> InputToCommand<Tui, T> for TransportCommand {
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
to_transport_command(state, input)
.or_else(||to_focus_command(input).map(TransportCommand::Focus))
}
}
impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
to_sequencer_command(state, input)
.or_else(||to_focus_command(input).map(SequencerCommand::Focus))
}
}
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_focus_command (input: &TuiInput) -> Option<FocusCommand> {
use KeyCode::{Tab, BackTab, Up, Down, Left, Right, Enter, Esc};
Some(match input.event() {
key!(Tab) => FocusCommand::Next,
key!(Shift-Tab) => FocusCommand::Prev,
key!(BackTab) => FocusCommand::Prev,
key!(Shift-BackTab) => FocusCommand::Prev,
key!(Up) => FocusCommand::Up,
key!(Down) => FocusCommand::Down,
key!(Left) => FocusCommand::Left,
key!(Right) => FocusCommand::Right,
key!(Enter) => FocusCommand::Enter,
key!(Esc) => FocusCommand::Exit,
_ => return None
})
}
fn to_transport_command <T> (state: &T, input: &TuiInput) -> Option<TransportCommand>
where
T: TransportControl
{
use ClockCommand::{SetBpm, SetQuant, SetSync};
use TransportCommand::{Focus, Clock};
use KeyCode::{Enter, Left, Right, Char};
Some(match input.event() {
key!(Left) => Focus(FocusCommand::Prev),
key!(Right) => Focus(FocusCommand::Next),
key!(Char(' ')) => todo!("toolbar space"),
key!(Shift-Char(' ')) => todo!("toolbar shift-space"),
_ => match state.transport_focused().unwrap() {
TransportFocus::Bpm => match input.event() {
key!(Char(',')) => Clock(SetBpm(state.bpm().get() - 1.0)),
key!(Char('.')) => Clock(SetBpm(state.bpm().get() + 1.0)),
key!(Char('<')) => Clock(SetBpm(state.bpm().get() - 0.001)),
key!(Char('>')) => Clock(SetBpm(state.bpm().get() + 0.001)),
_ => return None,
},
TransportFocus::Quant => match input.event() {
key!(Char(',')) => Clock(SetQuant(state.quant().prev())),
key!(Char('.')) => Clock(SetQuant(state.quant().next())),
key!(Char('<')) => Clock(SetQuant(state.quant().prev())),
key!(Char('>')) => Clock(SetQuant(state.quant().next())),
_ => return None,
},
TransportFocus::Sync => match input.event() {
key!(Char(',')) => Clock(SetSync(state.sync().prev())),
key!(Char('.')) => Clock(SetSync(state.sync().next())),
key!(Char('<')) => Clock(SetSync(state.sync().prev())),
key!(Char('>')) => Clock(SetSync(state.sync().next())),
_ => return None,
},
TransportFocus::Clock => match input.event() {
key!(Char(',')) => todo!("transport seek bar"),
key!(Char('.')) => todo!("transport seek bar"),
key!(Char('<')) => todo!("transport seek beat"),
key!(Char('>')) => todo!("transport seek beat"),
_ => return None,
},
TransportFocus::PlayPause => match input.event() {
key!(Enter) => todo!("transport play toggle"),
key!(Shift-Enter) => todo!("transport shift-play toggle"),
_ => return None,
},
}
})
}
fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option<SequencerCommand> {
use SequencerCommand::*;
use KeyCode::Char;
if !state.entered() {
return None
}
Some(match input.event() {
key!(Char('e')) => Editor(
PhraseCommand::Show(state.phrase_to_edit().clone())
),
key!(Char(' ')) => Clock(
if let Some(TransportState::Stopped) = *state.clock.playing.read().unwrap() {
ClockCommand::Play(None)
} else {
ClockCommand::Pause(None)
}
),
key!(Shift-Char(' ')) => Clock(
if let Some(TransportState::Stopped) = *state.clock.playing.read().unwrap() {
ClockCommand::Play(Some(0))
} else {
ClockCommand::Pause(Some(0))
}
),
_ => match state.focused() {
AppFocus::Menu => { todo!() },
AppFocus::Content(focused) => match focused {
SequencerFocus::Transport(_) => {
match TransportCommand::input_to_command(state, input)? {
TransportCommand::Clock(_) => { todo!() },
TransportCommand::Focus(command) => Focus(command),
}
},
SequencerFocus::Phrases => Phrases(
PhrasesCommand::input_to_command(state, input)?
),
SequencerFocus::PhraseEditor => Editor(
PhraseCommand::input_to_command(state, input)?
),
}
}
})
}
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(state.phrase_to_edit().clone())),
_ => match state.focused() {
AppFocus::Menu => { todo!() },
AppFocus::Content(focused) => match focused {
ArrangerFocus::Transport(_) => {
use TransportCommand::{Clock, Focus};
match TransportCommand::input_to_command(state, input)? {
Clock(_) => { todo!() },
Focus(command) => Cmd::Focus(command)
}
},
ArrangerFocus::PhraseEditor => {
Cmd::Editor(PhraseCommand::input_to_command(state, input)?)
},
ArrangerFocus::Phrases => {
Cmd::Phrases(PhrasesCommand::input_to_command(state, 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
})
}
impl<T: PhrasesControl> InputToCommand<Tui, T> for PhrasesCommand {
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
use PhraseRenameCommand as Rename;
use PhraseLengthCommand as Length;
use FileBrowserCommand as Browse;
Some(match state.phrases_mode() {
Some(PhrasesMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?),
Some(PhrasesMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?),
Some(PhrasesMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?),
Some(PhrasesMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?),
_ => to_phrases_command(state, input)?
})
}
}
fn to_phrases_command <T: PhrasesControl> (state: &T, input: &TuiInput) -> Option<PhrasesCommand> {
use KeyCode::{Up, Down, Delete, Char};
use PhrasesCommand as Cmd;
use PhrasePoolCommand as Pool;
use PhraseRenameCommand as Rename;
use PhraseLengthCommand as Length;
use FileBrowserCommand as Browse;
let index = state.phrase_index();
let count = state.phrases().len();
Some(match input.event() {
key!(Char('n')) => Cmd::Rename(Rename::Begin),
key!(Char('t')) => Cmd::Length(Length::Begin),
key!(Char('m')) => Cmd::Import(Browse::Begin),
key!(Char('x')) => Cmd::Export(Browse::Begin),
key!(Up) => Cmd::Select(index.overflowing_sub(1).0.min(state.phrases().len() - 1)),
key!(Down) => Cmd::Select(index.saturating_add(1) % state.phrases().len()),
key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())),
key!(Char(',')) => if index > 1 {
state.set_phrase_index(state.phrase_index().saturating_sub(1));
Cmd::Phrase(Pool::Swap(index - 1, index))
} else {
return None
},
key!(Char('.')) => if index < count.saturating_sub(1) {
state.set_phrase_index(state.phrase_index() + 1);
Cmd::Phrase(Pool::Swap(index + 1, index))
} else {
return None
},
key!(Delete) => if index > 0 {
state.set_phrase_index(index.min(count.saturating_sub(1)));
Cmd::Phrase(Pool::Delete(index))
} else {
return None
},
key!(Char('a')) => Cmd::Phrase(Pool::Add(
count, Phrase::new(
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
)
)),
key!(Char('i')) => Cmd::Phrase(Pool::Add(
index + 1, Phrase::new(
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
)
)),
key!(Char('d')) => {
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
Cmd::Phrase(Pool::Add(index + 1, phrase))
},
_ => return None
})
}
impl<T: PhrasesControl> InputToCommand<Tui, T> for FileBrowserCommand {
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
use KeyCode::{Up, Down, Right, Left, Enter, Esc, Char, Backspace};
use FileBrowserCommand::*;
if let Some(PhrasesMode::Import(index, browser)) = state.phrases_mode() {
Some(match from.event() {
key!(Up) => Select(
browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1))
),
key!(Down) => Select(
browser.index.saturating_add(1) % browser.len()
),
key!(Right) => Chdir(browser.cwd.clone()),
key!(Left) => Chdir(browser.cwd.clone()),
key!(Enter) => Confirm,
key!(Char(c)) => { todo!() },
key!(Backspace) => { todo!() },
key!(Esc) => Self::Cancel,
_ => return None
})
} else if let Some(PhrasesMode::Export(index, browser)) = state.phrases_mode() {
Some(match from.event() {
key!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())),
key!(Down) => Select(browser.index.saturating_add(1) % browser.len()),
key!(Right) => Chdir(browser.cwd.clone()),
key!(Left) => Chdir(browser.cwd.clone()),
key!(Enter) => Confirm,
key!(Char(c)) => { todo!() },
key!(Backspace) => { todo!() },
key!(Esc) => Self::Cancel,
_ => return None
})
} else {
unreachable!()
}
}
}
impl<T: PhrasesControl> InputToCommand<Tui, T> for PhraseLengthCommand {
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
use KeyCode::{Up, Down, Right, Left, Enter, Esc};
if let Some(PhrasesMode::Length(_, length, _)) = state.phrases_mode() {
Some(match from.event() {
key!(Up) => Self::Inc,
key!(Down) => Self::Dec,
key!(Right) => Self::Next,
key!(Left) => Self::Prev,
key!(Enter) => Self::Set(*length),
key!(Esc) => Self::Cancel,
_ => return None
})
} else {
unreachable!()
}
}
}
impl<T: PhrasesControl> InputToCommand<Tui, T> for PhraseRenameCommand {
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
use KeyCode::{Char, Backspace, Enter, Esc};
if let Some(PhrasesMode::Rename(_, ref old_name)) = state.phrases_mode() {
Some(match from.event() {
key!(Char(c)) => {
let mut new_name = old_name.clone();
new_name.push(*c);
Self::Set(new_name)
},
key!(Backspace) => {
let mut new_name = old_name.clone();
new_name.pop();
Self::Set(new_name)
},
key!(Enter) => Self::Confirm,
key!(Esc) => Self::Cancel,
_ => return None
})
} else {
unreachable!()
}
}
}
impl<T: PhraseEditorControl + HasEnter> InputToCommand<Tui, T> for PhraseCommand {
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
use PhraseCommand::*;
use KeyCode::{Char, Enter, Esc, Up, Down, PageUp, PageDown, Left, Right};
Some(match from.event() {
key!(Char('`')) => ToggleDirection,
key!(Enter) => EnterEditMode,
key!(Esc) => ExitEditMode,
key!(Char('a')) => NoteAppend,
key!(Char('s')) => NoteSet,
key!(Char('[')) => NoteLengthSet(prev_note_length(state.note_len())),
key!(Char(']')) => NoteLengthSet(next_note_length(state.note_len())),
key!(Char('-')) => TimeZoomSet(next_note_length(state.time_axis().read().unwrap().scale)),
key!(Char('_')) => TimeZoomSet(next_note_length(state.time_axis().read().unwrap().scale)),
key!(Char('=')) => TimeZoomSet(prev_note_length(state.time_axis().read().unwrap().scale)),
key!(Char('+')) => TimeZoomSet(prev_note_length(state.time_axis().read().unwrap().scale)),
key!(Up) => match state.phrase_editor_entered() {
true => NoteCursorSet(state.note_axis().write().unwrap().point_plus(1)),
false => NoteScrollSet(state.note_axis().write().unwrap().start_plus(1)),
},
key!(Down) => match state.phrase_editor_entered() {
true => NoteCursorSet(state.note_axis().write().unwrap().point_minus(1)),
false => NoteScrollSet(state.note_axis().write().unwrap().start_minus(1)),
},
key!(PageUp) => match state.phrase_editor_entered() {
true => NoteCursorSet(state.note_axis().write().unwrap().point_plus(3)),
false => NoteScrollSet(state.note_axis().write().unwrap().start_plus(3)),
},
key!(PageDown) => match state.phrase_editor_entered() {
true => NoteCursorSet(state.note_axis().write().unwrap().point_minus(3)),
false => NoteScrollSet(state.note_axis().write().unwrap().start_minus(3)),
},
key!(Left) => match state.phrase_editor_entered() {
true => TimeCursorSet(state.note_axis().write().unwrap().point_minus(1)),
false => TimeScrollSet(state.note_axis().write().unwrap().start_minus(1)),
},
key!(Right) => match state.phrase_editor_entered() {
true => TimeCursorSet(state.note_axis().write().unwrap().point_plus(1)),
false => TimeScrollSet(state.note_axis().write().unwrap().start_plus(1)),
},
_ => return None
})
}
}

View file

@ -1,91 +1 @@
use crate::*;
impl Audio for TransportTui {
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
ClockAudio(self).process(client, scope)
}
}
impl Audio for SequencerTui {
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
// Start profiling cycle
let t0 = self.perf.get_t0();
// Update transport clock
if ClockAudio(self).process(client, scope) == Control::Quit {
return Control::Quit
}
// Update MIDI sequencer
if PlayerAudio(
&mut self.player,
&mut self.note_buf,
&mut self.midi_buf,
).process(client, scope) == Control::Quit {
return Control::Quit
}
// Update sequencer playhead indicator
//self.now().set(0.);
//if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase {
//let phrase = phrase.read().unwrap();
//if *playing.read().unwrap() == *phrase {
//let pulse = self.current().pulse.get();
//let start = started_at.pulse.get();
//let now = (pulse - start) % phrase.length as f64;
//self.now().set(now);
//}
//}
// End profiling cycle
self.perf.update(t0, scope);
Control::Continue
}
}
impl Audio for ArrangerTui {
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
// Start profiling cycle
let t0 = self.perf.get_t0();
// Update transport clock
if ClockAudio(self).process(client, scope) == Control::Quit {
return Control::Quit
}
// Update MIDI sequencers
if TracksAudio(
&mut self.tracks,
&mut self.note_buf,
&mut self.midi_buf,
Default::default(),
).process(client, scope) == Control::Quit {
return Control::Quit
}
// FIXME: one of these per playing track
self.now().set(0.);
if let ArrangerSelection::Clip(t, s) = self.selected {
let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t));
if let Some(Some(Some(phrase))) = phrase {
if let Some(track) = self.tracks().get(t) {
if let Some((ref started_at, Some(ref playing))) = track.player.play_phrase {
let phrase = phrase.read().unwrap();
if *playing.read().unwrap() == *phrase {
let pulse = self.current().pulse.get();
let start = started_at.pulse.get();
let now = (pulse - start) % phrase.length as f64;
self.now().set(now);
}
}
}
}
}
// End profiling cycle
self.perf.update(t0, scope);
return Control::Continue
}
}

View file

@ -0,0 +1,53 @@
use crate::*;
impl JackApi for ArrangerTui {
fn jack (&self) -> &Arc<RwLock<JackClient>> {
&self.jack
}
}
impl Audio for ArrangerTui {
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
// Start profiling cycle
let t0 = self.perf.get_t0();
// Update transport clock
if ClockAudio(self).process(client, scope) == Control::Quit {
return Control::Quit
}
// Update MIDI sequencers
if TracksAudio(
&mut self.tracks,
&mut self.note_buf,
&mut self.midi_buf,
Default::default(),
).process(client, scope) == Control::Quit {
return Control::Quit
}
// FIXME: one of these per playing track
self.now().set(0.);
if let ArrangerSelection::Clip(t, s) = self.selected {
let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t));
if let Some(Some(Some(phrase))) = phrase {
if let Some(track) = self.tracks().get(t) {
if let Some((ref started_at, Some(ref playing))) = track.player.play_phrase {
let phrase = phrase.read().unwrap();
if *playing.read().unwrap() == *phrase {
let pulse = self.current().pulse.get();
let start = started_at.pulse.get();
let now = (pulse - start) % phrase.length as f64;
self.now().set(now);
}
}
}
}
}
// End profiling cycle
self.perf.update(t0, scope);
return Control::Continue
}
}

View file

@ -0,0 +1,45 @@
use crate::*;
impl JackApi for SequencerTui {
fn jack (&self) -> &Arc<RwLock<JackClient>> {
&self.jack
}
}
impl Audio for SequencerTui {
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
// Start profiling cycle
let t0 = self.perf.get_t0();
// Update transport clock
if ClockAudio(self).process(client, scope) == Control::Quit {
return Control::Quit
}
// Update MIDI sequencer
if PlayerAudio(
&mut self.player,
&mut self.note_buf,
&mut self.midi_buf,
).process(client, scope) == Control::Quit {
return Control::Quit
}
// Update sequencer playhead indicator
//self.now().set(0.);
//if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase {
//let phrase = phrase.read().unwrap();
//if *playing.read().unwrap() == *phrase {
//let pulse = self.current().pulse.get();
//let start = started_at.pulse.get();
//let now = (pulse - start) % phrase.length as f64;
//self.now().set(now);
//}
//}
// End profiling cycle
self.perf.update(t0, scope);
Control::Continue
}
}

View file

@ -0,0 +1,13 @@
use crate::*;
impl JackApi for TransportTui {
fn jack (&self) -> &Arc<RwLock<JackClient>> {
&self.jack
}
}
impl Audio for TransportTui {
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
ClockAudio(self).process(client, scope)
}
}

View file

@ -112,3 +112,72 @@ impl ArrangerTrackApi for ArrangerTrack {
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
}
}
}

View file

@ -35,3 +35,31 @@ impl PhraseLength {
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,
}
}
}

View file

@ -1,70 +0,0 @@
use crate::*;
#[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
}
}
}

View file

@ -1,30 +1 @@
use crate::*;
pub struct TuiTheme;
impl TuiTheme {
pub fn border_bg () -> Color {
Color::Rgb(40, 50, 30)
}
pub fn border_fg (focused: bool) -> Color {
if focused { Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) }
}
pub fn title_fg (focused: bool) -> Color {
if focused { Color::Rgb(150, 160, 90) } else { Color::Rgb(120, 130, 100) }
}
pub fn separator_fg (_: bool) -> Color {
Color::Rgb(0, 0, 0)
}
pub const fn hotkey_fg () -> Color {
Color::Rgb(255, 255, 0)
}
pub fn mode_bg () -> Color {
Color::Rgb(150, 160, 90)
}
pub fn mode_fg () -> Color {
Color::Rgb(255, 255, 255)
}
pub fn status_bar_bg () -> Color {
Color::Rgb(28, 35, 25)
}
}