mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-11 22:26:44 +01:00
separate control layer
This commit is contained in:
parent
416acd9f7b
commit
9319315595
29 changed files with 1647 additions and 1641 deletions
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
68
crates/tek_tui/src/tui_app_arranger.rs
Normal file
68
crates/tek_tui/src/tui_app_arranger.rs
Normal 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,
|
||||
}
|
||||
51
crates/tek_tui/src/tui_app_sequencer.rs
Normal file
51
crates/tek_tui/src/tui_app_sequencer.rs
Normal 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,
|
||||
}
|
||||
56
crates/tek_tui/src/tui_app_transport.rs
Normal file
56
crates/tek_tui/src/tui_app_transport.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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!()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
260
crates/tek_tui/src/tui_control_arranger.rs
Normal file
260
crates/tek_tui/src/tui_control_arranger.rs
Normal 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
|
||||
})
|
||||
}
|
||||
116
crates/tek_tui/src/tui_control_file_browser.rs
Normal file
116
crates/tek_tui/src/tui_control_file_browser.rs
Normal 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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
183
crates/tek_tui/src/tui_control_phrase_editor.rs
Normal file
183
crates/tek_tui/src/tui_control_phrase_editor.rs
Normal 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!()]
|
||||
);
|
||||
47
crates/tek_tui/src/tui_control_phrase_length.rs
Normal file
47
crates/tek_tui/src/tui_control_phrase_length.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
181
crates/tek_tui/src/tui_control_phrase_list.rs
Normal file
181
crates/tek_tui/src/tui_control_phrase_list.rs
Normal 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
|
||||
})
|
||||
}
|
||||
60
crates/tek_tui/src/tui_control_phrase_rename.rs
Normal file
60
crates/tek_tui/src/tui_control_phrase_rename.rs
Normal 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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
88
crates/tek_tui/src/tui_control_sequencer.rs
Normal file
88
crates/tek_tui/src/tui_control_sequencer.rs
Normal 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)?
|
||||
),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
119
crates/tek_tui/src/tui_control_transport.rs
Normal file
119
crates/tek_tui/src/tui_control_transport.rs
Normal 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,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
//}
|
||||
//}
|
||||
|
|
@ -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!()]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
53
crates/tek_tui/src/tui_jack_arranger.rs
Normal file
53
crates/tek_tui/src/tui_jack_arranger.rs
Normal 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
|
||||
}
|
||||
}
|
||||
45
crates/tek_tui/src/tui_jack_sequencer.rs
Normal file
45
crates/tek_tui/src/tui_jack_sequencer.rs
Normal 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
|
||||
}
|
||||
}
|
||||
13
crates/tek_tui/src/tui_jack_transport.rs
Normal file
13
crates/tek_tui/src/tui_jack_transport.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue