wip: refactor pt.9, 403 errors

This commit is contained in:
🪞👃🪞 2024-11-10 18:38:22 +01:00
parent a784f7a6f2
commit 8aa1ba8d0f
29 changed files with 1008 additions and 902 deletions

View file

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

View 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() },

View file

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

View file

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

View file

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

View file

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

View file

@ -1,14 +0,0 @@
use crate::*;
#[derive(Clone)]
pub enum SceneCommand {
Next,
Prev,
Add,
Delete,
MoveForward,
MoveBack,
RandomColor,
SetSize(usize),
SetZoom(usize),
}

View 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),
))?;
})
})
}
}

View file

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

View file

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

View file

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

View file

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

View 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),
}
}
}

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

View file

@ -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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View 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!()
}
}
}

View 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!()
}
}
}

View file

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

View file

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

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

View file

@ -8,6 +8,7 @@ pub struct TransportView<E: Engine> {
state: Transport,
focus: TransportViewFocus,
focused: bool,
size: Measure<E>,
}
/// JACK process callback for transport app

View file

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