mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
wip: refactor pt.9, 403 errors
This commit is contained in:
parent
a784f7a6f2
commit
8aa1ba8d0f
29 changed files with 1008 additions and 902 deletions
|
|
@ -1,5 +1,6 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Arrangement {
|
||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
|
|
@ -12,9 +13,10 @@ pub struct Arrangement {
|
|||
/// Collection of tracks.
|
||||
pub tracks: Vec<ArrangementTrack>,
|
||||
/// Collection of scenes.
|
||||
pub scenes: Vec<Scene>,
|
||||
pub scenes: Vec<ArrangementScene>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArrangementTrack {
|
||||
/// Name of track
|
||||
pub name: Arc<RwLock<String>>,
|
||||
|
|
@ -26,57 +28,93 @@ pub struct ArrangementTrack {
|
|||
pub player: MIDIPlayer
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct ArrangementScene {
|
||||
/// Name of scene
|
||||
pub name: Arc<RwLock<String>>,
|
||||
/// Clips in scene, one per track
|
||||
pub clips: Vec<Option<Arc<RwLock<Phrase>>>>,
|
||||
/// Identifying color of scene
|
||||
pub color: ItemColor,
|
||||
}
|
||||
|
||||
impl Audio for Arrangement {
|
||||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
for track in self.tracks.iter_mut() {
|
||||
track.player.process(client, scope);
|
||||
if track.process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
}
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio for ArrangementTrack {
|
||||
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
self.player.process(client, scope)
|
||||
}
|
||||
}
|
||||
|
||||
impl Arrangement {
|
||||
fn is_stopped (&self) -> bool {
|
||||
pub fn is_stopped (&self) -> bool {
|
||||
*self.clock.playing.read().unwrap() == Some(TransportState::Stopped)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ArrangementCommand {
|
||||
New,
|
||||
Load,
|
||||
Save,
|
||||
ToggleViewMode,
|
||||
Delete,
|
||||
Activate,
|
||||
Increment,
|
||||
Decrement,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
Go(Direction),
|
||||
Edit(Option<Arc<RwLock<Phrase>>>),
|
||||
Scene(SceneCommand),
|
||||
Track(ArrangementTrackCommand),
|
||||
Clip(ArrangementClipCommand),
|
||||
}
|
||||
impl ArrangementScene {
|
||||
/// Returns the pulse length of the longest phrase in the scene
|
||||
pub fn pulses (&self) -> usize {
|
||||
self.clips.iter().fold(0, |a, p|{
|
||||
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ArrangementTrackCommand {
|
||||
Next,
|
||||
Prev,
|
||||
Add,
|
||||
Delete,
|
||||
MoveForward,
|
||||
MoveBack,
|
||||
RandomColor,
|
||||
SetSize(usize),
|
||||
SetZoom(usize),
|
||||
}
|
||||
/// Returns true if all phrases in the scene are
|
||||
/// currently playing on the given collection of tracks.
|
||||
pub fn is_playing (&self, tracks: &[MIDIPlayer]) -> bool {
|
||||
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
|
||||
.all(|(track_index, clip)|match clip {
|
||||
Some(clip) => tracks
|
||||
.get(track_index)
|
||||
.map(|track|if let Some((_, Some(phrase))) = &track.phrase {
|
||||
*phrase.read().unwrap() == *clip.read().unwrap()
|
||||
} else {
|
||||
false
|
||||
})
|
||||
.unwrap_or(false),
|
||||
None => true
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ArrangementClipCommand {
|
||||
Get(usize, usize),
|
||||
Put(usize, usize, Option<Arc<RwLock<Phrase>>>),
|
||||
Edit(Option<Arc<RwLock<Phrase>>>),
|
||||
SetLoop(bool),
|
||||
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<Phrase>>> {
|
||||
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
||||
}
|
||||
|
||||
//TODO
|
||||
//pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
|
||||
//let mut name = None;
|
||||
//let mut clips = vec![];
|
||||
//edn!(edn in args {
|
||||
//Edn::Map(map) => {
|
||||
//let key = map.get(&Edn::Key(":name"));
|
||||
//if let Some(Edn::Str(n)) = key {
|
||||
//name = Some(*n);
|
||||
//} else {
|
||||
//panic!("unexpected key in scene '{name:?}': {key:?}")
|
||||
//}
|
||||
//},
|
||||
//Edn::Symbol("_") => {
|
||||
//clips.push(None);
|
||||
//},
|
||||
//Edn::Int(i) => {
|
||||
//clips.push(Some(*i as usize));
|
||||
//},
|
||||
//_ => panic!("unexpected in scene '{name:?}': {edn:?}")
|
||||
//});
|
||||
//Ok(ArrangementScene {
|
||||
//name: Arc::new(name.unwrap_or("").to_string().into()),
|
||||
//color: ItemColor::random(),
|
||||
//clips,
|
||||
//})
|
||||
//}
|
||||
}
|
||||
|
|
|
|||
94
crates/tek_api/src/arrange_cmd.rs
Normal file
94
crates/tek_api/src/arrange_cmd.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ArrangementCommand {
|
||||
Clear,
|
||||
Export,
|
||||
Import,
|
||||
StopAll,
|
||||
Scene(ArrangementSceneCommand),
|
||||
Track(ArrangementTrackCommand),
|
||||
Clip(ArrangementClipCommand),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ArrangementSceneCommand {
|
||||
Add,
|
||||
Delete,
|
||||
RandomColor,
|
||||
Play,
|
||||
Swap(usize),
|
||||
SetSize(usize),
|
||||
SetZoom(usize),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ArrangementTrackCommand {
|
||||
Add,
|
||||
Delete,
|
||||
RandomColor,
|
||||
Stop,
|
||||
Swap(usize),
|
||||
SetSize(usize),
|
||||
SetZoom(usize),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ArrangementClipCommand {
|
||||
Play,
|
||||
Get(usize, usize),
|
||||
Set(usize, usize, Option<Arc<RwLock<Phrase>>>),
|
||||
Edit(Option<Arc<RwLock<Phrase>>>),
|
||||
SetLoop(bool),
|
||||
RandomColor,
|
||||
}
|
||||
|
||||
impl Command<Arrangement> for ArrangementCommand {
|
||||
fn execute (self, state: &mut Arrangement) -> Perhaps<Self> {
|
||||
match self {
|
||||
Self::Clear => todo!(),
|
||||
Self::Export => todo!(),
|
||||
Self::Import => todo!(),
|
||||
Self::StopAll => todo!(),
|
||||
Self::Scene(command) => { return Ok(command.execute(state)?.map(Self::Scene)) },
|
||||
Self::Track(command) => { return Ok(command.execute(state)?.map(Self::Track)) },
|
||||
Self::Clip(command) => { return Ok(command.execute(state)?.map(Self::Clip)) },
|
||||
}
|
||||
return Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<Arrangement> for ArrangementSceneCommand {
|
||||
fn execute (self, state: &mut Arrangement) -> Perhaps<Self> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<Arrangement> for ArrangementTrackCommand {
|
||||
fn execute (self, state: &mut Arrangement) -> Perhaps<Self> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<Arrangement> for ArrangementClipCommand {
|
||||
fn execute (self, state: &mut Arrangement) -> Perhaps<Self> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
//impl Command<Arrangement> for ArrangementSceneCommand {
|
||||
//}
|
||||
//Edit(phrase) => { state.state.phrase = phrase.clone() },
|
||||
//ToggleViewMode => { state.state.mode.to_next(); },
|
||||
//Delete => { state.state.delete(); },
|
||||
//Activate => { state.state.activate(); },
|
||||
//ZoomIn => { state.state.zoom_in(); },
|
||||
//ZoomOut => { state.state.zoom_out(); },
|
||||
//MoveBack => { state.state.move_back(); },
|
||||
//MoveForward => { state.state.move_forward(); },
|
||||
//RandomColor => { state.state.randomize_color(); },
|
||||
//Put => { state.state.phrase_put(); },
|
||||
//Get => { state.state.phrase_get(); },
|
||||
//AddScene => { state.state.scene_add(None, None)?; },
|
||||
//AddTrack => { state.state.track_add(None, None)?; },
|
||||
//ToggleLoop => { state.state.toggle_loop() },
|
||||
|
|
@ -10,21 +10,31 @@ pub(crate) use tek_core::jack::{
|
|||
|
||||
submod! {
|
||||
api_jack
|
||||
|
||||
arrange
|
||||
arrange_cmd
|
||||
|
||||
clock
|
||||
|
||||
mixer
|
||||
mixer_track
|
||||
|
||||
phrase
|
||||
|
||||
plugin
|
||||
plugin_kind
|
||||
plugin_lv2
|
||||
|
||||
pool
|
||||
sample
|
||||
|
||||
sampler
|
||||
scene
|
||||
scene_cmd
|
||||
sampler_sample
|
||||
sampler_voice
|
||||
|
||||
sequencer
|
||||
track
|
||||
|
||||
status
|
||||
|
||||
transport
|
||||
transport_cmd
|
||||
voice
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,16 +14,6 @@ pub struct MixerTrack {
|
|||
pub devices: Vec<Box<dyn MixerTrackDevice>>,
|
||||
}
|
||||
|
||||
pub trait MixerTrackDevice: Audio + Debug {
|
||||
fn boxed (self) -> Box<dyn MixerTrackDevice> where Self: Sized + 'static {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl MixerTrackDevice for Sampler {}
|
||||
|
||||
impl MixerTrackDevice for Plugin {}
|
||||
|
||||
//impl MixerTrackDevice for LV2Plugin {}
|
||||
|
||||
impl MixerTrack {
|
||||
|
|
@ -81,3 +71,13 @@ impl MixerTrack {
|
|||
Ok(track)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MixerTrackDevice: Audio + Debug {
|
||||
fn boxed (self) -> Box<dyn MixerTrackDevice> where Self: Sized + 'static {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl MixerTrackDevice for Sampler {}
|
||||
|
||||
impl MixerTrackDevice for Plugin {}
|
||||
|
|
@ -4,46 +4,18 @@ use crate::*;
|
|||
pub struct PhrasePool {
|
||||
/// Phrases in the pool
|
||||
pub phrases: Vec<Arc<RwLock<Phrase>>>,
|
||||
/// Highlighted phrase
|
||||
pub phrase: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum PhrasePoolCommand {
|
||||
Prev,
|
||||
Next,
|
||||
MoveUp,
|
||||
MoveDown,
|
||||
Delete,
|
||||
Append,
|
||||
Insert,
|
||||
Duplicate,
|
||||
RandomColor,
|
||||
Edit,
|
||||
Import,
|
||||
Export,
|
||||
Rename(PhraseRenameCommand),
|
||||
Length(PhraseLengthCommand),
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum PhraseRenameCommand {
|
||||
Begin,
|
||||
Backspace,
|
||||
Append(char),
|
||||
Set(String),
|
||||
Confirm,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum PhraseLengthCommand {
|
||||
Begin,
|
||||
Next,
|
||||
Prev,
|
||||
Inc,
|
||||
Dec,
|
||||
Set(usize),
|
||||
Confirm,
|
||||
Cancel,
|
||||
Select(usize),
|
||||
Add(usize),
|
||||
Delete(usize),
|
||||
Duplicate(usize),
|
||||
Swap(usize),
|
||||
RandomColor(usize),
|
||||
Import(String),
|
||||
Export(String),
|
||||
SetName(String),
|
||||
SetLength(String),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Scene {
|
||||
/// Name of scene
|
||||
pub name: Arc<RwLock<String>>,
|
||||
/// Clips in scene, one per track
|
||||
pub clips: Vec<Option<Arc<RwLock<Phrase>>>>,
|
||||
/// Identifying color of scene
|
||||
pub color: ItemColor,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
//TODO
|
||||
//pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
|
||||
//let mut name = None;
|
||||
//let mut clips = vec![];
|
||||
//edn!(edn in args {
|
||||
//Edn::Map(map) => {
|
||||
//let key = map.get(&Edn::Key(":name"));
|
||||
//if let Some(Edn::Str(n)) = key {
|
||||
//name = Some(*n);
|
||||
//} else {
|
||||
//panic!("unexpected key in scene '{name:?}': {key:?}")
|
||||
//}
|
||||
//},
|
||||
//Edn::Symbol("_") => {
|
||||
//clips.push(None);
|
||||
//},
|
||||
//Edn::Int(i) => {
|
||||
//clips.push(Some(*i as usize));
|
||||
//},
|
||||
//_ => panic!("unexpected in scene '{name:?}': {edn:?}")
|
||||
//});
|
||||
//Ok(Scene {
|
||||
//name: Arc::new(name.unwrap_or("").to_string().into()),
|
||||
//color: ItemColor::random(),
|
||||
//clips,
|
||||
//})
|
||||
//}
|
||||
|
||||
/// Returns the pulse length of the longest phrase in the scene
|
||||
pub fn pulses (&self) -> usize {
|
||||
self.clips.iter().fold(0, |a, p|{
|
||||
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if all phrases in the scene are
|
||||
/// currently playing on the given collection of tracks.
|
||||
pub fn is_playing (&self, tracks: &[MIDIPlayer]) -> bool {
|
||||
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
|
||||
.all(|(track_index, clip)|match clip {
|
||||
Some(clip) => tracks
|
||||
.get(track_index)
|
||||
.map(|track|if let Some((_, Some(phrase))) = &track.phrase {
|
||||
*phrase.read().unwrap() == *clip.read().unwrap()
|
||||
} else {
|
||||
false
|
||||
})
|
||||
.unwrap_or(false),
|
||||
None => true
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<Phrase>>> {
|
||||
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum SceneCommand {
|
||||
Next,
|
||||
Prev,
|
||||
Add,
|
||||
Delete,
|
||||
MoveForward,
|
||||
MoveBack,
|
||||
RandomColor,
|
||||
SetSize(usize),
|
||||
SetZoom(usize),
|
||||
}
|
||||
19
crates/tek_api/src/status.rs
Normal file
19
crates/tek_api/src/status.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait StatusBar<E: Engine>: Widget<Engine = E> {
|
||||
fn hotkey_fg () -> Color;
|
||||
|
||||
fn command (commands: &[[impl Widget<Engine = Tui>;3]]) -> impl Widget<Engine = Tui> + '_ {
|
||||
let hotkey_fg = Self::hotkey_fg();
|
||||
Stack::right(move |add|{
|
||||
Ok(for [a, b, c] in commands.iter() {
|
||||
add(&row!(
|
||||
" ",
|
||||
widget(a),
|
||||
widget(b).bold(true).fg(hotkey_fg),
|
||||
widget(c),
|
||||
))?;
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,11 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum NextPrev {
|
||||
Next,
|
||||
Prev,
|
||||
}
|
||||
|
||||
pub trait Command<S>: Sized {
|
||||
fn translate (self, _: &S) -> Self { self }
|
||||
fn execute (self, state: &mut S) -> Perhaps<Self>;
|
||||
|
|
|
|||
|
|
@ -11,32 +11,46 @@ pub(crate) use std::fs::read_dir;
|
|||
|
||||
submod! {
|
||||
tui_app
|
||||
|
||||
tui_arrangement
|
||||
tui_arrangement_cmd
|
||||
tui_arrangement_foc
|
||||
|
||||
tui_arranger
|
||||
tui_arranger_bar
|
||||
tui_arranger_cmd
|
||||
tui_arranger_col
|
||||
tui_arranger_foc
|
||||
tui_arranger_hor
|
||||
tui_arranger_ver
|
||||
|
||||
tui_mixer
|
||||
tui_mixer_cmd
|
||||
|
||||
tui_phrase
|
||||
tui_phrase_cmd
|
||||
|
||||
tui_plugin
|
||||
tui_plugin_cmd
|
||||
tui_plugin_lv2
|
||||
tui_plugin_lv2_gui
|
||||
tui_plugin_vst2
|
||||
tui_plugin_vst3
|
||||
|
||||
tui_pool
|
||||
tui_pool_cmd
|
||||
tui_pool_length
|
||||
tui_pool_rename
|
||||
|
||||
tui_sampler
|
||||
tui_sampler_cmd
|
||||
|
||||
tui_sequencer
|
||||
tui_sequencer_bar
|
||||
tui_sequencer_cmd
|
||||
tui_sequencer_foc
|
||||
|
||||
tui_theme
|
||||
|
||||
tui_transport
|
||||
tui_transport_bar
|
||||
tui_transport_cmd
|
||||
|
|
|
|||
|
|
@ -1,39 +1,44 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct App<E, T, C, S>
|
||||
pub struct App<T, E, C, U, A, S>
|
||||
where
|
||||
E: Engine,
|
||||
T: Widget<Engine = E> + Handle<E> + Audio,
|
||||
C: Command<T>,
|
||||
S: Widget<Engine = E>
|
||||
U: From<Arc<RwLock<T>>> + Widget<Engine = E> + Handle<E>,
|
||||
A: From<Arc<RwLock<T>>> + Audio,
|
||||
S: From<Arc<RwLock<T>>> + StatusBar<E>
|
||||
{
|
||||
state: T,
|
||||
cursor: (usize, usize),
|
||||
entered: bool,
|
||||
menu_bar: Option<MenuBar<E, T, C>>,
|
||||
status_bar: Option<S>
|
||||
status_bar: Option<S>,
|
||||
history: Vec<C>,
|
||||
size: Measure<E>,
|
||||
ui: U,
|
||||
audio: A,
|
||||
model: Arc<RwLock<T>>,
|
||||
}
|
||||
|
||||
impl App<Tui, TransportView<Tui>, TransportViewCommand, TransportStatusBar> {
|
||||
fn new () -> Self {
|
||||
impl<T, E, C, U, A, S> From<T> for App<T, E, C, U, A, S>
|
||||
where
|
||||
E: Engine,
|
||||
C: Command<T>,
|
||||
U: From<Arc<RwLock<T>>> + Widget<Engine = E> + Handle<E>,
|
||||
A: From<Arc<RwLock<T>>> + Audio,
|
||||
S: From<Arc<RwLock<T>>> + Widget<Engine = E>
|
||||
{
|
||||
fn from (model: T) -> Self {
|
||||
let model = Arc::new(RwLock::new(model));
|
||||
Self {
|
||||
state: TransportView {
|
||||
_engine: Default::default(),
|
||||
state: Transport {
|
||||
jack: Default::default(),
|
||||
transport: Default::default(),
|
||||
clock: Default::default(),
|
||||
metronome: false
|
||||
}
|
||||
},
|
||||
cursor: (0, 0),
|
||||
entered: false,
|
||||
menu_bar: None,
|
||||
status_bar: None,
|
||||
history: vec![],
|
||||
size: Default::default(),
|
||||
cursor: (0, 0),
|
||||
entered: false,
|
||||
menu_bar: None,
|
||||
status_bar: None,
|
||||
history: vec![],
|
||||
size: (0, 0).into(),
|
||||
ui: U::from(model.clone()),
|
||||
audio: A::from(model.clone()),
|
||||
model,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,149 +1,61 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct ArrangementEditor<E: Engine> {
|
||||
/// Global JACK client
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
/// Global timebase
|
||||
pub clock: Arc<Clock>,
|
||||
/// Name of arranger
|
||||
pub name: Arc<RwLock<String>>,
|
||||
/// Collection of phrases.
|
||||
pub phrases: Arc<RwLock<PhrasePool<E>>>,
|
||||
/// Collection of tracks.
|
||||
pub tracks: Vec<ArrangementTrack>,
|
||||
/// Collection of scenes.
|
||||
pub scenes: Vec<Scene>,
|
||||
pub state: Arrangement,
|
||||
/// Currently selected element.
|
||||
pub selected: ArrangementEditorFocus,
|
||||
/// Display mode of arranger
|
||||
pub mode: ArrangementViewMode,
|
||||
/// Whether the arranger is currently focused
|
||||
pub focused: bool,
|
||||
pub mode: ArrangementEditorMode,
|
||||
/// Background color of arrangement
|
||||
pub color: ItemColor,
|
||||
/// Width and height of arrangement area at last render
|
||||
pub size: Measure<E>,
|
||||
/// Whether the arranger is currently focused
|
||||
pub focused: bool,
|
||||
/// Whether this is currently in edit mode
|
||||
pub entered: bool,
|
||||
}
|
||||
|
||||
/// Display mode of arranger
|
||||
#[derive(PartialEq)]
|
||||
pub enum ArrangementViewMode {
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum ArrangementEditorMode {
|
||||
/// Tracks are rows
|
||||
Horizontal,
|
||||
/// Tracks are columns
|
||||
Vertical(usize),
|
||||
}
|
||||
|
||||
impl<E: Engine> Audio for ArrangementEditor<E> {
|
||||
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
self.state.process(client, scope)
|
||||
}
|
||||
}
|
||||
|
||||
impl Content for ArrangementEditor<Tui> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
Layers::new(move |add|{
|
||||
match self.mode {
|
||||
ArrangementViewMode::Horizontal => { add(&HorizontalArranger(&self)) },
|
||||
ArrangementViewMode::Vertical(factor) => { add(&VerticalArranger(&self, factor)) },
|
||||
}?;
|
||||
ArrangementEditorMode::Horizontal =>
|
||||
add(&HorizontalArranger(&self))?,
|
||||
ArrangementEditorMode::Vertical(factor) =>
|
||||
add(&VerticalArranger(&self, factor))?
|
||||
};
|
||||
add(&self.size)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
/// Represents the current user selection in the arranger
|
||||
pub enum ArrangementEditorFocus {
|
||||
/// 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 ArrangementEditorFocus {
|
||||
pub fn description <E: Engine> (
|
||||
&self,
|
||||
tracks: &Vec<ArrangementTrack>,
|
||||
scenes: &Vec<Scene>,
|
||||
) -> 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> {
|
||||
match self { Self::Clip(t, _) => Some(*t), Self::Track(t) => Some(*t), _ => None }
|
||||
}
|
||||
pub fn track_next (&mut self, last_track: usize) {
|
||||
*self = match self {
|
||||
Self::Mix =>
|
||||
Self::Track(0),
|
||||
Self::Track(t) =>
|
||||
Self::Track(last_track.min(*t + 1)),
|
||||
Self::Scene(s) =>
|
||||
Self::Clip(0, *s),
|
||||
Self::Clip(t, s) =>
|
||||
Self::Clip(last_track.min(*t + 1), *s),
|
||||
}
|
||||
}
|
||||
pub fn track_prev (&mut self) {
|
||||
*self = match self {
|
||||
Self::Mix =>
|
||||
Self::Mix,
|
||||
Self::Scene(s) =>
|
||||
Self::Scene(*s),
|
||||
Self::Track(t) =>
|
||||
if *t == 0 { Self::Mix } else { Self::Track(*t - 1) },
|
||||
Self::Clip(t, s) =>
|
||||
if *t == 0 { Self::Scene(*s) } else { Self::Clip(t.saturating_sub(1), *s) }
|
||||
}
|
||||
}
|
||||
pub fn scene (&self) -> Option<usize> {
|
||||
match self { Self::Clip(_, s) => Some(*s), Self::Scene(s) => Some(*s), _ => None }
|
||||
}
|
||||
pub fn scene_next (&mut self, last_scene: usize) {
|
||||
*self = match self {
|
||||
Self::Mix =>
|
||||
Self::Scene(0),
|
||||
Self::Track(t) =>
|
||||
Self::Clip(*t, 0),
|
||||
Self::Scene(s) =>
|
||||
Self::Scene(last_scene.min(*s + 1)),
|
||||
Self::Clip(t, s) =>
|
||||
Self::Clip(*t, last_scene.min(*s + 1)),
|
||||
}
|
||||
}
|
||||
pub fn scene_prev (&mut self) {
|
||||
*self = match self {
|
||||
Self::Mix =>
|
||||
Self::Mix,
|
||||
Self::Track(t) =>
|
||||
Self::Track(*t),
|
||||
Self::Scene(s) =>
|
||||
if *s == 0 { Self::Mix } else { Self::Scene(*s - 1) },
|
||||
Self::Clip(t, s) =>
|
||||
if *s == 0 { Self::Track(*t) } else { Self::Clip(*t, s.saturating_sub(1)) }
|
||||
impl<E: Engine> ArrangementEditor<E> {
|
||||
pub fn new (state: Arrangement) -> Self {
|
||||
Self {
|
||||
state,
|
||||
selected: ArrangementEditorFocus::Clip(0, 0),
|
||||
mode: ArrangementEditorMode::Vertical(2),
|
||||
color: Color::Rgb(28, 35, 25).into(),
|
||||
size: Measure::new(),
|
||||
focused: false,
|
||||
entered: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
139
crates/tek_tui/src/tui_arrangement_cmd.rs
Normal file
139
crates/tek_tui/src/tui_arrangement_cmd.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ArrangementEditorCommand {
|
||||
Edit(ArrangementCommand),
|
||||
Select(ArrangementEditorFocus),
|
||||
Zoom(usize),
|
||||
}
|
||||
|
||||
/// Handle events for arrangement.
|
||||
impl Handle<Tui> for ArrangementEditor<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
ArrangementEditorCommand::execute_with_state(self, from)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, ArrangementEditor<Tui>> for ArrangementEditorCommand {
|
||||
fn input_to_command (state: &ArrangementEditor<Tui>, input: &TuiInput) -> Option<Self> {
|
||||
use ArrangementEditorCommand as Cmd;
|
||||
use ArrangementCommand as Edit;
|
||||
use ArrangementEditorFocus as Focus;
|
||||
use ArrangementTrackCommand as Track;
|
||||
use ArrangementClipCommand as Clip;
|
||||
use ArrangementSceneCommand as Scene;
|
||||
Some(match input.event() {
|
||||
// FIXME: boundary conditions
|
||||
|
||||
key!(KeyCode::Up) => match state.selected {
|
||||
ArrangementEditorFocus::Mix => return None,
|
||||
ArrangementEditorFocus::Track(t) => return None,
|
||||
ArrangementEditorFocus::Scene(s) => Cmd::Select(Focus::Scene(s - 1)),
|
||||
ArrangementEditorFocus::Clip(t, s) => Cmd::Select(Focus::Clip(t, s - 1)),
|
||||
},
|
||||
|
||||
key!(KeyCode::Down) => match state.selected {
|
||||
ArrangementEditorFocus::Mix => Cmd::Select(Focus::Scene(0)),
|
||||
ArrangementEditorFocus::Track(t) => Cmd::Select(Focus::Clip(t, 0)),
|
||||
ArrangementEditorFocus::Scene(s) => Cmd::Select(Focus::Scene(s + 1)),
|
||||
ArrangementEditorFocus::Clip(t, s) => Cmd::Select(Focus::Clip(t, s + 1)),
|
||||
},
|
||||
|
||||
key!(KeyCode::Left) => match state.selected {
|
||||
ArrangementEditorFocus::Mix => return None,
|
||||
ArrangementEditorFocus::Track(t) => Cmd::Select(Focus::Track(t - 1)),
|
||||
ArrangementEditorFocus::Scene(s) => return None,
|
||||
ArrangementEditorFocus::Clip(t, s) => Cmd::Select(Focus::Clip(t - 1, s)),
|
||||
},
|
||||
|
||||
key!(KeyCode::Right) => match state.selected {
|
||||
ArrangementEditorFocus::Mix => return None,
|
||||
ArrangementEditorFocus::Track(t) => Cmd::Select(Focus::Track(t + 1)),
|
||||
ArrangementEditorFocus::Scene(s) => Cmd::Select(Focus::Clip(0, s)),
|
||||
ArrangementEditorFocus::Clip(t, s) => Cmd::Select(Focus::Clip(t, s - 1)),
|
||||
},
|
||||
|
||||
key!(KeyCode::Char('+')) => Cmd::Zoom(0),
|
||||
|
||||
key!(KeyCode::Char('=')) => Cmd::Zoom(0),
|
||||
|
||||
key!(KeyCode::Char('_')) => Cmd::Zoom(0),
|
||||
|
||||
key!(KeyCode::Char('-')) => Cmd::Zoom(0),
|
||||
|
||||
key!(KeyCode::Char('`')) => { todo!("toggle view mode") },
|
||||
|
||||
key!(KeyCode::Char(',')) => match state.selected {
|
||||
ArrangementEditorFocus::Mix => Cmd::Zoom(0),
|
||||
ArrangementEditorFocus::Track(t) => Cmd::Edit(Edit::Track(Track::Swap(0))),
|
||||
ArrangementEditorFocus::Scene(s) => Cmd::Edit(Edit::Scene(Scene::Swap(0))),
|
||||
ArrangementEditorFocus::Clip(t, s) => Cmd::Edit(Edit::Clip(Clip::Set(t, s, None))),
|
||||
},
|
||||
|
||||
key!(KeyCode::Char('.')) => match state.selected {
|
||||
ArrangementEditorFocus::Mix => Cmd::Zoom(0),
|
||||
ArrangementEditorFocus::Track(t) => Cmd::Edit(Edit::Track(Track::Swap(0))),
|
||||
ArrangementEditorFocus::Scene(s) => Cmd::Edit(Edit::Scene(Scene::Swap(0))),
|
||||
ArrangementEditorFocus::Clip(t, s) => Cmd::Edit(Edit::Clip(Clip::Set(t, s, None))),
|
||||
},
|
||||
|
||||
key!(KeyCode::Char('<')) => match state.selected {
|
||||
ArrangementEditorFocus::Mix => Cmd::Zoom(0),
|
||||
ArrangementEditorFocus::Track(t) => Cmd::Edit(Edit::Track(Track::Swap(0))),
|
||||
ArrangementEditorFocus::Scene(s) => Cmd::Edit(Edit::Scene(Scene::Swap(0))),
|
||||
ArrangementEditorFocus::Clip(t, s) => Cmd::Edit(Edit::Clip(Clip::Set(t, s, None))),
|
||||
},
|
||||
|
||||
key!(KeyCode::Char('>')) => match state.selected {
|
||||
ArrangementEditorFocus::Mix => Cmd::Zoom(0),
|
||||
ArrangementEditorFocus::Track(t) => Cmd::Edit(Edit::Track(Track::Swap(0))),
|
||||
ArrangementEditorFocus::Scene(s) => Cmd::Edit(Edit::Scene(Scene::Swap(0))),
|
||||
ArrangementEditorFocus::Clip(t, s) => Cmd::Edit(Edit::Clip(Clip::Set(t, s, None))),
|
||||
},
|
||||
|
||||
key!(KeyCode::Enter) => match state.selected {
|
||||
ArrangementEditorFocus::Mix => return None,
|
||||
ArrangementEditorFocus::Track(t) => return None,
|
||||
ArrangementEditorFocus::Scene(s) => return None,
|
||||
ArrangementEditorFocus::Clip(t, s) => return None,
|
||||
},
|
||||
|
||||
key!(KeyCode::Delete) => match state.selected {
|
||||
ArrangementEditorFocus::Mix => Cmd::Edit(Edit::Clear),
|
||||
ArrangementEditorFocus::Track(t) => Cmd::Edit(Edit::Track(Track::Delete(t))),
|
||||
ArrangementEditorFocus::Scene(s) => Cmd::Edit(Edit::Scene(Scene::Delete(s))),
|
||||
ArrangementEditorFocus::Clip(t, s) => Cmd::Edit(Edit::Clip(Clip::Set(t, s, None))),
|
||||
},
|
||||
|
||||
key!(KeyCode::Char('c')) => Cmd::Edit(Edit::Clip(Clip::RandomColor)),
|
||||
|
||||
key!(KeyCode::Char('s')) => match state.selected {
|
||||
ArrangementEditorFocus::Clip(t, s) => Cmd::Edit(Edit::Clip(Clip::Set(t, s, None))),
|
||||
_ => return None,
|
||||
},
|
||||
|
||||
key!(KeyCode::Char('g')) => match state.selected {
|
||||
ArrangementEditorFocus::Clip(t, s) => Cmd::Edit(Edit::Clip(Clip::Get(t, s))),
|
||||
_ => return None,
|
||||
},
|
||||
|
||||
key!(Ctrl-KeyCode::Char('a')) => Cmd::Edit(Edit::Scene(Scene::Add)),
|
||||
|
||||
key!(Ctrl-KeyCode::Char('t')) => Cmd::Edit(Edit::Track(Track::Add)),
|
||||
|
||||
key!(KeyCode::Char('l')) => Cmd::Edit(Edit::Clip(Clip::SetLoop(false))),
|
||||
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Command<ArrangementEditor<E>> for ArrangementEditorCommand {
|
||||
fn execute (self, state: &mut ArrangementEditor<E>) -> Perhaps<Self> {
|
||||
match self {
|
||||
Self::Zoom(zoom) => { todo!(); },
|
||||
Self::Select(selected) => { state.selected = selected; },
|
||||
Self::Edit(command) => return command.execute(state.state).map(Self::Edit),
|
||||
}
|
||||
}
|
||||
}
|
||||
100
crates/tek_tui/src/tui_arrangement_foc.rs
Normal file
100
crates/tek_tui/src/tui_arrangement_foc.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
/// Represents the current user selection in the arranger
|
||||
pub enum ArrangementEditorFocus {
|
||||
/// 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 ArrangementEditorFocus {
|
||||
pub fn description <E: Engine> (
|
||||
&self,
|
||||
tracks: &Vec<ArrangementTrack>,
|
||||
scenes: &Vec<ArrangementScene>,
|
||||
) -> 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> {
|
||||
match self { Self::Clip(t, _) => Some(*t), Self::Track(t) => Some(*t), _ => None }
|
||||
}
|
||||
pub fn track_next (&mut self, last_track: usize) {
|
||||
*self = match self {
|
||||
Self::Mix =>
|
||||
Self::Track(0),
|
||||
Self::Track(t) =>
|
||||
Self::Track(last_track.min(*t + 1)),
|
||||
Self::Scene(s) =>
|
||||
Self::Clip(0, *s),
|
||||
Self::Clip(t, s) =>
|
||||
Self::Clip(last_track.min(*t + 1), *s),
|
||||
}
|
||||
}
|
||||
pub fn track_prev (&mut self) {
|
||||
*self = match self {
|
||||
Self::Mix =>
|
||||
Self::Mix,
|
||||
Self::Scene(s) =>
|
||||
Self::Scene(*s),
|
||||
Self::Track(t) =>
|
||||
if *t == 0 { Self::Mix } else { Self::Track(*t - 1) },
|
||||
Self::Clip(t, s) =>
|
||||
if *t == 0 { Self::Scene(*s) } else { Self::Clip(t.saturating_sub(1), *s) }
|
||||
}
|
||||
}
|
||||
pub fn scene (&self) -> Option<usize> {
|
||||
match self { Self::Clip(_, s) => Some(*s), Self::Scene(s) => Some(*s), _ => None }
|
||||
}
|
||||
pub fn scene_next (&mut self, last_scene: usize) {
|
||||
*self = match self {
|
||||
Self::Mix =>
|
||||
Self::Scene(0),
|
||||
Self::Track(t) =>
|
||||
Self::Clip(*t, 0),
|
||||
Self::Scene(s) =>
|
||||
Self::Scene(last_scene.min(*s + 1)),
|
||||
Self::Clip(t, s) =>
|
||||
Self::Clip(*t, last_scene.min(*s + 1)),
|
||||
}
|
||||
}
|
||||
pub fn scene_prev (&mut self) {
|
||||
*self = match self {
|
||||
Self::Mix =>
|
||||
Self::Mix,
|
||||
Self::Track(t) =>
|
||||
Self::Track(*t),
|
||||
Self::Scene(s) =>
|
||||
if *s == 0 { Self::Mix } else { Self::Scene(*s - 1) },
|
||||
Self::Clip(t, s) =>
|
||||
if *s == 0 { Self::Track(*t) } else { Self::Clip(*t, s.saturating_sub(1)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,49 +6,39 @@ pub struct ArrangerView<E: Engine> {
|
|||
pub sequencer: SequencerView<E>,
|
||||
/// Contains all the sequencers.
|
||||
pub arrangement: ArrangementEditor<E>,
|
||||
/// Status bar
|
||||
pub status: ArrangerStatusBar,
|
||||
/// Height of arrangement
|
||||
pub split: u16,
|
||||
/// Width and height of app at last render
|
||||
pub size: Measure<E>,
|
||||
/// Menu bar
|
||||
pub menu: MenuBar<E, Self, ArrangerViewCommand>,
|
||||
/// Command history
|
||||
pub history: Vec<ArrangerViewCommand>,
|
||||
/// Which view is focused
|
||||
pub cursor: (usize, usize),
|
||||
/// Whether the focused view is entered
|
||||
pub entered: bool,
|
||||
}
|
||||
|
||||
impl<E: Engine> Audio for ArrangerView<E> {
|
||||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
if let Some(ref transport) = self.transport {
|
||||
transport.write().unwrap().process(client, scope);
|
||||
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
if self.sequencer.transport.process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
let Arrangement { scenes, ref mut tracks, selected, .. } = &mut self.arrangement;
|
||||
for track in tracks.iter_mut() {
|
||||
track.player.process(client, scope);
|
||||
if self.arrangement.process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
if let ArrangementEditorFocus::Clip(t, s) = selected {
|
||||
if let Some(Some(Some(phrase))) = scenes.get(*s).map(|scene|scene.clips.get(*t)) {
|
||||
if let Some(track) = tracks.get(*t) {
|
||||
if let ArrangementEditorFocus::Clip(t, s) = self.arrangement.selected {
|
||||
let phrase = self.arrangement.state.scenes.get(s).map(|scene|scene.clips.get(t));
|
||||
if let Some(Some(Some(phrase))) = phrase {
|
||||
if let Some(track) = self.arrangement.state.tracks.get(t) {
|
||||
if let Some((ref started_at, Some(ref playing))) = track.player.phrase {
|
||||
let phrase = phrase.read().unwrap();
|
||||
if *playing.read().unwrap() == *phrase {
|
||||
let pulse = self.clock.current.pulse.get();
|
||||
let start = started_at.pulse.get();
|
||||
let now = (pulse - start) % phrase.length as f64;
|
||||
self.editor.now.set(now);
|
||||
self.sequencer.editor.now.set(now);
|
||||
return Control::Continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.editor.now.set(0.);
|
||||
Control::Continue
|
||||
self.sequencer.editor.now.set(0.);
|
||||
self.state.process(client, scope)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,9 +47,9 @@ impl Content for ArrangerView<Tui> {
|
|||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let focused = self.arrangement.focused;
|
||||
let border_bg = Arranger::<Tui>::border_bg();
|
||||
let border_fg = Arranger::<Tui>::border_fg(focused);
|
||||
let title_fg = Arranger::<Tui>::title_fg(focused);
|
||||
let border_bg = TuiTheme::border_bg();
|
||||
let border_fg = TuiTheme::border_fg(focused);
|
||||
let title_fg = TuiTheme::title_fg(focused);
|
||||
let border = Lozenge(Style::default().bg(border_bg).fg(border_fg));
|
||||
let entered = if self.arrangement.entered { "■" } else { " " };
|
||||
Split::down(
|
||||
|
|
@ -95,109 +85,14 @@ impl Content for ArrangerView<Tui> {
|
|||
/// General methods for arranger
|
||||
impl<E: Engine> ArrangerView<E> {
|
||||
pub fn new (
|
||||
jack: &Arc<RwLock<JackClient>>,
|
||||
transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
|
||||
arrangement: Arrangement<E>,
|
||||
phrases: Arc<RwLock<PhrasePool<E>>>,
|
||||
sequencer: SequencerView<E>,
|
||||
arrangement: ArrangementEditor<E>,
|
||||
) -> Self {
|
||||
let mut app = Self {
|
||||
jack: jack.clone(),
|
||||
focus_cursor: (0, 1),
|
||||
entered: false,
|
||||
phrases_split: 20,
|
||||
arrangement_split: 15,
|
||||
editor: PhraseEditor::new(),
|
||||
status: ArrangerStatusBar::ArrangementClip,
|
||||
transport: transport.clone(),
|
||||
arrangement,
|
||||
phrases,
|
||||
history: vec![],
|
||||
size: Measure::new(),
|
||||
clock: if let Some(ref transport) = transport {
|
||||
transport.read().unwrap().clock.clone()
|
||||
} else {
|
||||
Arc::new(Clock::default())
|
||||
},
|
||||
menu: {
|
||||
use ArrangerViewCommand::*;
|
||||
MenuBar::new()
|
||||
.add({
|
||||
use ArrangementCommand::*;
|
||||
Menu::new("File")
|
||||
.cmd("n", "New project", Arrangement(New))
|
||||
.cmd("l", "Load project", Arrangement(Load))
|
||||
.cmd("s", "Save project", Arrangement(Save))
|
||||
})
|
||||
.add({
|
||||
use TransportViewCommand::*;
|
||||
Menu::new("Transport")
|
||||
.cmd("p", "Play", Transport(Play))
|
||||
.cmd("s", "Play from start", Transport(PlayFromStart))
|
||||
.cmd("a", "Pause", Transport(Pause))
|
||||
})
|
||||
.add({
|
||||
use ArrangementCommand::*;
|
||||
Menu::new("Track")
|
||||
.cmd("a", "Append new", Arrangement(AddTrack))
|
||||
.cmd("i", "Insert new", Arrangement(AddTrack))
|
||||
.cmd("n", "Rename", Arrangement(AddTrack))
|
||||
.cmd("d", "Delete", Arrangement(AddTrack))
|
||||
.cmd(">", "Move up", Arrangement(AddTrack))
|
||||
.cmd("<", "Move down", Arrangement(AddTrack))
|
||||
})
|
||||
.add({
|
||||
use ArrangementCommand::*;
|
||||
Menu::new("Scene")
|
||||
.cmd("a", "Append new", Arrangement(AddScene))
|
||||
.cmd("i", "Insert new", Arrangement(AddTrack))
|
||||
.cmd("n", "Rename", Arrangement(AddTrack))
|
||||
.cmd("d", "Delete", Arrangement(AddTrack))
|
||||
.cmd(">", "Move up", Arrangement(AddTrack))
|
||||
.cmd("<", "Move down", Arrangement(AddTrack))
|
||||
})
|
||||
.add({
|
||||
use PhrasePoolCommand::*;
|
||||
use PhraseRenameCommand as Rename;
|
||||
use PhraseLengthCommand as Length;
|
||||
Menu::new("Phrase")
|
||||
.cmd("a", "Append new", Phrases(Append))
|
||||
.cmd("i", "Insert new", Phrases(Insert))
|
||||
.cmd("n", "Rename", Phrases(Rename(Rename::Begin)))
|
||||
.cmd("t", "Set length", Phrases(Length(Length::Begin)))
|
||||
.cmd("d", "Delete", Phrases(Delete))
|
||||
.cmd("l", "Load from MIDI...", Phrases(Import))
|
||||
.cmd("s", "Save to MIDI...", Phrases(Export))
|
||||
.cmd(">", "Move up", Phrases(MoveUp))
|
||||
.cmd("<", "Move down", Phrases(MoveDown))
|
||||
})
|
||||
}
|
||||
};
|
||||
let mut app = Self { sequencer, arrangement, split: 15, size: Default::default() };
|
||||
app.update_focus();
|
||||
app
|
||||
}
|
||||
|
||||
//pub fn new (
|
||||
//jack: &Arc<RwLock<JackClient>>,
|
||||
//clock: &Arc<Clock>,
|
||||
//name: &str,
|
||||
//phrases: &Arc<RwLock<PhrasePool<E>>>
|
||||
//) -> Self {
|
||||
//Self {
|
||||
//jack: jack.clone(),
|
||||
//clock: clock.clone(),
|
||||
//name: Arc::new(RwLock::new(name.into())),
|
||||
//mode: ArrangementViewMode::Vertical(2),
|
||||
//selected: ArrangementEditorFocus::Clip(0, 0),
|
||||
//phrases: phrases.clone(),
|
||||
//scenes: vec![],
|
||||
//tracks: vec![],
|
||||
//focused: false,
|
||||
//color: Color::Rgb(28, 35, 25).into(),
|
||||
//size: Measure::new(),
|
||||
//entered: false,
|
||||
//}
|
||||
//}
|
||||
|
||||
/// Toggle global play/pause
|
||||
pub fn toggle_play (&mut self) -> Perhaps<bool> {
|
||||
match self.transport {
|
||||
|
|
@ -216,10 +111,12 @@ impl<E: Engine> ArrangerView<E> {
|
|||
}
|
||||
}
|
||||
/// Focus the editor with the current phrase
|
||||
pub fn show_phrase (&mut self) { self.editor.show(self.arrangement.phrase().as_ref()); }
|
||||
pub fn show_phrase (&mut self) {
|
||||
self.editor.show(self.arrangement.state.phrase().as_ref());
|
||||
}
|
||||
/// Focus the editor with the current phrase
|
||||
pub fn edit_phrase (&mut self) {
|
||||
if self.arrangement.selected.is_clip() && self.arrangement.phrase().is_none() {
|
||||
if self.arrangement.selected.is_clip() && self.arrangement.state.phrase().is_none() {
|
||||
self.phrases.write().unwrap().append_new(None, Some(self.next_color().into()));
|
||||
self.arrangement.phrase_put();
|
||||
}
|
||||
|
|
@ -284,7 +181,7 @@ impl<E: Engine> ArrangerView<E> {
|
|||
}
|
||||
}
|
||||
pub fn delete (&mut self) {
|
||||
match self.selected {
|
||||
match self.arrangement.selected {
|
||||
ArrangementEditorFocus::Track(_) => self.track_del(),
|
||||
ArrangementEditorFocus::Scene(_) => self.scene_del(),
|
||||
ArrangementEditorFocus::Clip(_, _) => self.phrase_del(),
|
||||
|
|
@ -292,7 +189,7 @@ impl<E: Engine> ArrangerView<E> {
|
|||
}
|
||||
}
|
||||
pub fn increment (&mut self) {
|
||||
match self.selected {
|
||||
match self.arrangement.selected {
|
||||
ArrangementEditorFocus::Track(_) => self.track_width_inc(),
|
||||
ArrangementEditorFocus::Scene(_) => self.scene_next(),
|
||||
ArrangementEditorFocus::Clip(_, _) => self.phrase_next(),
|
||||
|
|
@ -300,7 +197,7 @@ impl<E: Engine> ArrangerView<E> {
|
|||
}
|
||||
}
|
||||
pub fn decrement (&mut self) {
|
||||
match self.selected {
|
||||
match self.arrangement.selected {
|
||||
ArrangementEditorFocus::Track(_) => self.track_width_dec(),
|
||||
ArrangementEditorFocus::Scene(_) => self.scene_prev(),
|
||||
ArrangementEditorFocus::Clip(_, _) => self.phrase_prev(),
|
||||
|
|
@ -308,13 +205,13 @@ impl<E: Engine> ArrangerView<E> {
|
|||
}
|
||||
}
|
||||
pub fn zoom_in (&mut self) {
|
||||
if let ArrangementViewMode::Vertical(factor) = self.mode {
|
||||
self.mode = ArrangementViewMode::Vertical(factor + 1)
|
||||
if let ArrangementEditorMode::Vertical(factor) = self.mode {
|
||||
self.mode = ArrangementEditorMode::Vertical(factor + 1)
|
||||
}
|
||||
}
|
||||
pub fn zoom_out (&mut self) {
|
||||
if let ArrangementViewMode::Vertical(factor) = self.mode {
|
||||
self.mode = ArrangementViewMode::Vertical(factor.saturating_sub(1))
|
||||
if let ArrangementEditorMode::Vertical(factor) = self.mode {
|
||||
self.mode = ArrangementEditorMode::Vertical(factor.saturating_sub(1))
|
||||
}
|
||||
}
|
||||
pub fn is_first_row (&self) -> bool {
|
||||
|
|
@ -336,25 +233,25 @@ impl<E: Engine> ArrangerView<E> {
|
|||
}
|
||||
pub fn go_up (&mut self) {
|
||||
match self.mode {
|
||||
ArrangementViewMode::Horizontal => self.track_prev(),
|
||||
ArrangementEditorMode::Horizontal => self.track_prev(),
|
||||
_ => self.scene_prev(),
|
||||
};
|
||||
}
|
||||
pub fn go_down (&mut self) {
|
||||
match self.mode {
|
||||
ArrangementViewMode::Horizontal => self.track_next(),
|
||||
ArrangementEditorMode::Horizontal => self.track_next(),
|
||||
_ => self.scene_next(),
|
||||
};
|
||||
}
|
||||
pub fn go_left (&mut self) {
|
||||
match self.mode {
|
||||
ArrangementViewMode::Horizontal => self.scene_prev(),
|
||||
ArrangementEditorMode::Horizontal => self.scene_prev(),
|
||||
_ => self.track_prev(),
|
||||
};
|
||||
}
|
||||
pub fn go_right (&mut self) {
|
||||
match self.mode {
|
||||
ArrangementViewMode::Horizontal => self.scene_next(),
|
||||
ArrangementEditorMode::Horizontal => self.scene_next(),
|
||||
_ => self.track_next(),
|
||||
};
|
||||
}
|
||||
|
|
@ -457,10 +354,10 @@ impl<E: Engine> Arrangement<E> {
|
|||
}
|
||||
/// Methods for scenes in arrangement
|
||||
impl<E: Engine> Arrangement<E> {
|
||||
pub fn scene (&self) -> Option<&Scene> {
|
||||
pub fn scene (&self) -> Option<&ArrangementScene> {
|
||||
self.selected.scene().map(|s|self.scenes.get(s)).flatten()
|
||||
}
|
||||
pub fn scene_mut (&mut self) -> Option<&mut Scene> {
|
||||
pub fn scene_mut (&mut self) -> Option<&mut ArrangementScene> {
|
||||
self.selected.scene().map(|s|self.scenes.get_mut(s)).flatten()
|
||||
}
|
||||
pub fn scene_next (&mut self) {
|
||||
|
|
@ -573,7 +470,7 @@ impl ArrangementTrack {
|
|||
}
|
||||
|
||||
/// Arranger display mode can be cycled
|
||||
impl ArrangementViewMode {
|
||||
impl ArrangementEditorMode {
|
||||
/// Cycle arranger display mode
|
||||
pub fn to_next (&mut self) {
|
||||
*self = match self {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,13 @@ pub enum ArrangerStatusBar {
|
|||
PhraseView,
|
||||
PhraseEdit,
|
||||
}
|
||||
|
||||
impl StatusBar<Tui> for ArrangerStatusBar {
|
||||
fn hotkey_fg () -> Color {
|
||||
TuiTheme::hotkey_fg()
|
||||
}
|
||||
}
|
||||
|
||||
impl Content for ArrangerStatusBar {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
|
|
@ -24,19 +31,19 @@ impl Content for ArrangerStatusBar {
|
|||
Self::PhraseView => "VIEW SEQ",
|
||||
Self::PhraseEdit => "EDIT SEQ",
|
||||
};
|
||||
let status_bar_bg = Arranger::<Tui>::status_bar_bg();
|
||||
let mode_bg = Arranger::<Tui>::mode_bg();
|
||||
let mode_fg = Arranger::<Tui>::mode_fg();
|
||||
let status_bar_bg = TuiTheme::status_bar_bg();
|
||||
let mode_bg = TuiTheme::mode_bg();
|
||||
let mode_fg = TuiTheme::mode_fg();
|
||||
let mode = TuiStyle::bold(format!(" {label} "), true).bg(mode_bg).fg(mode_fg);
|
||||
let commands = match self {
|
||||
Self::ArrangementMix => command(&[
|
||||
Self::ArrangementMix => Self::command(&[
|
||||
["", "c", "olor"],
|
||||
["", "<>", "resize"],
|
||||
["", "+-", "zoom"],
|
||||
["", "n", "ame/number"],
|
||||
["", "Enter", " stop all"],
|
||||
]),
|
||||
Self::ArrangementClip => command(&[
|
||||
Self::ArrangementClip => Self::command(&[
|
||||
["", "g", "et"],
|
||||
["", "s", "et"],
|
||||
["", "a", "dd"],
|
||||
|
|
@ -48,7 +55,7 @@ impl Content for ArrangerStatusBar {
|
|||
["", ",.", "select"],
|
||||
["", "Enter", " launch"],
|
||||
]),
|
||||
Self::ArrangementTrack => command(&[
|
||||
Self::ArrangementTrack => Self::command(&[
|
||||
["re", "n", "ame"],
|
||||
["", ",.", "resize"],
|
||||
["", "<>", "move"],
|
||||
|
|
@ -59,12 +66,12 @@ impl Content for ArrangerStatusBar {
|
|||
["", "Del", "ete"],
|
||||
["", "Enter", " stop"],
|
||||
]),
|
||||
Self::ArrangementScene => command(&[
|
||||
Self::ArrangementScene => Self::command(&[
|
||||
["re", "n", "ame"],
|
||||
["", "Del", "ete"],
|
||||
["", "Enter", " launch"],
|
||||
]),
|
||||
Self::PhrasePool => command(&[
|
||||
Self::PhrasePool => Self::command(&[
|
||||
["", "a", "ppend"],
|
||||
["", "i", "nsert"],
|
||||
["", "d", "uplicate"],
|
||||
|
|
@ -75,34 +82,81 @@ impl Content for ArrangerStatusBar {
|
|||
["", ",.", "move"],
|
||||
["", "+-", "resize view"],
|
||||
]),
|
||||
Self::PhraseView => command(&[
|
||||
Self::PhraseView => Self::command(&[
|
||||
["", "enter", " edit"],
|
||||
["", "arrows/pgup/pgdn", " scroll"],
|
||||
["", "+=", "zoom"],
|
||||
]),
|
||||
Self::PhraseEdit => command(&[
|
||||
Self::PhraseEdit => Self::command(&[
|
||||
["", "esc", " exit"],
|
||||
["", "a", "ppend"],
|
||||
["", "s", "et"],
|
||||
["", "][", "length"],
|
||||
["", "+-", "zoom"],
|
||||
]),
|
||||
_ => command(&[])
|
||||
_ => Self::command(&[])
|
||||
};
|
||||
//let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}"));
|
||||
row!(mode, commands).fill_x().bg(status_bar_bg)
|
||||
}
|
||||
}
|
||||
|
||||
fn command (commands: &[[impl Widget<Engine = Tui>;3]]) -> impl Widget<Engine = Tui> + '_ {
|
||||
Stack::right(|add|{
|
||||
Ok(for [a, b, c] in commands.iter() {
|
||||
add(&row!(
|
||||
" ",
|
||||
widget(a),
|
||||
widget(b).bold(true).fg(Arranger::<Tui>::hotkey_fg()),
|
||||
widget(c),
|
||||
))?;
|
||||
})
|
||||
})
|
||||
}
|
||||
//pub fn arranger_menu_bar () -> MenuBar {
|
||||
//use ArrangementEditorCommand as Cmd;
|
||||
//use ArrangementCommand as Edit;
|
||||
//use ArrangementEditorFocus as Focus;
|
||||
//use ArrangementTrackCommand as Track;
|
||||
//use ArrangementClipCommand as Clip;
|
||||
//use ArrangementSceneCommand as Scene;
|
||||
//use TransportCommand as Transport;
|
||||
//MenuBar::new()
|
||||
//.add({
|
||||
//use ArrangementCommand::*;
|
||||
//Menu::new("File")
|
||||
//.cmd("n", "New project", ArrangerViewCommand::Arrangement(New))
|
||||
//.cmd("l", "Load project", ArrangerViewCommand::Arrangement(Load))
|
||||
//.cmd("s", "Save project", ArrangerViewCommand::Arrangement(Save))
|
||||
//})
|
||||
//.add({
|
||||
//use TransportViewCommand::*;
|
||||
//Menu::new("Transport")
|
||||
//.cmd("p", "Play", TransportCommand::Transport(Play(None)))
|
||||
//.cmd("P", "Play from start", TransportCommand::Transport(Play(Some(0))))
|
||||
//.cmd("s", "Pause", TransportCommand::Transport(Stop(None)))
|
||||
//.cmd("S", "Stop and rewind", TransportCommand::Transport(Stop(Some(0))))
|
||||
//})
|
||||
//.add({
|
||||
//use ArrangementCommand::*;
|
||||
//Menu::new("Track")
|
||||
//.cmd("a", "Append new", ArrangerViewCommand::Arrangement(AddTrack))
|
||||
//.cmd("i", "Insert new", ArrangerViewCommand::Arrangement(AddTrack))
|
||||
//.cmd("n", "Rename", ArrangerViewCommand::Arrangement(AddTrack))
|
||||
//.cmd("d", "Delete", ArrangerViewCommand::Arrangement(AddTrack))
|
||||
//.cmd(">", "Move up", ArrangerViewCommand::Arrangement(AddTrack))
|
||||
//.cmd("<", "Move down", ArrangerViewCommand::Arrangement(AddTrack))
|
||||
//})
|
||||
//.add({
|
||||
//use ArrangementCommand::*;
|
||||
//Menu::new("Scene")
|
||||
//.cmd("a", "Append new", ArrangerViewCommand::Arrangement(AddScene))
|
||||
//.cmd("i", "Insert new", ArrangerViewCommand::Arrangement(AddTrack))
|
||||
//.cmd("n", "Rename", ArrangerViewCommand::Arrangement(AddTrack))
|
||||
//.cmd("d", "Delete", ArrangerViewCommand::Arrangement(AddTrack))
|
||||
//.cmd(">", "Move up", ArrangerViewCommand::Arrangement(AddTrack))
|
||||
//.cmd("<", "Move down", ArrangerViewCommand::Arrangement(AddTrack))
|
||||
//})
|
||||
//.add({
|
||||
//use PhraseRenameCommand as Rename;
|
||||
//use PhraseLengthCommand as Length;
|
||||
//Menu::new("Phrase")
|
||||
//.cmd("a", "Append new", PhrasePoolCommand::Phrases(Append))
|
||||
//.cmd("i", "Insert new", PhrasePoolCommand::Phrases(Insert))
|
||||
//.cmd("n", "Rename", PhrasePoolCommand::Phrases(Rename(Rename::Begin)))
|
||||
//.cmd("t", "Set length", PhrasePoolCommand::Phrases(Length(Length::Begin)))
|
||||
//.cmd("d", "Delete", PhrasePoolCommand::Phrases(Delete))
|
||||
//.cmd("l", "Load from MIDI...", PhrasePoolCommand::Phrases(Import))
|
||||
//.cmd("s", "Save to MIDI...", PhrasePoolCommand::Phrases(Export))
|
||||
//.cmd(">", "Move up", PhrasePoolCommand::Phrases(MoveUp))
|
||||
//.cmd("<", "Move down", PhrasePoolCommand::Phrases(MoveDown))
|
||||
//})
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ use crate::*;
|
|||
pub enum ArrangerViewCommand {
|
||||
Focus(FocusCommand),
|
||||
Transport(TransportCommand),
|
||||
Arrangement(ArrangementEditorCommand),
|
||||
EditPhrase(Option<Arc<RwLock<Phrase>>>),
|
||||
Phrases(PhrasePoolCommand),
|
||||
Editor(PhraseEditorCommand),
|
||||
Arrangement(ArrangementCommand),
|
||||
EditPhrase(Option<Arc<RwLock<Phrase>>>),
|
||||
}
|
||||
|
||||
/// Handle top-level events in standalone arranger.
|
||||
|
|
@ -16,10 +16,10 @@ impl Handle<Tui> for ArrangerView<Tui> {
|
|||
if let Some(entered) = self.entered() {
|
||||
use ArrangerViewFocus::*;
|
||||
if let Some(true) = match entered {
|
||||
Transport => self.transport.as_mut().map(|t|t.handle(i)).transpose()?.flatten(),
|
||||
Transport => self.sequencer.transport.map(|t|t.handle(i)).transpose()?.flatten(),
|
||||
Arrangement => self.arrangement.handle(i)?,
|
||||
PhrasePool => self.phrases.write().unwrap().handle(i)?,
|
||||
PhraseEditor => self.editor.handle(i)?,
|
||||
PhrasePool => self.sequencer.phrases.handle(i)?,
|
||||
PhraseEditor => self.sequencer.editor.handle(i)?,
|
||||
} {
|
||||
return Ok(Some(true))
|
||||
}
|
||||
|
|
@ -70,44 +70,6 @@ impl InputToCommand<Tui, ArrangerView<Tui>> for ArrangerViewCommand {
|
|||
}
|
||||
}
|
||||
|
||||
/// Handle events for arrangement.
|
||||
impl Handle<Tui> for Arrangement<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
ArrangementCommand::execute_with_state(self, from)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, Arrangement<Tui>> for ArrangementCommand {
|
||||
fn input_to_command (state: &Arrangement<Tui>, input: &TuiInput) -> Option<Self> {
|
||||
use ArrangementCommand::*;
|
||||
match input.event() {
|
||||
key!(KeyCode::Char('`')) => Some(ToggleViewMode),
|
||||
key!(KeyCode::Delete) => Some(Delete),
|
||||
key!(KeyCode::Enter) => Some(Activate),
|
||||
key!(KeyCode::Char('.')) => Some(Increment),
|
||||
key!(KeyCode::Char(',')) => Some(Decrement),
|
||||
key!(KeyCode::Char('+')) => Some(ZoomIn),
|
||||
key!(KeyCode::Char('=')) => Some(ZoomOut),
|
||||
key!(KeyCode::Char('_')) => Some(ZoomOut),
|
||||
key!(KeyCode::Char('-')) => Some(ZoomOut),
|
||||
key!(KeyCode::Char('<')) => Some(MoveBack),
|
||||
key!(KeyCode::Char('>')) => Some(MoveForward),
|
||||
key!(KeyCode::Char('c')) => Some(RandomColor),
|
||||
key!(KeyCode::Char('s')) => Some(Put),
|
||||
key!(KeyCode::Char('g')) => Some(Get),
|
||||
key!(KeyCode::Char('e')) => Some(Edit(state.phrase())),
|
||||
key!(Ctrl-KeyCode::Char('a')) => Some(AddScene),
|
||||
key!(Ctrl-KeyCode::Char('t')) => Some(AddTrack),
|
||||
key!(KeyCode::Char('l')) => Some(ToggleLoop),
|
||||
key!(KeyCode::Up) => Some(GoUp),
|
||||
key!(KeyCode::Down) => Some(GoDown),
|
||||
key!(KeyCode::Left) => Some(GoLeft),
|
||||
key!(KeyCode::Right) => Some(GoRight),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//impl ArrangerView<Tui> {
|
||||
///// Helper for event passthru to focused component
|
||||
//fn handle_focused (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
|
|
@ -160,33 +122,6 @@ impl InputToCommand<Tui, Arrangement<Tui>> for ArrangementCommand {
|
|||
//}
|
||||
//}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ArrangementCommand {
|
||||
New,
|
||||
Load,
|
||||
Save,
|
||||
ToggleViewMode,
|
||||
Delete,
|
||||
Activate,
|
||||
Increment,
|
||||
Decrement,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
MoveBack,
|
||||
MoveForward,
|
||||
RandomColor,
|
||||
Put,
|
||||
Get,
|
||||
AddScene,
|
||||
AddTrack,
|
||||
ToggleLoop,
|
||||
GoUp,
|
||||
GoDown,
|
||||
GoLeft,
|
||||
GoRight,
|
||||
Edit(Option<Arc<RwLock<Phrase>>>),
|
||||
}
|
||||
|
||||
impl<E: Engine> Command<ArrangerView<E>> for ArrangerViewCommand {
|
||||
fn execute (self, state: &mut ArrangerView<E>) -> Perhaps<Self> {
|
||||
let undo = match self {
|
||||
|
|
@ -194,59 +129,28 @@ impl<E: Engine> Command<ArrangerView<E>> for ArrangerViewCommand {
|
|||
delegate(cmd, Self::Focus, state)
|
||||
},
|
||||
Self::Phrases(cmd) => {
|
||||
delegate(cmd, Self::Phrases, &mut*state.phrases.write().unwrap())
|
||||
delegate(cmd, Self::Phrases, &mut*state.sequencer.phrases.write().unwrap())
|
||||
},
|
||||
Self::Editor(cmd) => {
|
||||
delegate(cmd, Self::Editor, &mut state.editor)
|
||||
delegate(cmd, Self::Editor, &mut state.sequencer.editor)
|
||||
},
|
||||
Self::Arrangement(cmd) => {
|
||||
delegate(cmd, Self::Arrangement, &mut state.arrangement)
|
||||
},
|
||||
Self::Transport(cmd) => if let Some(ref transport) = state.transport {
|
||||
Self::Transport(cmd) => if let Some(ref transport) = state.sequencer.transport {
|
||||
delegate(cmd, Self::Transport, &mut*transport.write().unwrap())
|
||||
} else {
|
||||
Ok(None)
|
||||
},
|
||||
Self::EditPhrase(phrase) => {
|
||||
state.editor.phrase = phrase.clone();
|
||||
state.sequencer.editor.phrase = phrase.clone();
|
||||
state.focus(ArrangerViewFocus::PhraseEditor);
|
||||
state.focus_enter();
|
||||
Ok(None)
|
||||
}
|
||||
}?;
|
||||
state.show_phrase();
|
||||
state.update_status();
|
||||
state.sequencer.show_phrase();
|
||||
state.sequencer.update_status();
|
||||
return Ok(undo);
|
||||
}
|
||||
}
|
||||
impl<E: Engine> Command<Arrangement<E>> for ArrangementCommand {
|
||||
fn execute (self, state: &mut Arrangement<E>) -> Perhaps<Self> {
|
||||
use ArrangementCommand::*;
|
||||
match self {
|
||||
New => todo!(),
|
||||
Load => todo!(),
|
||||
Save => todo!(),
|
||||
Edit(phrase) => { state.phrase = phrase.clone() },
|
||||
ToggleViewMode => { state.mode.to_next(); },
|
||||
Delete => { state.delete(); },
|
||||
Activate => { state.activate(); },
|
||||
Increment => { state.increment(); },
|
||||
Decrement => { state.decrement(); },
|
||||
ZoomIn => { state.zoom_in(); },
|
||||
ZoomOut => { state.zoom_out(); },
|
||||
MoveBack => { state.move_back(); },
|
||||
MoveForward => { state.move_forward(); },
|
||||
RandomColor => { state.randomize_color(); },
|
||||
Put => { state.phrase_put(); },
|
||||
Get => { state.phrase_get(); },
|
||||
AddScene => { state.scene_add(None, None)?; },
|
||||
AddTrack => { state.track_add(None, None)?; },
|
||||
ToggleLoop => { state.toggle_loop() },
|
||||
GoUp => { state.go_up() },
|
||||
GoDown => { state.go_down() },
|
||||
GoLeft => { state.go_left() },
|
||||
GoRight => { state.go_right() },
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait ArrangerTheme<E: Engine> {
|
||||
fn border_bg () -> Color;
|
||||
fn border_fg (focused: bool) -> Color;
|
||||
fn title_fg (focused: bool) -> Color;
|
||||
fn separator_fg (focused: bool) -> Color;
|
||||
fn hotkey_fg () -> Color;
|
||||
fn mode_bg () -> Color;
|
||||
fn mode_fg () -> Color;
|
||||
fn status_bar_bg () -> Color;
|
||||
}
|
||||
|
||||
impl ArrangerTheme<Tui> for Arranger<Tui> {
|
||||
fn border_bg () -> Color {
|
||||
Color::Rgb(40, 50, 30)
|
||||
}
|
||||
fn border_fg (focused: bool) -> Color {
|
||||
if focused { Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) }
|
||||
}
|
||||
fn title_fg (focused: bool) -> Color {
|
||||
if focused { Color::Rgb(150, 160, 90) } else { Color::Rgb(120, 130, 100) }
|
||||
}
|
||||
fn separator_fg (_: bool) -> Color {
|
||||
Color::Rgb(0, 0, 0)
|
||||
}
|
||||
fn hotkey_fg () -> Color {
|
||||
Color::Rgb(255, 255, 0)
|
||||
}
|
||||
fn mode_bg () -> Color {
|
||||
Color::Rgb(150, 160, 90)
|
||||
}
|
||||
fn mode_fg () -> Color {
|
||||
Color::Rgb(255, 255, 255)
|
||||
}
|
||||
fn status_bar_bg () -> Color {
|
||||
Color::Rgb(28, 35, 25)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@ use crate::*;
|
|||
pub struct PhrasePoolView<E: Engine> {
|
||||
_engine: PhantomData<E>,
|
||||
state: PhrasePool,
|
||||
/// Selected phrase
|
||||
pub phrase: usize,
|
||||
/// Scroll offset
|
||||
pub scroll: usize,
|
||||
/// Mode switch
|
||||
|
|
@ -21,68 +23,16 @@ pub enum PhrasePoolMode {
|
|||
Length(usize, usize, PhraseLengthFocus),
|
||||
}
|
||||
|
||||
/// Displays and edits phrase length.
|
||||
pub struct PhraseLength<E: Engine> {
|
||||
_engine: PhantomData<E>,
|
||||
/// Pulses per beat (quaver)
|
||||
pub ppq: usize,
|
||||
/// Beats per bar
|
||||
pub bpb: usize,
|
||||
/// Length of phrase in pulses
|
||||
pub pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<PhraseLengthFocus>,
|
||||
}
|
||||
|
||||
impl<E: Engine> PhraseLength<E> {
|
||||
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
|
||||
Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus }
|
||||
}
|
||||
pub fn bars (&self) -> usize { self.pulses / (self.bpb * self.ppq) }
|
||||
pub fn beats (&self) -> usize { (self.pulses % (self.bpb * self.ppq)) / self.ppq }
|
||||
pub fn ticks (&self) -> usize { self.pulses % self.ppq }
|
||||
pub fn bars_string (&self) -> String { format!("{}", self.bars()) }
|
||||
pub fn beats_string (&self) -> String { format!("{}", self.beats()) }
|
||||
pub fn ticks_string (&self) -> String { format!("{:>02}", self.ticks()) }
|
||||
}
|
||||
|
||||
/// Focused field of `PhraseLength`
|
||||
#[derive(Copy, Clone)] 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> PhrasePool<E> {
|
||||
pub fn new () -> Self {
|
||||
pub fn new (phrases: PhrasePool) -> Self {
|
||||
Self {
|
||||
_engine: Default::default(),
|
||||
scroll: 0,
|
||||
phrase: 0,
|
||||
phrases: vec![Arc::new(RwLock::new(Phrase::default()))],
|
||||
mode: None,
|
||||
focused: false,
|
||||
entered: false,
|
||||
phrases,
|
||||
}
|
||||
}
|
||||
pub fn len (&self) -> usize { self.phrases.len() }
|
||||
|
|
@ -196,37 +146,3 @@ impl Content for PhrasePool<Tui> {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Content for PhraseLength<Tui> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
Layers::new(move|add|{
|
||||
match self.focus {
|
||||
None => add(&row!(
|
||||
" ", self.bars_string(),
|
||||
".", self.beats_string(),
|
||||
".", self.ticks_string(),
|
||||
" "
|
||||
)),
|
||||
Some(PhraseLengthFocus::Bar) => add(&row!(
|
||||
"[", self.bars_string(),
|
||||
"]", self.beats_string(),
|
||||
".", self.ticks_string(),
|
||||
" "
|
||||
)),
|
||||
Some(PhraseLengthFocus::Beat) => add(&row!(
|
||||
" ", self.bars_string(),
|
||||
"[", self.beats_string(),
|
||||
"]", self.ticks_string(),
|
||||
" "
|
||||
)),
|
||||
Some(PhraseLengthFocus::Tick) => add(&row!(
|
||||
" ", self.bars_string(),
|
||||
".", self.beats_string(),
|
||||
"[", self.ticks_string(),
|
||||
"]"
|
||||
)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,109 +1,52 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum PhrasePoolCommand {
|
||||
Prev,
|
||||
Next,
|
||||
MoveUp,
|
||||
MoveDown,
|
||||
Delete,
|
||||
Append,
|
||||
Insert,
|
||||
Duplicate,
|
||||
RandomColor,
|
||||
Edit,
|
||||
Import,
|
||||
Export,
|
||||
pub enum PhrasePoolViewCommand {
|
||||
Edit(PhrasePoolCommand),
|
||||
Rename(PhraseRenameCommand),
|
||||
Length(PhraseLengthCommand),
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum PhraseRenameCommand {
|
||||
Begin,
|
||||
Backspace,
|
||||
Append(char),
|
||||
Set(String),
|
||||
Confirm,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum PhraseLengthCommand {
|
||||
Begin,
|
||||
Next,
|
||||
Prev,
|
||||
Inc,
|
||||
Dec,
|
||||
Set(usize),
|
||||
Confirm,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl Handle<Tui> for PhrasePool<Tui> {
|
||||
impl Handle<Tui> for PhrasePoolView<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
if let Some(command) = PhrasePoolCommand::input_to_command(self, from) {
|
||||
let _undo = command.execute(self)?;
|
||||
return Ok(Some(true))
|
||||
}
|
||||
Ok(None)
|
||||
PhrasePoolViewCommand::execute_with_state(self, from)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhrasePool<Tui>> for PhrasePoolCommand {
|
||||
fn input_to_command (state: &PhrasePool<Tui>, input: &TuiInput) -> Option<Self> {
|
||||
impl InputToCommand<Tui, PhrasePoolView<Tui>> for PhrasePoolViewCommand {
|
||||
fn input_to_command (state: &PhrasePoolView<Tui>, input: &TuiInput) -> Option<Self> {
|
||||
use PhrasePoolViewCommand as Cmd;
|
||||
use PhrasePoolCommand as Edit;
|
||||
use PhraseRenameCommand as Rename;
|
||||
use PhraseLengthCommand as Length;
|
||||
match input.event() {
|
||||
key!(KeyCode::Up) => Some(Self::Prev),
|
||||
key!(KeyCode::Down) => Some(Self::Next),
|
||||
key!(KeyCode::Char(',')) => Some(Self::MoveUp),
|
||||
key!(KeyCode::Char('.')) => Some(Self::MoveDown),
|
||||
key!(KeyCode::Delete) => Some(Self::Delete),
|
||||
key!(KeyCode::Char('a')) => Some(Self::Append),
|
||||
key!(KeyCode::Char('i')) => Some(Self::Insert),
|
||||
key!(KeyCode::Char('d')) => Some(Self::Duplicate),
|
||||
key!(KeyCode::Char('c')) => Some(Self::RandomColor),
|
||||
key!(KeyCode::Char('n')) => Some(Self::Rename(PhraseRenameCommand::Begin)),
|
||||
key!(KeyCode::Char('t')) => Some(Self::Length(PhraseLengthCommand::Begin)),
|
||||
key!(KeyCode::Up) => Some(Cmd::Edit(Edit::Select(0))),
|
||||
key!(KeyCode::Down) => Some(Cmd::Edit(Edit::Select(0))),
|
||||
key!(KeyCode::Char(',')) => Some(Cmd::Edit(Edit::Swap(0))),
|
||||
key!(KeyCode::Char('.')) => Some(Cmd::Edit(Edit::Swap(0))),
|
||||
key!(KeyCode::Delete) => Some(Cmd::Edit(Edit::Delete(0))),
|
||||
key!(KeyCode::Char('a')) => Some(Cmd::Edit(Edit::Add(0))),
|
||||
key!(KeyCode::Char('i')) => Some(Cmd::Edit(Edit::Add(0))),
|
||||
key!(KeyCode::Char('d')) => Some(Cmd::Edit(Edit::Duplicate(0))),
|
||||
key!(KeyCode::Char('c')) => Some(Cmd::Edit(Edit::RandomColor(0))),
|
||||
key!(KeyCode::Char('n')) => Some(Cmd::Rename(Rename::Begin)),
|
||||
key!(KeyCode::Char('t')) => Some(Cmd::Length(Length::Begin)),
|
||||
_ => match state.mode {
|
||||
Some(PhrasePoolMode::Rename(..)) => PhraseRenameCommand::input_to_command(state, input)
|
||||
.map(Self::Rename),
|
||||
Some(PhrasePoolMode::Length(..)) => PhraseLengthCommand::input_to_command(state, input)
|
||||
.map(Self::Length),
|
||||
Some(PhrasePoolMode::Rename(..)) => {
|
||||
Rename::input_to_command(state, input).map(Cmd::Rename)
|
||||
},
|
||||
Some(PhrasePoolMode::Length(..)) => {
|
||||
Length::input_to_command(state, input).map(Cmd::Length)
|
||||
},
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhrasePool<Tui>> for PhraseRenameCommand {
|
||||
fn input_to_command (_: &PhrasePool<Tui>, from: &TuiInput) -> Option<Self> {
|
||||
match from.event() {
|
||||
key!(KeyCode::Backspace) => Some(Self::Backspace),
|
||||
key!(KeyCode::Enter) => Some(Self::Confirm),
|
||||
key!(KeyCode::Esc) => Some(Self::Cancel),
|
||||
key!(KeyCode::Char(c)) => Some(Self::Append(*c)),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhrasePool<Tui>> for PhraseLengthCommand {
|
||||
fn input_to_command (_: &PhrasePool<Tui>, from: &TuiInput) -> Option<Self> {
|
||||
match from.event() {
|
||||
key!(KeyCode::Up) => Some(Self::Inc),
|
||||
key!(KeyCode::Down) => Some(Self::Dec),
|
||||
key!(KeyCode::Right) => Some(Self::Next),
|
||||
key!(KeyCode::Left) => Some(Self::Prev),
|
||||
key!(KeyCode::Enter) => Some(Self::Confirm),
|
||||
key!(KeyCode::Esc) => Some(Self::Cancel),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Command<PhrasePool<E>> for PhrasePoolCommand {
|
||||
fn execute (self, state: &mut PhrasePool<E>) -> Perhaps<Self> {
|
||||
use PhrasePoolCommand::*;
|
||||
impl<E: Engine> Command<PhrasePoolView<E>> for PhrasePoolViewCommand {
|
||||
fn execute (self, state: &mut PhrasePoolView<E>) -> Perhaps<Self> {
|
||||
use PhrasePoolViewCommand::*;
|
||||
use PhraseRenameCommand as Rename;
|
||||
use PhraseLengthCommand as Length;
|
||||
match self {
|
||||
|
|
@ -123,103 +66,3 @@ impl<E: Engine> Command<PhrasePool<E>> for PhrasePoolCommand {
|
|||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Command<PhrasePool<E>> for PhraseRenameCommand {
|
||||
fn translate (self, state: &PhrasePool<E>) -> Self {
|
||||
use PhraseRenameCommand::*;
|
||||
if let Some(PhrasePoolMode::Rename(_, ref old_name)) = state.mode {
|
||||
match self {
|
||||
Backspace => {
|
||||
let mut new_name = old_name.clone();
|
||||
new_name.pop();
|
||||
return Self::Set(new_name)
|
||||
},
|
||||
Append(c) => {
|
||||
let mut new_name = old_name.clone();
|
||||
new_name.push(c);
|
||||
return Self::Set(new_name)
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
} else if self != Begin {
|
||||
unreachable!()
|
||||
}
|
||||
self
|
||||
}
|
||||
fn execute (self, state: &mut PhrasePool<E>) -> Perhaps<Self> {
|
||||
use PhraseRenameCommand::*;
|
||||
if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = state.mode {
|
||||
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.mode = None;
|
||||
return Ok(Some(Self::Set(old_name)))
|
||||
},
|
||||
Cancel => {
|
||||
let mut phrase = state.phrases[phrase].write().unwrap();
|
||||
phrase.name = old_name.clone();
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
Ok(None)
|
||||
} else if self == Begin {
|
||||
todo!()
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Command<PhrasePool<E>> for PhraseLengthCommand {
|
||||
fn translate (self, state: &PhrasePool<E>) -> Self {
|
||||
use PhraseLengthCommand::*;
|
||||
if let Some(PhrasePoolMode::Length(_, length, _)) = state.mode {
|
||||
match self {
|
||||
Confirm => { return Self::Set(length) },
|
||||
_ => self
|
||||
}
|
||||
} else if self == Begin {
|
||||
todo!()
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
fn execute (self, state: &mut PhrasePool<E>) -> Perhaps<Self> {
|
||||
use PhraseLengthFocus::*;
|
||||
use PhraseLengthCommand::*;
|
||||
if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = state.mode {
|
||||
match self {
|
||||
Cancel => { state.mode = 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;
|
||||
state.mode = None;
|
||||
return Ok(Some(Self::Set(old_length)))
|
||||
},
|
||||
_ => unreachable!()
|
||||
}
|
||||
Ok(None)
|
||||
} else if self == Begin {
|
||||
todo!()
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
176
crates/tek_tui/src/tui_pool_length.rs
Normal file
176
crates/tek_tui/src/tui_pool_length.rs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
use crate::*;
|
||||
|
||||
/// Displays and edits phrase length.
|
||||
pub struct PhraseLength<E: Engine> {
|
||||
_engine: PhantomData<E>,
|
||||
/// Pulses per beat (quaver)
|
||||
pub ppq: usize,
|
||||
/// Beats per bar
|
||||
pub bpb: usize,
|
||||
/// Length of phrase in pulses
|
||||
pub pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<PhraseLengthFocus>,
|
||||
}
|
||||
|
||||
impl<E: Engine> PhraseLength<E> {
|
||||
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
|
||||
Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus }
|
||||
}
|
||||
pub fn bars (&self) -> usize {
|
||||
self.pulses / (self.bpb * self.ppq)
|
||||
}
|
||||
pub fn beats (&self) -> usize {
|
||||
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
||||
}
|
||||
pub fn ticks (&self) -> usize {
|
||||
self.pulses % self.ppq
|
||||
}
|
||||
pub fn bars_string (&self) -> String {
|
||||
format!("{}", self.bars())
|
||||
}
|
||||
pub fn beats_string (&self) -> String {
|
||||
format!("{}", self.beats())
|
||||
}
|
||||
pub fn ticks_string (&self) -> String {
|
||||
format!("{:>02}", self.ticks())
|
||||
}
|
||||
}
|
||||
|
||||
impl Content for PhraseLength<Tui> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
Layers::new(move|add|{
|
||||
match self.focus {
|
||||
None => add(&row!(
|
||||
" ", self.bars_string(),
|
||||
".", self.beats_string(),
|
||||
".", self.ticks_string(),
|
||||
" "
|
||||
)),
|
||||
Some(PhraseLengthFocus::Bar) => add(&row!(
|
||||
"[", self.bars_string(),
|
||||
"]", self.beats_string(),
|
||||
".", self.ticks_string(),
|
||||
" "
|
||||
)),
|
||||
Some(PhraseLengthFocus::Beat) => add(&row!(
|
||||
" ", self.bars_string(),
|
||||
"[", self.beats_string(),
|
||||
"]", self.ticks_string(),
|
||||
" "
|
||||
)),
|
||||
Some(PhraseLengthFocus::Tick) => add(&row!(
|
||||
" ", self.bars_string(),
|
||||
".", self.beats_string(),
|
||||
"[", self.ticks_string(),
|
||||
"]"
|
||||
)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Focused field of `PhraseLength`
|
||||
#[derive(Copy, Clone)]
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum PhraseLengthCommand {
|
||||
Begin,
|
||||
Next,
|
||||
Prev,
|
||||
Inc,
|
||||
Dec,
|
||||
Set(usize),
|
||||
Confirm,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhrasePoolView<Tui>> for PhraseLengthCommand {
|
||||
fn input_to_command (_: &PhrasePoolView<Tui>, from: &TuiInput) -> Option<Self> {
|
||||
match from.event() {
|
||||
key!(KeyCode::Up) => Some(Self::Inc),
|
||||
key!(KeyCode::Down) => Some(Self::Dec),
|
||||
key!(KeyCode::Right) => Some(Self::Next),
|
||||
key!(KeyCode::Left) => Some(Self::Prev),
|
||||
key!(KeyCode::Enter) => Some(Self::Confirm),
|
||||
key!(KeyCode::Esc) => Some(Self::Cancel),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Command<PhrasePoolView<E>> for PhraseLengthCommand {
|
||||
fn translate (self, state: &PhrasePoolView<E>) -> Self {
|
||||
use PhraseLengthCommand::*;
|
||||
if let Some(PhrasePoolMode::Length(_, length, _)) = state.mode {
|
||||
match self {
|
||||
Confirm => { return Self::Set(length) },
|
||||
_ => self
|
||||
}
|
||||
} else if self == Begin {
|
||||
todo!()
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
fn execute (self, state: &mut PhrasePoolView<E>) -> Perhaps<Self> {
|
||||
use PhraseLengthFocus::*;
|
||||
use PhraseLengthCommand::*;
|
||||
if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = state.mode {
|
||||
match self {
|
||||
Cancel => { state.mode = 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;
|
||||
state.mode = None;
|
||||
return Ok(Some(Self::Set(old_length)))
|
||||
},
|
||||
_ => unreachable!()
|
||||
}
|
||||
Ok(None)
|
||||
} else if self == Begin {
|
||||
todo!()
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
73
crates/tek_tui/src/tui_pool_rename.rs
Normal file
73
crates/tek_tui/src/tui_pool_rename.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum PhraseRenameCommand {
|
||||
Begin,
|
||||
Backspace,
|
||||
Append(char),
|
||||
Set(String),
|
||||
Confirm,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhrasePoolView<Tui>> for PhraseRenameCommand {
|
||||
fn input_to_command (_: &PhrasePoolView<Tui>, from: &TuiInput) -> Option<Self> {
|
||||
match from.event() {
|
||||
key!(KeyCode::Backspace) => Some(Self::Backspace),
|
||||
key!(KeyCode::Enter) => Some(Self::Confirm),
|
||||
key!(KeyCode::Esc) => Some(Self::Cancel),
|
||||
key!(KeyCode::Char(c)) => Some(Self::Append(*c)),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Command<PhrasePoolView<E>> for PhraseRenameCommand {
|
||||
fn translate (self, state: &PhrasePoolView<E>) -> Self {
|
||||
use PhraseRenameCommand::*;
|
||||
if let Some(PhrasePoolMode::Rename(_, ref old_name)) = state.mode {
|
||||
match self {
|
||||
Backspace => {
|
||||
let mut new_name = old_name.clone();
|
||||
new_name.pop();
|
||||
return Self::Set(new_name)
|
||||
},
|
||||
Append(c) => {
|
||||
let mut new_name = old_name.clone();
|
||||
new_name.push(c);
|
||||
return Self::Set(new_name)
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
} else if self != Begin {
|
||||
unreachable!()
|
||||
}
|
||||
self
|
||||
}
|
||||
fn execute (self, state: &mut PhrasePoolView<E>) -> Perhaps<Self> {
|
||||
use PhraseRenameCommand::*;
|
||||
if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = state.mode {
|
||||
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.mode = None;
|
||||
return Ok(Some(Self::Set(old_name)))
|
||||
},
|
||||
Cancel => {
|
||||
let mut phrase = state.phrases[phrase].write().unwrap();
|
||||
phrase.name = old_name.clone();
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
Ok(None)
|
||||
} else if self == Begin {
|
||||
todo!()
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,10 +13,6 @@ pub struct SequencerView<E: Engine> {
|
|||
pub editor: PhraseEditor<E>,
|
||||
/// Phrase player
|
||||
pub player: MIDIPlayer,
|
||||
/// Which view is focused
|
||||
pub cursor: (usize, usize),
|
||||
/// Whether the currently focused item is entered
|
||||
pub entered: bool,
|
||||
}
|
||||
|
||||
/// JACK process callback for sequencer app
|
||||
|
|
|
|||
|
|
@ -6,3 +6,17 @@ pub enum SequencerStatusBar {
|
|||
PhrasePool,
|
||||
PhraseEditor,
|
||||
}
|
||||
|
||||
impl StatusBar<Tui> for SequencerStatusBar {
|
||||
fn hotkey_fg () -> Color {
|
||||
TuiTheme::hotkey_fg()
|
||||
}
|
||||
}
|
||||
|
||||
impl Content for SequencerStatusBar {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
todo!();
|
||||
""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
crates/tek_tui/src/tui_theme.rs
Normal file
30
crates/tek_tui/src/tui_theme.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
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 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ pub struct TransportView<E: Engine> {
|
|||
state: Transport,
|
||||
focus: TransportViewFocus,
|
||||
focused: bool,
|
||||
size: Measure<E>,
|
||||
}
|
||||
|
||||
/// JACK process callback for transport app
|
||||
|
|
|
|||
|
|
@ -1 +1,17 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct TransportStatusBar;
|
||||
|
||||
impl StatusBar<Tui> for TransportStatusBar {
|
||||
fn hotkey_fg () -> Color {
|
||||
TuiTheme::hotkey_fg()
|
||||
}
|
||||
}
|
||||
|
||||
impl Content for TransportStatusBar {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
todo!();
|
||||
""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue