wip: refactor pt.29: 12 errors

This commit is contained in:
🪞👃🪞 2024-11-14 20:56:23 +01:00
parent ceead4131c
commit ab85a86b6b
27 changed files with 1890 additions and 1865 deletions

View file

@ -1,25 +1 @@
use crate::*;
pub trait StatusBar<E: Engine, S>: Widget<Engine = E> {
fn hotkey_fg () -> Color where Self: Sized;
fn update (&mut self, state: &S);
fn command (commands: &[[impl Widget<Engine = Tui>;3]])
-> impl Widget<Engine = Tui> + '_
where
Self: Sized
{
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

@ -122,6 +122,21 @@ impl<E: Engine, W: Widget<Engine = E>> Widget for Option<W> {
self.as_ref().map(|widget|widget.render(to)).unwrap_or(Ok(()))
}
}
/// Render either of two widgets depending on predicate
pub struct Either<E: Engine, A: Widget<Engine = E>, B: Widget<Engine = E>>(
pub bool,
pub A,
pub B,
);
impl<E: Engine, A: Widget<Engine = E>, B: Widget<Engine = E>> Widget for Either<E, A, B> {
type Engine = E;
fn layout (&self, to: E::Size) -> Perhaps<E::Size> {
if self.0 { self.1.layout(to) } else { self.2.layout(to) }
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
if self.0 { self.1.render(to) } else { self.2.render(to) }
}
}
/// A custom [Widget] defined by passing layout and render closures in place.
pub struct CustomWidget<
E: Engine,

View file

@ -10,73 +10,69 @@ pub(crate) use std::path::PathBuf;
pub(crate) use std::ffi::OsString;
pub(crate) use std::fs::read_dir;
use std::fmt::Debug;
submod! {
tui_arranger
tui_arranger_bar
tui_arranger_cmd
tui_arranger_foc
tui_arranger_hor
tui_arranger_ver
//tui_mixer // TODO
//tui_mixer_cmd
tui_phrase
tui_phrase_cmd
//tui_plugin // TODO
//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 // TODO
//tui_sampler_cmd
tui_sequencer
tui_sequencer_bar
tui_sequencer_cmd
tui_sequencer_foc
tui_status
tui_theme
tui_transport
tui_transport_bar
tui_transport_cmd
tui_transport_foc
}
pub struct AppView<E, A, C>
pub struct AppView<E, A, C, S>
where
E: Engine,
A: Widget<Engine = E> + Handle<E> + Audio,
C: Command<A>,
S: StatusBar,
{
pub app: A,
pub cursor: (usize, usize),
pub entered: bool,
pub menu_bar: Option<MenuBar<E, A, C>>,
pub status_bar: Option<Box<dyn StatusBar<E, Self>>>,
pub status_bar: Option<S>,
pub history: Vec<C>,
pub size: Measure<E>,
}
impl<E, A, C> AppView<E, A, C>
#[derive(Debug, Copy, Clone)]
pub enum AppViewCommand<T: Debug + Copy + Clone> {
Focus(FocusCommand),
Undo,
Redo,
App(T)
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum AppViewFocus<F: Debug + Copy + Clone + PartialEq> {
Menu,
Content(F),
}
impl<E, A, C, S> AppView<E, A, C, S>
where
E: Engine,
A: Widget<Engine = E> + Handle<E> + Audio,
C: Command<A>
C: Command<A>,
S: StatusBar
{
pub fn new (
app: A,
menu_bar: Option<MenuBar<E, A, C>>,
status_bar: Option<Box<dyn StatusBar<E, Self>>>,
status_bar: Option<S>,
) -> Self {
Self {
app,
@ -90,10 +86,11 @@ where
}
}
impl<A, C> Content for AppView<Tui, A, C>
impl<A, C, S> Content for AppView<Tui, A, C, S>
where
A: Widget<Engine = Tui> + Handle<Tui> + Audio,
C: Command<A>
C: Command<A>,
S: StatusBar,
{
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
@ -106,25 +103,15 @@ where
row!(menu in menus.iter() => {
row!(" ", menu.title.as_str(), " ")
}),
Split::up(
if self.status_bar.is_some() { 1 } else { 0 },
widget(&self.status_bar),
Either(
self.status_bar.is_some(),
Split::up(
1,
widget(self.status_bar.as_ref().unwrap()),
widget(&self.app)
),
widget(&self.app)
)
)
}
}
#[derive(Debug, Copy, Clone)]
pub enum AppViewCommand<T: std::fmt::Debug + Copy + Clone> {
Focus(FocusCommand),
Undo,
Redo,
App(T)
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum AppViewFocus<F: std::fmt::Debug + Copy + Clone + PartialEq> {
Menu,
Content(F),
}

File diff suppressed because it is too large Load diff

View file

@ -1,180 +0,0 @@
use crate::*;
/// Status bar for arranger ap
pub enum ArrangerStatusBar {
Transport,
ArrangerMix,
ArrangerTrack,
ArrangerScene,
ArrangerClip,
PhrasePool,
PhraseView,
PhraseEdit,
}
impl StatusBar<Tui, ArrangerApp<Tui>> for ArrangerStatusBar {
fn hotkey_fg () -> Color where Self: Sized {
TuiTheme::hotkey_fg()
}
fn update (&mut self, state: &ArrangerApp<Tui>) {
use AppViewFocus::*;
if let Content(focused) = state.focused() {
*self = match focused {
ArrangerViewFocus::Transport => ArrangerStatusBar::Transport,
ArrangerViewFocus::Arranger => match state.app.selected {
ArrangerFocus::Mix => ArrangerStatusBar::ArrangerMix,
ArrangerFocus::Track(_) => ArrangerStatusBar::ArrangerTrack,
ArrangerFocus::Scene(_) => ArrangerStatusBar::ArrangerScene,
ArrangerFocus::Clip(_, _) => ArrangerStatusBar::ArrangerClip,
},
ArrangerViewFocus::PhrasePool => ArrangerStatusBar::PhrasePool,
ArrangerViewFocus::PhraseEditor => match state.app.sequencer.editor.entered {
true => ArrangerStatusBar::PhraseEdit,
false => ArrangerStatusBar::PhraseView,
},
}
}
}
}
impl Content for ArrangerStatusBar {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let label = match self {
Self::Transport => "TRANSPORT",
Self::ArrangerMix => "PROJECT",
Self::ArrangerTrack => "TRACK",
Self::ArrangerScene => "SCENE",
Self::ArrangerClip => "CLIP",
Self::PhrasePool => "SEQ LIST",
Self::PhraseView => "VIEW SEQ",
Self::PhraseEdit => "EDIT SEQ",
};
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::ArrangerMix => Self::command(&[
["", "c", "olor"],
["", "<>", "resize"],
["", "+-", "zoom"],
["", "n", "ame/number"],
["", "Enter", " stop all"],
]),
Self::ArrangerClip => Self::command(&[
["", "g", "et"],
["", "s", "et"],
["", "a", "dd"],
["", "i", "ns"],
["", "d", "up"],
["", "e", "dit"],
["", "c", "olor"],
["re", "n", "ame"],
["", ",.", "select"],
["", "Enter", " launch"],
]),
Self::ArrangerTrack => Self::command(&[
["re", "n", "ame"],
["", ",.", "resize"],
["", "<>", "move"],
["", "i", "nput"],
["", "o", "utput"],
["", "m", "ute"],
["", "s", "olo"],
["", "Del", "ete"],
["", "Enter", " stop"],
]),
Self::ArrangerScene => Self::command(&[
["re", "n", "ame"],
["", "Del", "ete"],
["", "Enter", " launch"],
]),
Self::PhrasePool => Self::command(&[
["", "a", "ppend"],
["", "i", "nsert"],
["", "d", "uplicate"],
["", "Del", "ete"],
["", "c", "olor"],
["re", "n", "ame"],
["leng", "t", "h"],
["", ",.", "move"],
["", "+-", "resize view"],
]),
Self::PhraseView => Self::command(&[
["", "enter", " edit"],
["", "arrows/pgup/pgdn", " scroll"],
["", "+=", "zoom"],
]),
Self::PhraseEdit => Self::command(&[
["", "esc", " exit"],
["", "a", "ppend"],
["", "s", "et"],
["", "][", "length"],
["", "+-", "zoom"],
]),
_ => 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)
}
}
//pub fn arranger_menu_bar () -> MenuBar {
//use ArrangerCommand as Cmd;
//use ArrangerCommand as Edit;
//use ArrangerFocus as Focus;
//use ArrangerTrackCommand as Track;
//use ArrangerClipCommand as Clip;
//use ArrangerSceneCommand as Scene;
//use TransportCommand as Transport;
//MenuBar::new()
//.add({
//use ArrangerCommand::*;
//Menu::new("File")
//.cmd("n", "New project", ArrangerViewCommand::Arranger(New))
//.cmd("l", "Load project", ArrangerViewCommand::Arranger(Load))
//.cmd("s", "Save project", ArrangerViewCommand::Arranger(Save))
//})
//.add({
//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 ArrangerCommand::*;
//Menu::new("Track")
//.cmd("a", "Append new", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("i", "Insert new", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("n", "Rename", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("d", "Delete", ArrangerViewCommand::Arranger(AddTrack))
//.cmd(">", "Move up", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("<", "Move down", ArrangerViewCommand::Arranger(AddTrack))
//})
//.add({
//use ArrangerCommand::*;
//Menu::new("Scene")
//.cmd("a", "Append new", ArrangerViewCommand::Arranger(AddScene))
//.cmd("i", "Insert new", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("n", "Rename", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("d", "Delete", ArrangerViewCommand::Arranger(AddTrack))
//.cmd(">", "Move up", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("<", "Move down", ArrangerViewCommand::Arranger(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

@ -1,289 +0,0 @@
use crate::*;
#[derive(Clone)]
pub enum ArrangerViewCommand {
Focus(FocusCommand),
Edit(ArrangerCommand),
Select(ArrangerFocus),
Zoom(usize),
Transport(TransportViewCommand),
Phrases(PhrasePoolViewCommand),
Editor(PhraseEditorCommand),
EditPhrase(Option<Arc<RwLock<Phrase>>>),
}
/// Handle top-level events in standalone arranger.
impl Handle<Tui> for ArrangerView<Tui> {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
ArrangerViewCommand::execute_with_state(self, i)
}
}
impl InputToCommand<Tui, ArrangerView<Tui>> for ArrangerViewCommand {
fn input_to_command (view: &ArrangerView<Tui>, input: &TuiInput) -> Option<Self> {
use FocusCommand::*;
use ArrangerViewCommand::*;
Some(match input.event() {
key!(KeyCode::Tab) => Focus(Next),
key!(Shift-KeyCode::Tab) => Focus(Prev),
key!(KeyCode::BackTab) => Focus(Prev),
key!(Shift-KeyCode::BackTab) => Focus(Prev),
key!(KeyCode::Up) => Focus(Up),
key!(KeyCode::Down) => Focus(Down),
key!(KeyCode::Left) => Focus(Left),
key!(KeyCode::Right) => Focus(Right),
key!(KeyCode::Enter) => Focus(Enter),
key!(KeyCode::Esc) => Focus(Exit),
key!(KeyCode::Char(' ')) => {
Transport(TransportViewCommand::Transport(TransportCommand::Play(None)))
},
_ => match view.focused() {
ArrangerViewFocus::Transport => Transport(
TransportViewCommand::input_to_command(&view.sequencer.transport, input)?
),
ArrangerViewFocus::PhraseEditor => Editor(
PhraseEditorCommand::input_to_command(&view.sequencer.editor, input)?
),
ArrangerViewFocus::PhrasePool => match input.event() {
key!(KeyCode::Char('e')) => EditPhrase(
Some(view.sequencer.phrases.phrase().clone())
),
_ => Phrases(
PhrasePoolViewCommand::input_to_command(&view.sequencer.phrases, input)?
)
},
ArrangerViewFocus::Arranger => {
use ArrangerFocus as Focus;
use ArrangerCommand as Model;
use ArrangerTrackCommand as Track;
use ArrangerClipCommand as Clip;
use ArrangerSceneCommand as Scene;
match input.event() {
key!(KeyCode::Char('e')) => EditPhrase(
view.selected_phrase()
),
_ => match input.event() {
// FIXME: boundary conditions
key!(KeyCode::Up) => match view.selected {
ArrangerFocus::Mix => return None,
ArrangerFocus::Track(t) => return None,
ArrangerFocus::Scene(s) => Select(Focus::Scene(s - 1)),
ArrangerFocus::Clip(t, s) => Select(Focus::Clip(t, s - 1)),
},
key!(KeyCode::Down) => match view.selected {
ArrangerFocus::Mix => Select(Focus::Scene(0)),
ArrangerFocus::Track(t) => Select(Focus::Clip(t, 0)),
ArrangerFocus::Scene(s) => Select(Focus::Scene(s + 1)),
ArrangerFocus::Clip(t, s) => Select(Focus::Clip(t, s + 1)),
},
key!(KeyCode::Left) => match view.selected {
ArrangerFocus::Mix => return None,
ArrangerFocus::Track(t) => Select(Focus::Track(t - 1)),
ArrangerFocus::Scene(s) => return None,
ArrangerFocus::Clip(t, s) => Select(Focus::Clip(t - 1, s)),
},
key!(KeyCode::Right) => match view.selected {
ArrangerFocus::Mix => return None,
ArrangerFocus::Track(t) => Select(Focus::Track(t + 1)),
ArrangerFocus::Scene(s) => Select(Focus::Clip(0, s)),
ArrangerFocus::Clip(t, s) => Select(Focus::Clip(t, s - 1)),
},
key!(KeyCode::Char('+')) => Zoom(0),
key!(KeyCode::Char('=')) => Zoom(0),
key!(KeyCode::Char('_')) => Zoom(0),
key!(KeyCode::Char('-')) => Zoom(0),
key!(KeyCode::Char('`')) => { todo!("toggle view mode") },
key!(KeyCode::Char(',')) => match view.selected {
ArrangerFocus::Mix => Zoom(0),
ArrangerFocus::Track(t) => Edit(Model::Track(Track::Swap(t, t - 1))),
ArrangerFocus::Scene(s) => Edit(Model::Scene(Scene::Swap(s, s - 1))),
ArrangerFocus::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))),
},
key!(KeyCode::Char('.')) => match view.selected {
ArrangerFocus::Mix => Zoom(0),
ArrangerFocus::Track(t) => Edit(Model::Track(Track::Swap(t, t + 1))),
ArrangerFocus::Scene(s) => Edit(Model::Scene(Scene::Swap(s, s + 1))),
ArrangerFocus::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))),
},
key!(KeyCode::Char('<')) => match view.selected {
ArrangerFocus::Mix => Zoom(0),
ArrangerFocus::Track(t) => Edit(Model::Track(Track::Swap(t, t - 1))),
ArrangerFocus::Scene(s) => Edit(Model::Scene(Scene::Swap(s, s - 1))),
ArrangerFocus::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))),
},
key!(KeyCode::Char('>')) => match view.selected {
ArrangerFocus::Mix => Zoom(0),
ArrangerFocus::Track(t) => Edit(Model::Track(Track::Swap(t, t + 1))),
ArrangerFocus::Scene(s) => Edit(Model::Scene(Scene::Swap(s, s + 1))),
ArrangerFocus::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))),
},
key!(KeyCode::Enter) => match view.selected {
ArrangerFocus::Mix => return None,
ArrangerFocus::Track(t) => return None,
ArrangerFocus::Scene(s) => Edit(Model::Scene(Scene::Play(s))),
ArrangerFocus::Clip(t, s) => return None,
},
key!(KeyCode::Delete) => match view.selected {
ArrangerFocus::Mix => Edit(Model::Clear),
ArrangerFocus::Track(t) => Edit(Model::Track(Track::Delete(t))),
ArrangerFocus::Scene(s) => Edit(Model::Scene(Scene::Delete(s))),
ArrangerFocus::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))),
},
key!(KeyCode::Char('c')) => Edit(Model::Clip(Clip::RandomColor)),
key!(KeyCode::Char('s')) => match view.selected {
ArrangerFocus::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))),
_ => return None,
},
key!(KeyCode::Char('g')) => match view.selected {
ArrangerFocus::Clip(t, s) => Edit(Model::Clip(Clip::Get(t, s))),
_ => return None,
},
key!(Ctrl-KeyCode::Char('a')) => Edit(Model::Scene(Scene::Add)),
key!(Ctrl-KeyCode::Char('t')) => Edit(Model::Track(Track::Add)),
key!(KeyCode::Char('l')) => Edit(Model::Clip(Clip::SetLoop(false))),
_ => return None
}
}
}
}
})
}
}
impl<E: Engine> Command<ArrangerView<E>> for ArrangerViewCommand {
fn execute (self, view: &mut ArrangerView<E>) -> Perhaps<Self> {
let undo = match self {
Self::Focus(cmd) =>
delegate(cmd, Self::Focus, view),
Self::Phrases(cmd) =>
delegate(cmd, Self::Phrases, &mut view.sequencer.phrases),
Self::Editor(cmd) =>
delegate(cmd, Self::Editor, &mut view.sequencer.editor),
Self::Transport(cmd) =>
delegate(cmd, Self::Transport, &mut view.sequencer.transport),
Self::Zoom(zoom) => {
todo!();
},
Self::Select(selected) => {
view.selected = selected;
Ok(None)
},
Self::Edit(command) => {
return Ok(command.execute(&mut view.model)?.map(Self::Edit))
},
Self::EditPhrase(phrase) => {
view.sequencer.editor.phrase = phrase.clone();
view.focus(ArrangerViewFocus::PhraseEditor);
view.focus_enter();
Ok(None)
}
}?;
view.show_phrase();
view.update_status();
return Ok(undo);
}
}
//pub fn phrase_next (&mut self) {
//if let ArrangerFocus::Clip(track, scene) = self.selected {
//if let Some(ref mut phrase) = self.model.scenes[scene].clips[track] {
//let phrases = self.model.phrases.read().unwrap();
//let index = phrases.index_of(&*phrase.read().unwrap());
//if let Some(index) = index {
//if index < phrases.len().saturating_sub(1) {
//*phrase = phrases[index + 1].clone();
//}
//}
//}
//}
//}
//pub fn phrase_prev (&mut self) {
//if let ArrangerFocus::Clip(track, scene) = self.selected {
//if let Some(ref mut phrase) = self.model.scenes[scene].clips[track] {
//let phrases = self.model.phrases.read().unwrap();
//let index = phrases.index_of(&*phrase.read().unwrap());
//if let Some(index) = index {
//if index > 0 {
//*phrase = phrases[index - 1].clone();
//}
//}
//}
//}
//}
//pub fn phrase_get (&mut self) {
//if let ArrangerFocus::Clip(track, scene) = self.selected {
//if let Some(phrase) = &self.model.scenes[scene].clips[track] {
//let mut phrases = self.model.phrases.write().unwrap();
//if let Some(index) = &*phrases.index_of(&*phrase.read().unwrap()) {
//self.model.phrase = index;
//}
//}
//}
//}
///// Focus the editor with the current phrase
//pub fn edit_phrase (&mut self) {
//if self.arrangement.selected.is_clip() && self.arrangement.phrase().is_none() {
//self.sequencer.phrases.append_new(None, Some(self.next_color().into()));
//self.arrangement.phrase_put();
//}
//self.show_phrase();
//self.focus(ArrangerViewFocus::PhraseEditor);
//self.sequencer.editor.entered = true;
//}
//pub fn next_color (&self) -> ItemColor {
//if let ArrangerFocus::Clip(track, scene) = self.arrangement.selected {
//let track_color = self.arrangement.model.tracks[track].color;
//let scene_color = self.arrangement.model.scenes[scene].color;
//track_color.mix(scene_color, 0.5).mix(ItemColor::random(), 0.25)
//} else {
//panic!("could not compute next color")
//}
//}
//pub fn phrase_del (&mut self) {
//let track_index = self.selected.track();
//let scene_index = self.selected.scene();
//track_index
//.and_then(|index|self.model.tracks.get_mut(index).map(|track|(index, track)))
//.map(|(track_index, _)|scene_index
//.and_then(|index|self.model.scenes.get_mut(index))
//.map(|scene|scene.clips[track_index] = None));
//}
//pub fn phrase_put (&mut self) {
//if let ArrangerFocus::Clip(track, scene) = self.selected {
//self.model.scenes[scene].clips[track] = self.selected_phrase().clone();
//}
//}
//pub fn selected_scene (&self) -> Option<&ArrangerScene> {
//self.selected.scene().map(|s|self.model.scenes.get(s)).flatten()
//}
//pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
//self.selected.scene().map(|s|self.model.scenes.get_mut(s)).flatten()
//}
//pub fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>> {
//self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
//}

View file

@ -1,176 +0,0 @@
use crate::*;
/// Sections in the arranger app that may be focused
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum ArrangerViewFocus {
/// The transport (toolbar) is focused
Transport,
/// The arrangement (grid) is focused
Arranger,
/// The phrase list (pool) is focused
PhrasePool,
/// The phrase editor (sequencer) is focused
PhraseEditor,
}
/// Focus layout of arranger app
impl FocusGrid for AppView<Tui, ArrangerView<Tui>, ArrangerViewCommand> {
type Item = AppViewFocus<ArrangerViewFocus>;
fn cursor (&self) -> (usize, usize) {
self.cursor
}
fn cursor_mut (&mut self) -> &mut (usize, usize) {
&mut self.cursor
}
fn focus_enter (&mut self) {
use AppViewFocus::*;
use ArrangerViewFocus::*;
let focused = self.focused();
if !self.entered {
self.entered = focused == Content(Arranger);
self.app.sequencer.editor.entered = focused == Content(PhraseEditor);
self.app.sequencer.phrases.entered = focused == Content(PhrasePool);
}
}
fn focus_exit (&mut self) {
if self.entered {
self.entered = false;
self.app.sequencer.editor.entered = false;
self.app.sequencer.phrases.entered = false;
}
}
fn entered (&self) -> Option<Self::Item> {
if self.entered {
Some(self.focused())
} else {
None
}
}
fn layout (&self) -> &[&[Self::Item]] {
use AppViewFocus::*;
use ArrangerViewFocus::*;
&[
&[Menu, Menu ],
&[Content(Transport), Content(Transport) ],
&[Content(Arranger), Content(Arranger) ],
&[Content(PhrasePool), Content(PhraseEditor)],
]
}
fn update_focus (&mut self) {
use AppViewFocus::*;
use ArrangerViewFocus::*;
let focused = self.focused();
self.app.focused = focused == Content(Arranger);
self.app.sequencer.transport.focused = focused == Content(Transport);
self.app.sequencer.phrases.focused = focused == Content(PhrasePool);
self.app.sequencer.editor.focused = focused == Content(PhraseEditor);
if let Some(status_bar) = self.status_bar {
status_bar.update(&self.app)
}
}
}
#[derive(PartialEq, Clone, Copy)]
/// Represents the current user selection in the arranger
pub enum ArrangerFocus {
/// 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 ArrangerFocus {
pub fn description <E: Engine> (
&self,
tracks: &Vec<ArrangerTrack>,
scenes: &Vec<ArrangerScene>,
) -> String {
format!("Selected: {}", match self {
Self::Mix => format!("Everything"),
Self::Track(t) => match tracks.get(*t) {
Some(track) => format!("T{t}: {}", &track.name.read().unwrap()),
None => format!("T??"),
},
Self::Scene(s) => match scenes.get(*s) {
Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()),
None => format!("S??"),
},
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
(Some(_), Some(scene)) => match scene.clip(*t) {
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
None => format!("T{t} S{s}: Empty")
},
_ => format!("T{t} S{s}: Empty"),
}
})
}
pub fn is_mix (&self) -> bool {
match self { Self::Mix => true, _ => false }
}
pub fn is_track (&self) -> bool {
match self { Self::Track(_) => true, _ => false }
}
pub fn is_scene (&self) -> bool {
match self { Self::Scene(_) => true, _ => false }
}
pub fn is_clip (&self) -> bool {
match self { Self::Clip(_, _) => true, _ => false }
}
pub fn track (&self) -> Option<usize> {
use ArrangerFocus::*;
match self {
Clip(t, _) => Some(*t),
Track(t) => Some(*t),
_ => None
}
}
pub fn scene (&self) -> Option<usize> {
use ArrangerFocus::*;
match self {
Clip(_, s) => Some(*s),
Scene(s) => Some(*s),
_ => None
}
}
}
//pub fn track_next (&mut self, last_track: usize) {
//use ArrangerFocus::*;
//*self = match self {
//Mix => Track(0),
//Track(t) => Track(last_track.min(*t + 1)),
//Scene(s) => Clip(0, *s),
//Clip(t, s) => Clip(last_track.min(*t + 1), *s),
//}
//}
//pub fn track_prev (&mut self) {
//use ArrangerFocus::*;
//*self = match self {
//Mix => Mix,
//Scene(s) => Scene(*s),
//Track(t) => if *t == 0 { Mix } else { Track(*t - 1) },
//Clip(t, s) => if *t == 0 { Scene(*s) } else { Clip(t.saturating_sub(1), *s) }
//}
//}
//pub fn scene_next (&mut self, last_scene: usize) {
//use ArrangerFocus::*;
//*self = match self {
//Mix => Scene(0),
//Track(t) => Clip(*t, 0),
//Scene(s) => Scene(last_scene.min(*s + 1)),
//Clip(t, s) => Clip(*t, last_scene.min(*s + 1)),
//}
//}
//pub fn scene_prev (&mut self) {
//use ArrangerFocus::*;
//*self = match self {
//Mix => Mix,
//Track(t) => Track(*t),
//Scene(s) => if *s == 0 { Mix } else { Scene(*s - 1) },
//Clip(t, s) => if *s == 0 { Track(*t) } else { Clip(*t, s.saturating_sub(1)) }
//}
//}

View file

@ -1,194 +0,0 @@
use crate::*;
pub fn arranger_content_horizontal (
view: &ArrangerView<Tui>,
) -> impl Widget<Engine = Tui> + use<'_> {
let focused = view.focused;
let _tracks = view.model.tracks();
lay!(
focused.then_some(Background(TuiTheme::border_bg())),
row!(
// name
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks, selected) = self;
//let yellow = Some(Style::default().yellow().bold().not_dim());
//let white = Some(Style::default().white().bold().not_dim());
//let area = to.area();
//let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()];
//let offset = 0; // track scroll offset
//for y in 0..area.h() {
//if y == 0 {
//to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2 + offset;
//if let Some(track) = tracks.get(index) {
//let selected = selected.track() == Some(index);
//let style = if selected { yellow } else { white };
//to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?;
//to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?;
//}
//}
//}
//Ok(Some(area))
}),
// monitor
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks) = self;
//let mut area = to.area();
//let on = Some(Style::default().not_dim().green().bold());
//let off = Some(DIM);
//area.x += 1;
//for y in 0..area.h() {
//if y == 0 {
////" MON ".blit(to.buffer, area.x, area.y + y, style2)?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2;
//if let Some(track) = tracks.get(index) {
//let style = if track.monitoring { on } else { off };
//to.blit(&" MON ", area.x(), area.y() + y, style)?;
//} else {
//area.height = y;
//break
//}
//}
//}
//area.width = 4;
//Ok(Some(area))
}),
// record
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks) = self;
//let mut area = to.area();
//let on = Some(Style::default().not_dim().red().bold());
//let off = Some(Style::default().dim());
//area.x += 1;
//for y in 0..area.h() {
//if y == 0 {
////" REC ".blit(to.buffer, area.x, area.y + y, style2)?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2;
//if let Some(track) = tracks.get(index) {
//let style = if track.recording { on } else { off };
//to.blit(&" REC ", area.x(), area.y() + y, style)?;
//} else {
//area.height = y;
//break
//}
//}
//}
//area.width = 4;
//Ok(Some(area))
}),
// overdub
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks) = self;
//let mut area = to.area();
//let on = Some(Style::default().not_dim().yellow().bold());
//let off = Some(Style::default().dim());
//area.x = area.x + 1;
//for y in 0..area.h() {
//if y == 0 {
////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2;
//if let Some(track) = tracks.get(index) {
//to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub {
//on
//} else {
//off
//})?;
//} else {
//area.height = y;
//break
//}
//}
//}
//area.width = 4;
//Ok(Some(area))
}),
// erase
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks) = self;
//let mut area = to.area();
//let off = Some(Style::default().dim());
//area.x = area.x + 1;
//for y in 0..area.h() {
//if y == 0 {
////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2;
//if let Some(_) = tracks.get(index) {
//to.blit(&" DEL ", area.x(), area.y() + y, off)?;
//} else {
//area.height = y;
//break
//}
//}
//}
//area.width = 4;
//Ok(Some(area))
}),
// gain
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks) = self;
//let mut area = to.area();
//let off = Some(Style::default().dim());
//area.x = area.x() + 1;
//for y in 0..area.h() {
//if y == 0 {
////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2;
//if let Some(_) = tracks.get(index) {
//to.blit(&" +0.0 ", area.x(), area.y() + y, off)?;
//} else {
//area.height = y;
//break
//}
//}
//}
//area.width = 7;
//Ok(Some(area))
}),
// scenes
CustomWidget::new(|_|{todo!()}, |to: &mut TuiOutput|{
let [x, y, _, height] = to.area();
let mut x2 = 0;
Ok(for (scene_index, scene) in view.model.scenes().iter().enumerate() {
let active_scene = view.selected.scene() == Some(scene_index);
let sep = Some(if active_scene {
Style::default().yellow().not_dim()
} else {
Style::default().dim()
});
for y in y+1..y+height {
to.blit(&"", x + x2, y, sep);
}
let name = scene.name.read().unwrap();
let mut x3 = name.len() as u16;
to.blit(&*name, x + x2, y, sep);
for (i, clip) in scene.clips.iter().enumerate() {
let active_track = view.selected.track() == Some(i);
if let Some(clip) = clip {
let y2 = y + 2 + i as u16 * 2;
let label = format!("{}", clip.read().unwrap().name);
to.blit(&label, x + x2, y2, Some(if active_track && active_scene {
Style::default().not_dim().yellow().bold()
} else {
Style::default().not_dim()
}));
x3 = x3.max(label.len() as u16)
}
}
x2 = x2 + x3 + 1;
})
}),
)
)
}

View file

@ -1,200 +0,0 @@
use crate::*;
fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> {
let mut widths = vec![];
let mut total = 0;
for track in tracks.iter() {
let width = track.width;
widths.push((width, total));
total += width;
}
widths.push((0, total));
widths
}
pub fn arranger_content_vertical (
view: &ArrangerView<Tui>,
factor: usize
) -> impl Widget<Engine = Tui> + use<'_> {
let clock = view.model.clock();
let tracks = view.model.tracks();
let scenes = view.model.scenes();
let cols = track_widths(tracks);
let rows = ArrangerScene::ppqs(scenes, factor);
let bg = view.color;
let clip_bg = TuiTheme::border_bg();
let sep_fg = TuiTheme::separator_fg(false);
let header_h = 3u16;//5u16;
let scenes_w = 3 + ArrangerScene::longest_name(scenes) as u16; // x of 1st track
let arrangement = Layers::new(move |add|{
let rows: &[(usize, usize)] = rows.as_ref();
let cols: &[(usize, usize)] = cols.as_ref();
let any_size = |_|Ok(Some([0,0]));
// column separators
add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{
let style = Some(Style::default().fg(sep_fg));
Ok(for x in cols.iter().map(|col|col.1) {
let x = scenes_w + to.area().x() + x as u16;
for y in to.area().y()..to.area().y2() { to.blit(&"", x, y, style); }
})
}))?;
// row separators
add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{
Ok(for y in rows.iter().map(|row|row.1) {
let y = to.area().y() + (y / PPQ) as u16 + 1;
if y >= to.buffer.area.height { break }
for x in to.area().x()..to.area().x2().saturating_sub(2) {
if x < to.buffer.area.x && y < to.buffer.area.y {
let cell = to.buffer.get_mut(x, y);
cell.modifier = Modifier::UNDERLINED;
cell.underline_color = sep_fg;
}
}
})
}))?;
// track titles
let header = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{
// name and width of track
let name = track.name.read().unwrap();
let player = &track.player;
let max_w = w.saturating_sub(1).min(name.len()).max(2);
let name = format!("{}", &name[0..max_w]);
let name = TuiStyle::bold(name, true);
// beats elapsed
let elapsed = if let Some((_, Some(phrase))) = player.phrase.as_ref() {
let length = phrase.read().unwrap().length;
let elapsed = player.pulses_since_start().unwrap();
let elapsed = clock.timebase().format_beats_1_short(
(elapsed as usize % length) as f64
);
format!("▎+{elapsed:>}")
} else {
String::from("")
};
// beats until switchover
let until_next = player.next_phrase.as_ref().map(|(t, _)|{
let target = t.pulse.get();
let current = clock.current.pulse.get();
if target > current {
let remaining = target - current;
format!("▎-{:>}", clock.timebase().format_beats_0_short(remaining))
} else {
String::new()
}
}).unwrap_or(String::from(""));
// name of active MIDI input
let input = format!("▎>{}", track.player.midi_inputs.get(0)
.map(|port|port.short_name())
.transpose()?
.unwrap_or("(none)".into()));
// name of active MIDI output
let output = format!("▎<{}", track.player.midi_outputs.get(0)
.map(|port|port.short_name())
.transpose()?
.unwrap_or("(none)".into()));
col!(name, /*input, output,*/ until_next, elapsed)
.min_xy(w as u16, header_h)
.bg(track.color.rgb)
.push_x(scenes_w)
});
// tracks and scenes
let content = col!(
// scenes:
(scene, pulses) in scenes.iter().zip(rows.iter().map(|row|row.0)) => {
let height = 1.max((pulses / PPQ) as u16);
let playing = scene.is_playing(tracks);
Stack::right(move |add| {
// scene title:
add(&row!(
if playing { "" } else { " " },
TuiStyle::bold(scene.name.read().unwrap().as_str(), true),
).fixed_xy(scenes_w, height).bg(scene.color.rgb))?;
// clip per track:
Ok(for (track, w) in cols.iter().map(|col|col.0).enumerate() {
add(&Layers::new(move |add|{
let mut bg = clip_bg;
match (tracks.get(track), scene.clips.get(track)) {
(Some(track), Some(Some(phrase))) => {
let name = &(phrase as &Arc<RwLock<Phrase>>).read().unwrap().name;
let name = format!("{}", name);
let max_w = name.len().min((w as usize).saturating_sub(2));
let color = phrase.read().unwrap().color;
add(&name.as_str()[0..max_w].push_x(1).fixed_x(w as u16))?;
bg = color.dark.rgb;
if let Some((_, Some(ref playing))) = track.player.phrase {
if *playing.read().unwrap() == *phrase.read().unwrap() {
bg = color.light.rgb
}
};
},
_ => {}
};
add(&Background(bg))
}).fixed_xy(w as u16, height))?;
})
}).fixed_y(height)
}
).fixed_y((view.size.h() as u16).saturating_sub(header_h));
// full grid with header and footer
add(&col!(header, content))?;
// cursor
add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{
let area = to.area();
let focused = view.focused;
let selected = view.selected;
let get_track_area = |t: usize| [
scenes_w + area.x() + cols[t].1 as u16, area.y(),
cols[t].0 as u16, area.h(),
];
let get_scene_area = |s: usize| [
area.x(), header_h + area.y() + (rows[s].1 / PPQ) as u16,
area.w(), (rows[s].0 / PPQ) as u16
];
let get_clip_area = |t: usize, s: usize| [
scenes_w + area.x() + cols[t].1 as u16,
header_h + area.y() + (rows[s].1/PPQ) as u16,
cols[t].0 as u16,
(rows[s].0 / PPQ) as u16
];
let mut track_area: Option<[u16;4]> = None;
let mut scene_area: Option<[u16;4]> = None;
let mut clip_area: Option<[u16;4]> = None;
let area = match selected {
ArrangerFocus::Mix => area,
ArrangerFocus::Track(t) => {
track_area = Some(get_track_area(t));
area
},
ArrangerFocus::Scene(s) => {
scene_area = Some(get_scene_area(s));
area
},
ArrangerFocus::Clip(t, s) => {
track_area = Some(get_track_area(t));
scene_area = Some(get_scene_area(s));
clip_area = Some(get_clip_area(t, s));
area
},
};
let bg = TuiTheme::border_bg();
if let Some([x, y, width, height]) = track_area {
to.fill_fg([x, y, 1, height], bg);
to.fill_fg([x + width, y, 1, height], bg);
}
if let Some([_, y, _, height]) = scene_area {
to.fill_ul([area.x(), y - 1, area.w(), 1], bg);
to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg);
}
Ok(if focused {
to.render_in(if let Some(clip_area) = clip_area { clip_area }
else if let Some(track_area) = track_area { track_area.clip_h(header_h) }
else if let Some(scene_area) = scene_area { scene_area.clip_w(scenes_w) }
else { area.clip_w(scenes_w).clip_h(header_h) }, &CORNERS)?
})
}))
}).bg(bg.rgb);
let color = TuiTheme::title_fg(view.focused);
let size = format!("{}x{}", view.size.w(), view.size.h());
let lower_right = TuiStyle::fg(size, color).pull_x(1).align_se().fill_xy();
lay!(arrangement, lower_right)
}

View file

@ -166,3 +166,89 @@ impl Content for Track<Tui> {
}
}
}
impl Handle<Tui> for Mixer<Tui> {
fn handle (&mut self, engine: &TuiInput) -> Perhaps<bool> {
if let TuiEvent::Input(crossterm::event::Event::Key(event)) = engine.event() {
match event.code {
//KeyCode::Char('c') => {
//if event.modifiers == KeyModifiers::CONTROL {
//self.exit();
//}
//},
KeyCode::Down => {
self.selected_track = (self.selected_track + 1) % self.tracks.len();
println!("{}", self.selected_track);
return Ok(Some(true))
},
KeyCode::Up => {
if self.selected_track == 0 {
self.selected_track = self.tracks.len() - 1;
} else {
self.selected_track -= 1;
}
println!("{}", self.selected_track);
return Ok(Some(true))
},
KeyCode::Left => {
if self.selected_column == 0 {
self.selected_column = 6
} else {
self.selected_column -= 1;
}
return Ok(Some(true))
},
KeyCode::Right => {
if self.selected_column == 6 {
self.selected_column = 0
} else {
self.selected_column += 1;
}
return Ok(Some(true))
},
_ => {
println!("\n{event:?}");
}
}
}
Ok(None)
}
}
impl Handle<Tui> for Track<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
match from.event() {
//, NONE, "chain_cursor_up", "move cursor up", || {
key!(KeyCode::Up) => {
Ok(Some(true))
},
// , NONE, "chain_cursor_down", "move cursor down", || {
key!(KeyCode::Down) => {
Ok(Some(true))
},
// Left, NONE, "chain_cursor_left", "move cursor left", || {
key!(KeyCode::Left) => {
//if let Some(track) = app.arranger.track_mut() {
//track.device = track.device.saturating_sub(1);
//return Ok(true)
//}
Ok(Some(true))
},
// , NONE, "chain_cursor_right", "move cursor right", || {
key!(KeyCode::Right) => {
//if let Some(track) = app.arranger.track_mut() {
//track.device = (track.device + 1).min(track.devices.len().saturating_sub(1));
//return Ok(true)
//}
Ok(Some(true))
},
// , NONE, "chain_mode_switch", "switch the display mode", || {
key!(KeyCode::Char('`')) => {
//app.chain_mode = !app.chain_mode;
Ok(Some(true))
},
_ => Ok(None)
}
}
}

View file

@ -1,87 +0,0 @@
use crate::*;
impl Handle<Tui> for Mixer<Tui> {
fn handle (&mut self, engine: &TuiInput) -> Perhaps<bool> {
if let TuiEvent::Input(crossterm::event::Event::Key(event)) = engine.event() {
match event.code {
//KeyCode::Char('c') => {
//if event.modifiers == KeyModifiers::CONTROL {
//self.exit();
//}
//},
KeyCode::Down => {
self.selected_track = (self.selected_track + 1) % self.tracks.len();
println!("{}", self.selected_track);
return Ok(Some(true))
},
KeyCode::Up => {
if self.selected_track == 0 {
self.selected_track = self.tracks.len() - 1;
} else {
self.selected_track -= 1;
}
println!("{}", self.selected_track);
return Ok(Some(true))
},
KeyCode::Left => {
if self.selected_column == 0 {
self.selected_column = 6
} else {
self.selected_column -= 1;
}
return Ok(Some(true))
},
KeyCode::Right => {
if self.selected_column == 6 {
self.selected_column = 0
} else {
self.selected_column += 1;
}
return Ok(Some(true))
},
_ => {
println!("\n{event:?}");
}
}
}
Ok(None)
}
}
impl Handle<Tui> for Track<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
match from.event() {
//, NONE, "chain_cursor_up", "move cursor up", || {
key!(KeyCode::Up) => {
Ok(Some(true))
},
// , NONE, "chain_cursor_down", "move cursor down", || {
key!(KeyCode::Down) => {
Ok(Some(true))
},
// Left, NONE, "chain_cursor_left", "move cursor left", || {
key!(KeyCode::Left) => {
//if let Some(track) = app.arranger.track_mut() {
//track.device = track.device.saturating_sub(1);
//return Ok(true)
//}
Ok(Some(true))
},
// , NONE, "chain_cursor_right", "move cursor right", || {
key!(KeyCode::Right) => {
//if let Some(track) = app.arranger.track_mut() {
//track.device = (track.device + 1).min(track.devices.len().saturating_sub(1));
//return Ok(true)
//}
Ok(Some(true))
},
// , NONE, "chain_mode_switch", "switch the display mode", || {
key!(KeyCode::Char('`')) => {
//app.chain_mode = !app.chain_mode;
Ok(Some(true))
},
_ => Ok(None)
}
}
}

View file

@ -351,3 +351,157 @@ pub(crate) fn keys_vert () -> Buffer {
const NTH_OCTAVE: [&'static str; 11] = [
"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8",
];
#[derive(Clone, PartialEq)]
pub enum PhraseEditorCommand {
// TODO: 1-9 seek markers that by default start every 8th of the phrase
ToggleDirection,
EnterEditMode,
ExitEditMode,
NoteAppend,
NoteSet,
NoteCursorSet(usize),
NoteLengthSet(usize),
NoteScrollSet(usize),
TimeCursorSet(usize),
TimeScrollSet(usize),
TimeZoomSet(usize),
}
impl Handle<Tui> for PhraseEditor<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
PhraseEditorCommand::execute_with_state(self, from)
}
}
impl InputToCommand<Tui, PhraseEditor<Tui>> for PhraseEditorCommand {
fn input_to_command (state: &PhraseEditor<Tui>, from: &TuiInput) -> Option<Self> {
use PhraseEditorCommand::*;
Some(match from.event() {
key!(KeyCode::Char('`')) => ToggleDirection,
key!(KeyCode::Enter) => EnterEditMode,
key!(KeyCode::Esc) => ExitEditMode,
key!(KeyCode::Char('[')) => NoteLengthSet(0),
key!(KeyCode::Char(']')) => NoteLengthSet(0),
key!(KeyCode::Char('a')) => NoteAppend,
key!(KeyCode::Char('s')) => NoteSet,
key!(KeyCode::Char('-')) => TimeZoomSet(0),
key!(KeyCode::Char('_')) => TimeZoomSet(0),
key!(KeyCode::Char('=')) => TimeZoomSet(0),
key!(KeyCode::Char('+')) => TimeZoomSet(0),
key!(KeyCode::PageUp) => NoteScrollSet(0),
key!(KeyCode::PageDown) => NoteScrollSet(0),
key!(KeyCode::Up) => match state.entered {
true => NoteCursorSet(0),
false => NoteScrollSet(0),
},
key!(KeyCode::Down) => match state.entered {
true => NoteCursorSet(0),
false => NoteScrollSet(0),
},
key!(KeyCode::Left) => match state.entered {
true => TimeCursorSet(0),
false => TimeScrollSet(0),
},
key!(KeyCode::Right) => match state.entered {
true => TimeCursorSet(0),
false => TimeScrollSet(0),
},
_ => return None
})
}
}
impl<E: Engine> Command<PhraseEditor<E>> for PhraseEditorCommand {
//fn translate (self, state: &PhraseEditor<E>) -> Self {
//use PhraseEditorCommand::*;
//match self {
//GoUp => match state.entered { true => NoteCursorInc, false => NoteScrollInc, },
//GoDown => match state.entered { true => NoteCursorDec, false => NoteScrollDec, },
//GoLeft => match state.entered { true => TimeCursorDec, false => TimeScrollDec, },
//GoRight => match state.entered { true => TimeCursorInc, false => TimeScrollInc, },
//_ => self
//}
//}
fn execute (self, state: &mut PhraseEditor<E>) -> Perhaps<Self> {
use PhraseEditorCommand::*;
match self.translate(state) {
ToggleDirection => {
state.mode = !state.mode;
},
EnterEditMode => {
state.entered = true;
},
ExitEditMode => {
state.entered = false;
},
TimeZoomOut => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().scale = next_note_length(scale)
},
TimeZoomIn => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().scale = prev_note_length(scale)
},
TimeCursorDec => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().point_dec(scale);
},
TimeCursorInc => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().point_inc(scale);
},
TimeScrollDec => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().start_dec(scale);
},
TimeScrollInc => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().start_inc(scale);
},
NoteCursorDec => {
let mut axis = state.note_axis.write().unwrap();
axis.point_inc(1);
if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } }
},
NoteCursorInc => {
let mut axis = state.note_axis.write().unwrap();
axis.point_dec(1);
if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } }
},
NoteScrollDec => {
state.note_axis.write().unwrap().start_inc(1);
},
NoteScrollInc => {
state.note_axis.write().unwrap().start_dec(1);
},
NoteLengthDec => {
state.note_len = prev_note_length(state.note_len)
},
NoteLengthInc => {
state.note_len = next_note_length(state.note_len)
},
NotePageUp => {
let mut axis = state.note_axis.write().unwrap();
axis.start_dec(3);
axis.point_dec(3);
},
NotePageDown => {
let mut axis = state.note_axis.write().unwrap();
axis.start_inc(3);
axis.point_inc(3);
},
NoteAppend => {
if state.entered {
state.put();
state.time_cursor_advance();
}
},
NoteSet => {
if state.entered { state.put(); }
},
_ => unreachable!()
}
Ok(None)
}
}

View file

@ -1,155 +0,0 @@
use crate::*;
#[derive(Clone, PartialEq)]
pub enum PhraseEditorCommand {
// TODO: 1-9 seek markers that by default start every 8th of the phrase
ToggleDirection,
EnterEditMode,
ExitEditMode,
NoteAppend,
NoteSet,
NoteCursorSet(usize),
NoteLengthSet(usize),
NoteScrollSet(usize),
TimeCursorSet(usize),
TimeScrollSet(usize),
TimeZoomSet(usize),
}
impl Handle<Tui> for PhraseEditor<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
PhraseEditorCommand::execute_with_state(self, from)
}
}
impl InputToCommand<Tui, PhraseEditor<Tui>> for PhraseEditorCommand {
fn input_to_command (state: &PhraseEditor<Tui>, from: &TuiInput) -> Option<Self> {
use PhraseEditorCommand::*;
Some(match from.event() {
key!(KeyCode::Char('`')) => ToggleDirection,
key!(KeyCode::Enter) => EnterEditMode,
key!(KeyCode::Esc) => ExitEditMode,
key!(KeyCode::Char('[')) => NoteLengthSet(0),
key!(KeyCode::Char(']')) => NoteLengthSet(0),
key!(KeyCode::Char('a')) => NoteAppend,
key!(KeyCode::Char('s')) => NoteSet,
key!(KeyCode::Char('-')) => TimeZoomSet(0),
key!(KeyCode::Char('_')) => TimeZoomSet(0),
key!(KeyCode::Char('=')) => TimeZoomSet(0),
key!(KeyCode::Char('+')) => TimeZoomSet(0),
key!(KeyCode::PageUp) => NoteScrollSet(0),
key!(KeyCode::PageDown) => NoteScrollSet(0),
key!(KeyCode::Up) => match state.entered {
true => NoteCursorSet(0),
false => NoteScrollSet(0),
},
key!(KeyCode::Down) => match state.entered {
true => NoteCursorSet(0),
false => NoteScrollSet(0),
},
key!(KeyCode::Left) => match state.entered {
true => TimeCursorSet(0),
false => TimeScrollSet(0),
},
key!(KeyCode::Right) => match state.entered {
true => TimeCursorSet(0),
false => TimeScrollSet(0),
},
_ => return None
})
}
}
impl<E: Engine> Command<PhraseEditor<E>> for PhraseEditorCommand {
//fn translate (self, state: &PhraseEditor<E>) -> Self {
//use PhraseEditorCommand::*;
//match self {
//GoUp => match state.entered { true => NoteCursorInc, false => NoteScrollInc, },
//GoDown => match state.entered { true => NoteCursorDec, false => NoteScrollDec, },
//GoLeft => match state.entered { true => TimeCursorDec, false => TimeScrollDec, },
//GoRight => match state.entered { true => TimeCursorInc, false => TimeScrollInc, },
//_ => self
//}
//}
fn execute (self, state: &mut PhraseEditor<E>) -> Perhaps<Self> {
use PhraseEditorCommand::*;
match self.translate(state) {
ToggleDirection => {
state.mode = !state.mode;
},
EnterEditMode => {
state.entered = true;
},
ExitEditMode => {
state.entered = false;
},
TimeZoomOut => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().scale = next_note_length(scale)
},
TimeZoomIn => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().scale = prev_note_length(scale)
},
TimeCursorDec => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().point_dec(scale);
},
TimeCursorInc => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().point_inc(scale);
},
TimeScrollDec => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().start_dec(scale);
},
TimeScrollInc => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().start_inc(scale);
},
NoteCursorDec => {
let mut axis = state.note_axis.write().unwrap();
axis.point_inc(1);
if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } }
},
NoteCursorInc => {
let mut axis = state.note_axis.write().unwrap();
axis.point_dec(1);
if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } }
},
NoteScrollDec => {
state.note_axis.write().unwrap().start_inc(1);
},
NoteScrollInc => {
state.note_axis.write().unwrap().start_dec(1);
},
NoteLengthDec => {
state.note_len = prev_note_length(state.note_len)
},
NoteLengthInc => {
state.note_len = next_note_length(state.note_len)
},
NotePageUp => {
let mut axis = state.note_axis.write().unwrap();
axis.start_dec(3);
axis.point_dec(3);
},
NotePageDown => {
let mut axis = state.note_axis.write().unwrap();
axis.start_inc(3);
axis.point_inc(3);
},
NoteAppend => {
if state.entered {
state.put();
state.time_cursor_advance();
}
},
NoteSet => {
if state.entered { state.put(); }
},
_ => unreachable!()
}
Ok(None)
}
}

View file

@ -83,3 +83,67 @@ fn draw_header <E> (state: &Plugin<E>, to: &mut TuiOutput, x: u16, y: u16, w: u1
}
Ok(Rect { x, y, width: w, height: 1 })
}
impl Handle<Tui> for Plugin<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
match from.event() {
key!(KeyCode::Up) => {
self.selected = self.selected.saturating_sub(1);
Ok(Some(true))
},
key!(KeyCode::Down) => {
self.selected = (self.selected + 1).min(match &self.plugin {
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
_ => unimplemented!()
});
Ok(Some(true))
},
key!(KeyCode::PageUp) => {
self.selected = self.selected.saturating_sub(8);
Ok(Some(true))
},
key!(KeyCode::PageDown) => {
self.selected = (self.selected + 10).min(match &self.plugin {
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
_ => unimplemented!()
});
Ok(Some(true))
},
key!(KeyCode::Char(',')) => {
match self.plugin.as_mut() {
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
let index = port_list[self.selected].index;
if let Some(value) = instance.control_input(index) {
instance.set_control_input(index, value - 0.01);
}
},
_ => {}
}
Ok(Some(true))
},
key!(KeyCode::Char('.')) => {
match self.plugin.as_mut() {
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
let index = port_list[self.selected].index;
if let Some(value) = instance.control_input(index) {
instance.set_control_input(index, value + 0.01);
}
},
_ => {}
}
Ok(Some(true))
},
key!(KeyCode::Char('g')) => {
match self.plugin {
Some(PluginKind::LV2(ref mut plugin)) => {
plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?);
},
Some(_) => unreachable!(),
None => {}
}
Ok(Some(true))
},
_ => Ok(None)
}
}
}

View file

@ -1,64 +0,0 @@
use crate::*;
impl Handle<Tui> for Plugin<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
match from.event() {
key!(KeyCode::Up) => {
self.selected = self.selected.saturating_sub(1);
Ok(Some(true))
},
key!(KeyCode::Down) => {
self.selected = (self.selected + 1).min(match &self.plugin {
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
_ => unimplemented!()
});
Ok(Some(true))
},
key!(KeyCode::PageUp) => {
self.selected = self.selected.saturating_sub(8);
Ok(Some(true))
},
key!(KeyCode::PageDown) => {
self.selected = (self.selected + 10).min(match &self.plugin {
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
_ => unimplemented!()
});
Ok(Some(true))
},
key!(KeyCode::Char(',')) => {
match self.plugin.as_mut() {
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
let index = port_list[self.selected].index;
if let Some(value) = instance.control_input(index) {
instance.set_control_input(index, value - 0.01);
}
},
_ => {}
}
Ok(Some(true))
},
key!(KeyCode::Char('.')) => {
match self.plugin.as_mut() {
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
let index = port_list[self.selected].index;
if let Some(value) = instance.control_input(index) {
instance.set_control_input(index, value + 0.01);
}
},
_ => {}
}
Ok(Some(true))
},
key!(KeyCode::Char('g')) => {
match self.plugin {
Some(PluginKind::LV2(ref mut plugin)) => {
plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?);
},
Some(_) => unreachable!(),
None => {}
}
Ok(Some(true))
},
_ => Ok(None)
}
}
}

View file

@ -132,3 +132,87 @@ impl Content for PhrasePoolView<Tui> {
)
}
}
#[derive(Clone, PartialEq)]
pub enum PhrasePoolViewCommand {
Select(usize),
Edit(PhrasePoolCommand),
Rename(PhraseRenameCommand),
Length(PhraseLengthCommand),
}
impl Handle<Tui> for PhrasePoolView<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
PhrasePoolViewCommand::execute_with_state(self, from)
}
}
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(Cmd::Select(0)),
key!(KeyCode::Down) => Some(Cmd::Select(0)),
key!(KeyCode::Char(',')) => Some(Cmd::Edit(Edit::Swap(0, 0))),
key!(KeyCode::Char('.')) => Some(Cmd::Edit(Edit::Swap(0, 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(..)) => {
Rename::input_to_command(state, input).map(Cmd::Rename)
},
Some(PhrasePoolMode::Length(..)) => {
Length::input_to_command(state, input).map(Cmd::Length)
},
_ => None
}
}
}
}
impl<E: Engine> Command<PhrasePoolView<E>> for PhrasePoolViewCommand {
fn execute (self, view: &mut PhrasePoolView<E>) -> Perhaps<Self> {
use PhraseRenameCommand as Rename;
use PhraseLengthCommand as Length;
match self {
Self::Select(phrase) => {
view.phrase = phrase
},
Self::Edit(command) => {
return Ok(command.execute(&mut view.model)?.map(Self::Edit))
}
Self::Rename(command) => match command {
Rename::Begin => {
view.mode = Some(PhrasePoolMode::Rename(
view.phrase,
view.model.phrases[view.phrase].read().unwrap().name.clone()
))
},
_ => {
return Ok(command.execute(view)?.map(Self::Rename))
}
},
Self::Length(command) => match command {
Length::Begin => {
view.mode = Some(PhrasePoolMode::Length(
view.phrase,
view.model.phrases[view.phrase].read().unwrap().length,
PhraseLengthFocus::Bar
))
},
_ => {
return Ok(command.execute(view)?.map(Self::Length))
}
},
}
Ok(None)
}
}

View file

@ -1,85 +0,0 @@
use crate::*;
#[derive(Clone, PartialEq)]
pub enum PhrasePoolViewCommand {
Select(usize),
Edit(PhrasePoolCommand),
Rename(PhraseRenameCommand),
Length(PhraseLengthCommand),
}
impl Handle<Tui> for PhrasePoolView<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
PhrasePoolViewCommand::execute_with_state(self, from)
}
}
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(Cmd::Select(0)),
key!(KeyCode::Down) => Some(Cmd::Select(0)),
key!(KeyCode::Char(',')) => Some(Cmd::Edit(Edit::Swap(0, 0))),
key!(KeyCode::Char('.')) => Some(Cmd::Edit(Edit::Swap(0, 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(..)) => {
Rename::input_to_command(state, input).map(Cmd::Rename)
},
Some(PhrasePoolMode::Length(..)) => {
Length::input_to_command(state, input).map(Cmd::Length)
},
_ => None
}
}
}
}
impl<E: Engine> Command<PhrasePoolView<E>> for PhrasePoolViewCommand {
fn execute (self, view: &mut PhrasePoolView<E>) -> Perhaps<Self> {
use PhraseRenameCommand as Rename;
use PhraseLengthCommand as Length;
match self {
Self::Select(phrase) => {
view.phrase = phrase
},
Self::Edit(command) => {
return Ok(command.execute(&mut view.model)?.map(Self::Edit))
}
Self::Rename(command) => match command {
Rename::Begin => {
view.mode = Some(PhrasePoolMode::Rename(
view.phrase,
view.model.phrases[view.phrase].read().unwrap().name.clone()
))
},
_ => {
return Ok(command.execute(view)?.map(Self::Rename))
}
},
Self::Length(command) => match command {
Length::Begin => {
view.mode = Some(PhrasePoolMode::Length(
view.phrase,
view.model.phrases[view.phrase].read().unwrap().length,
PhraseLengthFocus::Bar
))
},
_ => {
return Ok(command.execute(view)?.map(Self::Length))
}
},
}
Ok(None)
}
}

View file

@ -1,6 +1,11 @@
use crate::*;
pub type SequencerApp<E: Engine> = AppView<E, SequencerView<E>, SequencerViewCommand>;
pub type SequencerApp<E: Engine> = AppView<
E,
SequencerView<E>,
SequencerViewCommand,
SequencerStatusBar,
>;
impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerApp<Tui> {
type Error = Box<dyn std::error::Error>;
@ -44,6 +49,33 @@ pub struct SequencerView<E: Engine> {
pub split: u16,
}
/// Status bar for sequencer app
#[derive(Copy, Clone)]
pub enum SequencerStatusBar {
Transport,
PhrasePool,
PhraseEditor,
}
#[derive(Clone, PartialEq)]
pub enum SequencerViewCommand {
Focus(FocusCommand),
Transport(TransportViewCommand),
Phrases(PhrasePoolViewCommand),
Editor(PhraseEditorCommand),
}
/// Sections in the sequencer app that may be focused
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum SequencerFocus {
/// The transport (toolbar) is focused
Transport,
/// The phrase list (pool) is focused
PhrasePool,
/// The phrase editor (sequencer) is focused
PhraseEditor,
}
impl Content for SequencerView<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
@ -62,3 +94,108 @@ impl Audio for SequencerView<Tui> {
self.model.process(client, scope)
}
}
impl StatusBar for SequencerStatusBar {
type State = ();
fn hotkey_fg () -> Color {
TuiTheme::hotkey_fg()
}
fn update (&mut self, state: &()) {
todo!()
}
}
impl Content for SequencerStatusBar {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
todo!();
""
}
}
impl Handle<Tui> for SequencerView<Tui> {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
SequencerViewCommand::execute_with_state(self, i)
}
}
impl<E: Engine> Command<SequencerView<E>> for SequencerViewCommand {
fn execute (self, state: &mut SequencerView<E>) -> Perhaps<Self> {
match self {
Self::Focus(cmd) => delegate(cmd, Self::Focus, state),
Self::Phrases(cmd) => delegate(cmd, Self::Phrases, &mut state.phrases),
Self::Editor(cmd) => delegate(cmd, Self::Editor, &mut state.editor),
Self::Transport(cmd) => delegate(cmd, Self::Transport, &mut state.transport)
}
}
}
impl InputToCommand<Tui, SequencerView<Tui>> for SequencerViewCommand {
fn input_to_command (state: &SequencerView<Tui>, input: &TuiInput) -> Option<Self> {
use FocusCommand::*;
match input.event() {
key!(KeyCode::Tab) => Some(Self::Focus(Next)),
key!(Shift-KeyCode::Tab) => Some(Self::Focus(Prev)),
key!(KeyCode::BackTab) => Some(Self::Focus(Prev)),
key!(Shift-KeyCode::BackTab) => Some(Self::Focus(Prev)),
key!(KeyCode::Up) => Some(Self::Focus(Up)),
key!(KeyCode::Down) => Some(Self::Focus(Down)),
key!(KeyCode::Left) => Some(Self::Focus(Left)),
key!(KeyCode::Right) => Some(Self::Focus(Right)),
_ => match state.focused() {
SequencerFocus::Transport => TransportViewCommand::input_to_command(
&state.transport, input
).map(Self::Transport),
SequencerFocus::PhrasePool => PhrasePoolViewCommand::input_to_command(
&state.phrases, input
).map(Self::Phrases),
SequencerFocus::PhraseEditor => PhraseEditorCommand::input_to_command(
&state.editor, input
).map(Self::Editor),
}
}
}
}
impl FocusGrid for SequencerApp<Tui> {
type Item = AppViewFocus<SequencerFocus>;
fn cursor (&self) -> (usize, usize) {
self.cursor
}
fn cursor_mut (&mut self) -> &mut (usize, usize) {
&mut self.cursor
}
fn focus_enter (&mut self) {
let focused = self.focused();
if !self.entered {
self.entered = true;
// TODO
}
}
fn focus_exit (&mut self) {
if self.entered {
self.entered = false;
// TODO
}
}
fn entered (&self) -> Option<Self::Item> {
if self.entered {
Some(self.focused())
} else {
None
}
}
fn layout (&self) -> &[&[Self::Item]] {
use AppViewFocus::*;
use SequencerFocus::*;
&[
&[Menu, Menu ],
&[Content(Transport), Content(Transport) ],
&[Content(PhrasePool), Content(PhraseEditor)],
]
}
fn update_focus (&mut self) {
// TODO
}
}

View file

@ -1,22 +0,0 @@
use crate::*;
/// Status bar for sequencer app
pub enum SequencerStatusBar {
Transport,
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

@ -1,54 +0,0 @@
use crate::*;
#[derive(Clone, PartialEq)]
pub enum SequencerViewCommand {
Focus(FocusCommand),
Transport(TransportViewCommand),
Phrases(PhrasePoolViewCommand),
Editor(PhraseEditorCommand),
}
impl Handle<Tui> for SequencerView<Tui> {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
SequencerViewCommand::execute_with_state(self, i)
}
}
impl<E: Engine> Command<SequencerView<E>> for SequencerViewCommand {
fn execute (self, state: &mut SequencerView<E>) -> Perhaps<Self> {
match self {
Self::Focus(cmd) => delegate(cmd, Self::Focus, state),
Self::Phrases(cmd) => delegate(cmd, Self::Phrases, &mut state.phrases),
Self::Editor(cmd) => delegate(cmd, Self::Editor, &mut state.editor),
Self::Transport(cmd) => delegate(cmd, Self::Transport, &mut state.transport)
}
}
}
impl InputToCommand<Tui, SequencerView<Tui>> for SequencerViewCommand {
fn input_to_command (state: &SequencerView<Tui>, input: &TuiInput) -> Option<Self> {
use FocusCommand::*;
match input.event() {
key!(KeyCode::Tab) => Some(Self::Focus(Next)),
key!(Shift-KeyCode::Tab) => Some(Self::Focus(Prev)),
key!(KeyCode::BackTab) => Some(Self::Focus(Prev)),
key!(Shift-KeyCode::BackTab) => Some(Self::Focus(Prev)),
key!(KeyCode::Up) => Some(Self::Focus(Up)),
key!(KeyCode::Down) => Some(Self::Focus(Down)),
key!(KeyCode::Left) => Some(Self::Focus(Left)),
key!(KeyCode::Right) => Some(Self::Focus(Right)),
_ => match state.focused() {
SequencerFocus::Transport => TransportViewCommand::input_to_command(
&state.transport, input
).map(Self::Transport),
SequencerFocus::PhrasePool => PhrasePoolViewCommand::input_to_command(
&state.phrases, input
).map(Self::Phrases),
SequencerFocus::PhraseEditor => PhraseEditorCommand::input_to_command(
&state.editor, input
).map(Self::Editor),
}
}
}
}

View file

@ -1,54 +0,0 @@
use crate::*;
/// Sections in the sequencer app that may be focused
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum SequencerFocus {
/// The transport (toolbar) is focused
Transport,
/// The phrase list (pool) is focused
PhrasePool,
/// The phrase editor (sequencer) is focused
PhraseEditor,
}
impl FocusGrid for AppView<Tui, SequencerView<Tui>, SequencerViewCommand> {
type Item = AppViewFocus<SequencerFocus>;
fn cursor (&self) -> (usize, usize) {
self.cursor
}
fn cursor_mut (&mut self) -> &mut (usize, usize) {
&mut self.cursor
}
fn focus_enter (&mut self) {
let focused = self.focused();
if !self.entered {
self.entered = true;
// TODO
}
}
fn focus_exit (&mut self) {
if self.entered {
self.entered = false;
// TODO
}
}
fn entered (&self) -> Option<Self::Item> {
if self.entered {
Some(self.focused())
} else {
None
}
}
fn layout (&self) -> &[&[Self::Item]] {
use AppViewFocus::*;
use SequencerFocus::*;
&[
&[Menu, Menu ],
&[Content(Transport), Content(Transport) ],
&[Content(PhrasePool), Content(PhraseEditor)],
]
}
fn update_focus (&mut self) {
// TODO
}
}

View file

@ -0,0 +1,28 @@
use crate::*;
pub trait StatusBar: Copy + Widget<Engine = Tui> {
type State;
fn hotkey_fg () -> Color where Self: Sized;
fn update (&mut self, state: &Self::State) where Self: Sized;
fn command (commands: &[[impl Widget<Engine = Tui>;3]])
-> impl Widget<Engine = Tui> + '_
where
Self: Sized
{
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

@ -15,7 +15,7 @@ impl TuiTheme {
pub fn separator_fg (_: bool) -> Color {
Color::Rgb(0, 0, 0)
}
pub fn hotkey_fg () -> Color {
pub const fn hotkey_fg () -> Color {
Color::Rgb(255, 255, 0)
}
pub fn mode_bg () -> Color {

View file

@ -1,6 +1,11 @@
use crate::*;
pub type TransportApp<E: Engine> = AppView<E, TransportView<E>, TransportViewCommand>;
pub type TransportApp<E: Engine> = AppView<
E,
TransportView<E>,
TransportViewCommand,
TransportStatusBar
>;
impl TryFrom<&Arc<RwLock<JackClient>>> for TransportApp<Tui> {
type Error = Box<dyn std::error::Error>;
@ -78,3 +83,194 @@ impl Audio for TransportView<Tui> {
self.model.process(client, scope)
}
}
#[derive(Copy, Clone)]
pub struct TransportStatusBar;
impl StatusBar for TransportStatusBar {
type State = ();
fn hotkey_fg () -> Color {
TuiTheme::hotkey_fg()
}
fn update (&mut self, state: &()) {
todo!()
}
}
impl Content for TransportStatusBar {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
todo!();
""
}
}
#[derive(Copy, Clone, PartialEq)]
pub enum TransportViewCommand {
Focus(FocusCommand),
Transport(TransportCommand),
}
impl Handle<Tui> for TransportView<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
TransportViewCommand::execute_with_state(self, from)
}
}
impl InputToCommand<Tui, TransportView<Tui>> for TransportViewCommand {
fn input_to_command (view: &TransportView<Tui>, input: &TuiInput) -> Option<Self> {
use TransportViewFocus as Focus;
use FocusCommand as FocusCmd;
use TransportCommand as Cmd;
let clock = view.model.clock();
Some(match input.event() {
key!(KeyCode::Left) => Self::Focus(FocusCmd::Prev),
key!(KeyCode::Right) => Self::Focus(FocusCmd::Next),
key!(KeyCode::Char('.')) => Self::Transport(match view.focus {
Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() + 1.0),
Focus::Quant => Cmd::SetQuant(next_note_length(clock.quant.get()as usize)as f64),
Focus::Sync => Cmd::SetSync(next_note_length(clock.sync.get()as usize)as f64+1.),
Focus::PlayPause => {todo!()},
Focus::Clock => {todo!()}
}),
key!(KeyCode::Char(',')) => Self::Transport(match view.focus {
Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() - 1.0),
Focus::Quant => Cmd::SetQuant(prev_note_length(clock.quant.get()as usize)as f64),
Focus::Sync => Cmd::SetSync(prev_note_length(clock.sync.get()as usize)as f64+1.),
Focus::PlayPause => {todo!()},
Focus::Clock => {todo!()}
}),
key!(KeyCode::Char('>')) => Self::Transport(match view.focus {
Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() + 0.001),
Focus::Quant => Cmd::SetQuant(next_note_length(clock.quant.get()as usize)as f64),
Focus::Sync => Cmd::SetSync(next_note_length(clock.sync.get()as usize)as f64+1.),
Focus::PlayPause => {todo!()},
Focus::Clock => {todo!()}
}),
key!(KeyCode::Char('<')) => Self::Transport(match view.focus {
Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() - 0.001),
Focus::Quant => Cmd::SetQuant(prev_note_length(clock.quant.get()as usize)as f64),
Focus::Sync => Cmd::SetSync(prev_note_length(clock.sync.get()as usize)as f64+1.),
Focus::PlayPause => {todo!()},
Focus::Clock => {todo!()}
}),
_ => return None
})
}
}
impl<E: Engine> Command<TransportView<E>> for TransportViewCommand {
fn execute (self, view: &mut TransportView<E>) -> Perhaps<Self> {
let clock = view.model.clock();
Ok(Some(match self {
Self::Focus(command) => Self::Focus({
use FocusCommand::*;
match command {
Next => { todo!() },
Prev => { todo!() },
_ => { todo!() }
}
}),
Self::Transport(command) => Self::Transport({
use TransportCommand::*;
match command {
SetBpm(bpm) => SetBpm(clock.timebase().bpm.set(bpm)),
SetQuant(quant) => SetQuant(clock.quant.set(quant)),
SetSync(sync) => SetSync(clock.sync.set(sync)),
_ => {
todo!()
}
}
}),
}))
}
}
impl TransportViewFocus {
pub fn next (&mut self) {
*self = match self {
Self::PlayPause => Self::Bpm,
Self::Bpm => Self::Quant,
Self::Quant => Self::Sync,
Self::Sync => Self::Clock,
Self::Clock => Self::PlayPause,
}
}
pub fn prev (&mut self) {
*self = match self {
Self::PlayPause => Self::Clock,
Self::Bpm => Self::PlayPause,
Self::Quant => Self::Bpm,
Self::Sync => Self::Quant,
Self::Clock => Self::Sync,
}
}
pub fn wrap <'a, W: Widget<Engine = Tui>> (
self, parent_focus: bool, focus: Self, widget: &'a W
) -> impl Widget<Engine = Tui> + 'a {
let focused = parent_focus && focus == self;
let corners = focused.then_some(CORNERS);
let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50)));
lay!(corners, highlight, *widget)
}
}
/// Which item of the transport toolbar is focused
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TransportViewFocus {
Bpm,
Sync,
PlayPause,
Clock,
Quant,
}
impl FocusGrid for TransportApp<Tui> {
type Item = AppViewFocus<TransportViewFocus>;
fn cursor (&self) -> (usize, usize) {
self.cursor
}
fn cursor_mut (&mut self) -> &mut (usize, usize) {
&mut self.cursor
}
fn focus_enter (&mut self) {
let focused = self.focused();
if !self.entered {
self.entered = true;
// TODO
}
}
fn focus_exit (&mut self) {
if self.entered {
self.entered = false;
// TODO
}
}
fn entered (&self) -> Option<Self::Item> {
if self.entered {
Some(self.focused())
} else {
None
}
}
fn layout (&self) -> &[&[Self::Item]] {
use AppViewFocus::*;
use TransportViewFocus::*;
&[
&[Menu],
&[
Content(Bpm),
Content(Sync),
Content(PlayPause),
Content(Clock),
Content(Quant),
],
]
}
fn update_focus (&mut self) {
// TODO
}
}

View file

@ -1,17 +0,0 @@
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!();
""
}
}

View file

@ -1,85 +0,0 @@
use crate::*;
#[derive(Copy, Clone, PartialEq)]
pub enum TransportViewCommand {
Focus(FocusCommand),
Transport(TransportCommand),
}
impl Handle<Tui> for TransportView<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
TransportViewCommand::execute_with_state(self, from)
}
}
impl InputToCommand<Tui, TransportView<Tui>> for TransportViewCommand {
fn input_to_command (view: &TransportView<Tui>, input: &TuiInput) -> Option<Self> {
use TransportViewFocus as Focus;
use FocusCommand as FocusCmd;
use TransportCommand as Cmd;
let clock = view.model.clock();
Some(match input.event() {
key!(KeyCode::Left) => Self::Focus(FocusCmd::Prev),
key!(KeyCode::Right) => Self::Focus(FocusCmd::Next),
key!(KeyCode::Char('.')) => Self::Transport(match view.focus {
Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() + 1.0),
Focus::Quant => Cmd::SetQuant(next_note_length(clock.quant.get()as usize)as f64),
Focus::Sync => Cmd::SetSync(next_note_length(clock.sync.get()as usize)as f64+1.),
Focus::PlayPause => {todo!()},
Focus::Clock => {todo!()}
}),
key!(KeyCode::Char(',')) => Self::Transport(match view.focus {
Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() - 1.0),
Focus::Quant => Cmd::SetQuant(prev_note_length(clock.quant.get()as usize)as f64),
Focus::Sync => Cmd::SetSync(prev_note_length(clock.sync.get()as usize)as f64+1.),
Focus::PlayPause => {todo!()},
Focus::Clock => {todo!()}
}),
key!(KeyCode::Char('>')) => Self::Transport(match view.focus {
Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() + 0.001),
Focus::Quant => Cmd::SetQuant(next_note_length(clock.quant.get()as usize)as f64),
Focus::Sync => Cmd::SetSync(next_note_length(clock.sync.get()as usize)as f64+1.),
Focus::PlayPause => {todo!()},
Focus::Clock => {todo!()}
}),
key!(KeyCode::Char('<')) => Self::Transport(match view.focus {
Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() - 0.001),
Focus::Quant => Cmd::SetQuant(prev_note_length(clock.quant.get()as usize)as f64),
Focus::Sync => Cmd::SetSync(prev_note_length(clock.sync.get()as usize)as f64+1.),
Focus::PlayPause => {todo!()},
Focus::Clock => {todo!()}
}),
_ => return None
})
}
}
impl<E: Engine> Command<TransportView<E>> for TransportViewCommand {
fn execute (self, view: &mut TransportView<E>) -> Perhaps<Self> {
let clock = view.model.clock();
Ok(Some(match self {
Self::Focus(command) => Self::Focus({
use FocusCommand::*;
match command {
Next => { todo!() },
Prev => { todo!() },
_ => { todo!() }
}
}),
Self::Transport(command) => Self::Transport({
use TransportCommand::*;
match command {
SetBpm(bpm) => SetBpm(clock.timebase().bpm.set(bpm)),
SetQuant(quant) => SetQuant(clock.quant.set(quant)),
SetSync(sync) => SetSync(clock.sync.set(sync)),
_ => {
todo!()
}
}
}),
}))
}
}

View file

@ -1,87 +0,0 @@
use crate::*;
impl TransportViewFocus {
pub fn next (&mut self) {
*self = match self {
Self::PlayPause => Self::Bpm,
Self::Bpm => Self::Quant,
Self::Quant => Self::Sync,
Self::Sync => Self::Clock,
Self::Clock => Self::PlayPause,
}
}
pub fn prev (&mut self) {
*self = match self {
Self::PlayPause => Self::Clock,
Self::Bpm => Self::PlayPause,
Self::Quant => Self::Bpm,
Self::Sync => Self::Quant,
Self::Clock => Self::Sync,
}
}
pub fn wrap <'a, W: Widget<Engine = Tui>> (
self, parent_focus: bool, focus: Self, widget: &'a W
) -> impl Widget<Engine = Tui> + 'a {
let focused = parent_focus && focus == self;
let corners = focused.then_some(CORNERS);
let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50)));
lay!(corners, highlight, *widget)
}
}
/// Which item of the transport toolbar is focused
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TransportViewFocus {
Bpm,
Sync,
PlayPause,
Clock,
Quant,
}
impl FocusGrid for AppView<Tui, TransportView<Tui>, TransportViewCommand> {
type Item = AppViewFocus<TransportViewFocus>;
fn cursor (&self) -> (usize, usize) {
self.cursor
}
fn cursor_mut (&mut self) -> &mut (usize, usize) {
&mut self.cursor
}
fn focus_enter (&mut self) {
let focused = self.focused();
if !self.entered {
self.entered = true;
// TODO
}
}
fn focus_exit (&mut self) {
if self.entered {
self.entered = false;
// TODO
}
}
fn entered (&self) -> Option<Self::Item> {
if self.entered {
Some(self.focused())
} else {
None
}
}
fn layout (&self) -> &[&[Self::Item]] {
use AppViewFocus::*;
use TransportViewFocus::*;
&[
&[Menu],
&[
Content(Bpm),
Content(Sync),
Content(PlayPause),
Content(Clock),
Content(Quant),
],
]
}
fn update_focus (&mut self) {
// TODO
}
}