cmdsys: menubar pt.1

This commit is contained in:
🪞👃🪞 2024-11-08 20:05:05 +01:00
parent 2b163e9e27
commit 38e8cfc214
7 changed files with 180 additions and 136 deletions

View file

@ -6,28 +6,65 @@ pub trait Command<S>: Sized {
pub trait MatchInput<E: Engine, S>: Sized { pub trait MatchInput<E: Engine, S>: Sized {
fn match_input (state: &S, input: &E::Input) -> Option<Self>; fn match_input (state: &S, input: &E::Input) -> Option<Self>;
} }
pub struct Menu<E: Engine, S: Handle<E>, C: Command<S>> { pub struct MenuBar<E: Engine, S, C: Command<S>> {
pub menus: Vec<Menu<E, S, C>>,
pub index: usize,
}
impl<E: Engine, S, C: Command<S>> MenuBar<E, S, C> {
pub fn new () -> Self { Self { menus: vec![], index: 0 } }
pub fn add (mut self, menu: Menu<E, S, C>) -> Self {
self.menus.push(menu);
self
}
}
pub struct Menu<E: Engine, S, C: Command<S>> {
pub title: String,
pub items: Vec<MenuItem<E, S, C>>, pub items: Vec<MenuItem<E, S, C>>,
pub index: usize, pub index: usize,
} }
impl<E: Engine, S: Handle<E>, C: Command<S>> Menu<E, S, C> { impl<E: Engine, S, C: Command<S>> Menu<E, S, C> {
pub const fn item (command: C, name: &'static str, key: &'static str) -> MenuItem<E, S, C> { pub fn new (title: impl AsRef<str>) -> Self {
MenuItem::Command(command, name, key) Self {
title: title.as_ref().to_string(),
items: vec![],
index: 0
}
}
pub fn add (mut self, item: MenuItem<E, S, C>) -> Self {
self.items.push(item);
self
}
pub fn sep (mut self) -> Self {
self.items.push(MenuItem::sep());
self
}
pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self {
self.items.push(MenuItem::cmd(hotkey, text, command));
self
}
pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self {
self.items.push(MenuItem::off(hotkey, text));
self
} }
} }
pub enum MenuItem<E: Engine, S: Handle<E>, C: Command<S>> { pub enum MenuItem<E: Engine, S, C: Command<S>> {
/// Unused. /// Unused.
__(PhantomData<E>, PhantomData<S>), __(PhantomData<E>, PhantomData<S>),
/// A separator. Skip it. /// A separator. Skip it.
Separator, Separator,
/// A menu item with command, description and hotkey. /// A menu item with command, description and hotkey.
Command(C, &'static str, &'static str) Command(&'static str, &'static str, C),
/// A menu item that can't be activated but has description and hotkey
Disabled(&'static str, &'static str)
} }
impl<E: Engine, S: Handle<E>, C: Command<S>> MenuItem<E, S, C> { impl<E: Engine, S, C: Command<S>> MenuItem<E, S, C> {
pub fn sep () -> Self { pub fn sep () -> Self {
Self::Separator Self::Separator
} }
pub fn cmd (command: C, text: &'static str, hotkey: &'static str) -> Self { pub fn cmd (hotkey: &'static str, text: &'static str, command: C) -> Self {
Self::Command(command, text, hotkey) Self::Command(hotkey, text, command)
}
pub fn off (hotkey: &'static str, text: &'static str) -> Self {
Self::Disabled(hotkey, text)
} }
} }

View file

@ -24,6 +24,8 @@ pub struct Arranger<E: Engine> {
pub phrases_split: u16, pub phrases_split: u16,
/// Width and height of app at last render /// Width and height of app at last render
pub size: Measure<E>, pub size: Measure<E>,
/// Menu bar
pub menu: MenuBar<E, Self, ArrangerCommand>,
} }
/// Sections in the arranger app that may be focused /// Sections in the arranger app that may be focused
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
@ -141,6 +143,59 @@ impl<E: Engine> Arranger<E> {
} else { } else {
Arc::new(TransportTime::default()) Arc::new(TransportTime::default())
}, },
menu: {
use ArrangerCommand::*;
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 TransportCommand::*;
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))
})
}
}; };
app.update_focus(); app.update_focus();
app app

View file

@ -15,6 +15,9 @@ pub enum ArrangerCommand {
} }
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub enum ArrangementCommand { pub enum ArrangementCommand {
New,
Load,
Save,
ToggleViewMode, ToggleViewMode,
Delete, Delete,
Activate, Activate,
@ -67,6 +70,9 @@ impl <E: Engine> Command<Arrangement<E>> for ArrangementCommand {
fn run (&self, state: &mut Arrangement<E>) -> Perhaps<Self> { fn run (&self, state: &mut Arrangement<E>) -> Perhaps<Self> {
use ArrangementCommand::*; use ArrangementCommand::*;
match self { match self {
New => todo!(),
Load => todo!(),
Save => todo!(),
ToggleViewMode => { state.mode.to_next(); }, ToggleViewMode => { state.mode.to_next(); },
Delete => { state.delete(); }, Delete => { state.delete(); },
Activate => { state.activate(); }, Activate => { state.activate(); },

View file

@ -533,7 +533,7 @@ impl MatchInput<Tui, Arranger<Tui>> for ArrangerCommand {
key!(KeyCode::Down) => Some(Self::FocusDown), key!(KeyCode::Down) => Some(Self::FocusDown),
key!(KeyCode::Left) => Some(Self::FocusLeft), key!(KeyCode::Left) => Some(Self::FocusLeft),
key!(KeyCode::Right) => Some(Self::FocusRight), key!(KeyCode::Right) => Some(Self::FocusRight),
key!(KeyCode::Char(' ')) => Some(Self::Transport(TransportCommand::TogglePlay)), key!(KeyCode::Char(' ')) => Some(Self::Transport(TransportCommand::PlayToggle)),
_ => match state.focused() { _ => match state.focused() {
ArrangerFocus::Transport => state.transport.as_ref() ArrangerFocus::Transport => state.transport.as_ref()
.map(|t|TransportCommand::match_input(&*t.read().unwrap(), input) .map(|t|TransportCommand::match_input(&*t.read().unwrap(), input)

View file

@ -24,6 +24,8 @@ pub enum PhrasePoolCommand {
Duplicate, Duplicate,
RandomColor, RandomColor,
Edit, Edit,
Import,
Export,
Rename(PhraseRenameCommand), Rename(PhraseRenameCommand),
Length(PhraseLengthCommand), Length(PhraseLengthCommand),
} }
@ -91,89 +93,65 @@ impl<E: Engine> Command<Sequencer<E>> for SequencerCommand {
} }
impl<E: Engine> Command<PhrasePool<E>> for PhrasePoolCommand { impl<E: Engine> Command<PhrasePool<E>> for PhrasePoolCommand {
fn run (&self, state: &mut PhrasePool<E>) -> Perhaps<Self> { fn run (&self, state: &mut PhrasePool<E>) -> Perhaps<Self> {
use PhrasePoolCommand::*;
use PhraseRenameCommand as Rename;
use PhraseLengthCommand as Length;
match self { match self {
Self::Prev => { Prev => { state.select_prev() },
state.select_prev() Next => { state.select_next() },
}, Delete => { state.delete_selected() },
Self::Next => { Append => { state.append_new(None, None) },
state.select_next() Insert => { state.insert_new(None, None) },
}, Duplicate => { state.insert_dup() },
Self::Delete => { Edit => { todo!(); }
state.delete_selected() RandomColor => { state.randomize_color() },
}, MoveUp => { state.move_up() },
Self::Append => { MoveDown => { state.move_down() },
state.append_new(None, None) Rename(Rename::Begin) => { state.begin_rename() },
}, Rename(_) => { unreachable!() },
Self::Insert => { Length(Length::Begin) => { state.begin_length() },
state.insert_new(None, None) Length(_) => { unreachable!() },
}, Import => todo!(),
Self::Duplicate => { Export => todo!(),
state.insert_dup()
},
Self::Edit => {
todo!();
}
Self::RandomColor => {
state.randomize_color()
},
Self::MoveUp => {
state.move_up()
},
Self::MoveDown => {
state.move_down()
},
Self::Rename(PhraseRenameCommand::Begin) => {
state.begin_rename()
},
Self::Rename(_) => {
unreachable!()
},
Self::Length(PhraseLengthCommand::Begin) => {
state.begin_length()
},
Self::Length(_) => {
unreachable!()
},
} }
Ok(None) Ok(None)
} }
} }
impl<E: Engine> Command<PhrasePool<E>> for PhraseRenameCommand { impl<E: Engine> Command<PhrasePool<E>> for PhraseRenameCommand {
fn run (&self, state: &mut PhrasePool<E>) -> Perhaps<Self> { fn run (&self, state: &mut PhrasePool<E>) -> Perhaps<Self> {
use PhraseRenameCommand::*;
if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = state.mode { if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = state.mode {
match self { match self {
Self::Begin => { Begin => { unreachable!(); },
unreachable!(); Backspace => {
},
Self::Backspace => {
let mut phrase = state.phrases[phrase].write().unwrap(); let mut phrase = state.phrases[phrase].write().unwrap();
let old_name = phrase.name.clone(); let old_name = phrase.name.clone();
phrase.name.pop(); phrase.name.pop();
return Ok(Some(Self::Set(old_name))) return Ok(Some(Self::Set(old_name)))
}, },
Self::Append(c) => { Append(c) => {
let mut phrase = state.phrases[phrase].write().unwrap(); let mut phrase = state.phrases[phrase].write().unwrap();
let old_name = phrase.name.clone(); let old_name = phrase.name.clone();
phrase.name.push(*c); phrase.name.push(*c);
return Ok(Some(Self::Set(old_name))) return Ok(Some(Self::Set(old_name)))
}, },
Self::Set(s) => { Set(s) => {
let mut phrase = state.phrases[phrase].write().unwrap(); let mut phrase = state.phrases[phrase].write().unwrap();
phrase.name = s.into(); phrase.name = s.into();
return Ok(Some(Self::Set(old_name.clone()))) return Ok(Some(Self::Set(old_name.clone())))
}, },
Self::Confirm => { Confirm => {
let old_name = old_name.clone(); let old_name = old_name.clone();
state.mode = None; state.mode = None;
return Ok(Some(Self::Set(old_name))) return Ok(Some(Self::Set(old_name)))
}, },
Self::Cancel => { Cancel => {
let mut phrase = state.phrases[phrase].write().unwrap(); let mut phrase = state.phrases[phrase].write().unwrap();
phrase.name = old_name.clone(); phrase.name = old_name.clone();
} }
}; };
Ok(None) Ok(None)
} else if *self == Self::Begin { } else if *self == Begin {
todo!() todo!()
} else { } else {
unreachable!() unreachable!()
@ -182,46 +160,31 @@ impl<E: Engine> Command<PhrasePool<E>> for PhraseRenameCommand {
} }
impl<E: Engine> Command<PhrasePool<E>> for PhraseLengthCommand { impl<E: Engine> Command<PhrasePool<E>> for PhraseLengthCommand {
fn run (&self, state: &mut PhrasePool<E>) -> Perhaps<Self> { fn run (&self, state: &mut PhrasePool<E>) -> Perhaps<Self> {
use PhraseLengthCommand::*;
if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = state.mode { if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = state.mode {
match self { match self {
Self::Begin => { Begin => { unreachable!(); },
unreachable!(); Cancel => { state.mode = None; },
Confirm => { return Self::Set(*length).run(state) },
Prev => { focus.prev() },
Next => { focus.next() },
Inc => {
use PhraseLengthFocus::*;
match focus {
Bar => { *length += 4 * PPQ },
Beat => { *length += PPQ },
Tick => { *length += 1 },
}
}, },
Self::Prev => { Dec => {
focus.prev() use PhraseLengthFocus::*;
match focus {
Bar => { *length = length.saturating_sub(4 * PPQ) },
Beat => { *length = length.saturating_sub(PPQ) },
Tick => { *length = length.saturating_sub(1) },
}
}, },
Self::Next => { Set(length) => {
focus.next()
},
Self::Inc => match focus {
PhraseLengthFocus::Bar => {
*length += 4 * PPQ
},
PhraseLengthFocus::Beat => {
*length += PPQ
},
PhraseLengthFocus::Tick => {
*length += 1
},
},
Self::Dec => match focus {
PhraseLengthFocus::Bar => {
*length = length.saturating_sub(4 * PPQ)
},
PhraseLengthFocus::Beat => {
*length = length.saturating_sub(PPQ)
},
PhraseLengthFocus::Tick => {
*length = length.saturating_sub(1)
},
},
Self::Cancel => {
state.mode = None;
},
Self::Confirm => {
return Self::Set(*length).run(state)
},
Self::Set(length) => {
let mut phrase = state.phrases[phrase].write().unwrap(); let mut phrase = state.phrases[phrase].write().unwrap();
let old_length = phrase.length; let old_length = phrase.length;
phrase.length = *length; phrase.length = *length;
@ -230,7 +193,7 @@ impl<E: Engine> Command<PhrasePool<E>> for PhraseLengthCommand {
}, },
} }
Ok(None) Ok(None)
} else if *self == Self::Begin { } else if *self == Begin {
todo!() todo!()
} else { } else {
unreachable!() unreachable!()
@ -239,54 +202,35 @@ impl<E: Engine> Command<PhrasePool<E>> for PhraseLengthCommand {
} }
impl<E: Engine> Command<PhraseEditor<E>> for PhraseEditorCommand { impl<E: Engine> Command<PhraseEditor<E>> for PhraseEditorCommand {
fn run (&self, state: &mut PhraseEditor<E>) -> Perhaps<Self> { fn run (&self, state: &mut PhraseEditor<E>) -> Perhaps<Self> {
use PhraseEditorCommand::*;
match self { match self {
Self::ToggleDirection => { ToggleDirection => { state.mode = !state.mode; },
state.mode = !state.mode; EnterEditMode => { state.entered = true; },
}, ExitEditMode => { state.entered = false; },
Self::EnterEditMode => { TimeZoomOut => { state.time_zoom_out() },
state.entered = true; TimeZoomIn => { state.time_zoom_in() },
}, NoteLengthDec => { state.note_length_dec() },
Self::ExitEditMode => { NoteLengthInc => { state.note_length_inc() },
state.entered = false; NotePageUp => { state.note_page_up() },
}, NotePageDown => { state.note_page_down() },
Self::TimeZoomOut => { NoteAppend => if state.entered {
state.time_zoom_out()
},
Self::TimeZoomIn => {
state.time_zoom_in()
},
Self::NoteLengthDec => {
state.note_length_dec()
},
Self::NoteLengthInc => {
state.note_length_inc()
},
Self::NotePageUp => {
state.note_page_up()
},
Self::NotePageDown => {
state.note_page_down()
},
Self::NoteAppend => if state.entered {
state.put(); state.put();
state.time_cursor_advance(); state.time_cursor_advance();
}, },
Self::NoteSet => if state.entered { NoteSet => if state.entered { state.put(); },
state.put(); GoUp => match state.entered {
},
Self::GoUp => match state.entered {
true => state.note_cursor_inc(), true => state.note_cursor_inc(),
false => state.note_scroll_inc(), false => state.note_scroll_inc(),
}, },
Self::GoDown => match state.entered { GoDown => match state.entered {
true => state.note_cursor_dec(), true => state.note_cursor_dec(),
false => state.note_scroll_dec(), false => state.note_scroll_dec(),
}, },
Self::GoLeft => match state.entered { GoLeft => match state.entered {
true => state.time_cursor_dec(), true => state.time_cursor_dec(),
false => state.time_scroll_dec(), false => state.time_scroll_dec(),
}, },
Self::GoRight => match state.entered { GoRight => match state.entered {
true => state.time_cursor_inc(), true => state.time_cursor_inc(),
false => state.time_scroll_inc(), false => state.time_scroll_inc(),
}, },

View file

@ -30,7 +30,7 @@ impl MatchInput<Tui, Sequencer<Tui>> for SequencerCommand {
key!(KeyCode::Down) => Some(Self::FocusDown), key!(KeyCode::Down) => Some(Self::FocusDown),
key!(KeyCode::Left) => Some(Self::FocusLeft), key!(KeyCode::Left) => Some(Self::FocusLeft),
key!(KeyCode::Right) => Some(Self::FocusRight), key!(KeyCode::Right) => Some(Self::FocusRight),
key!(KeyCode::Char(' ')) => Some(Self::Transport(TransportCommand::TogglePlay)), key!(KeyCode::Char(' ')) => Some(Self::Transport(TransportCommand::PlayToggle)),
_ => match state.focused() { _ => match state.focused() {
SequencerFocus::Transport => state.transport.as_ref() SequencerFocus::Transport => state.transport.as_ref()
.map(|t|TransportCommand::match_input(&*t.read().unwrap(), input) .map(|t|TransportCommand::match_input(&*t.read().unwrap(), input)

View file

@ -3,8 +3,10 @@ use crate::*;
pub enum TransportCommand { pub enum TransportCommand {
FocusNext, FocusNext,
FocusPrev, FocusPrev,
Play,
Pause,
PlayToggle,
PlayFromStart, PlayFromStart,
TogglePlay,
Increment, Increment,
Decrement, Decrement,
FineIncrement, FineIncrement,
@ -25,7 +27,7 @@ impl<E: Engine> Command<TransportToolbar<E>> for TransportCommand {
Self::FocusPrev => { Self::FocusPrev => {
state.focus.prev(); state.focus.prev();
}, },
Self::TogglePlay => { Self::PlayToggle => {
state.toggle_play()?; state.toggle_play()?;
}, },
Self::Increment => { Self::Increment => {