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 {
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 index: usize,
}
impl<E: Engine, S: Handle<E>, C: Command<S>> Menu<E, S, C> {
pub const fn item (command: C, name: &'static str, key: &'static str) -> MenuItem<E, S, C> {
MenuItem::Command(command, name, key)
impl<E: Engine, S, C: Command<S>> Menu<E, S, C> {
pub fn new (title: impl AsRef<str>) -> Self {
Self {
title: title.as_ref().to_string(),
items: vec![],
index: 0
}
}
pub enum MenuItem<E: Engine, S: Handle<E>, C: Command<S>> {
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, C: Command<S>> {
/// Unused.
__(PhantomData<E>, PhantomData<S>),
/// A separator. Skip it.
Separator,
/// 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 {
Self::Separator
}
pub fn cmd (command: C, text: &'static str, hotkey: &'static str) -> Self {
Self::Command(command, text, hotkey)
pub fn cmd (hotkey: &'static str, text: &'static str, command: C) -> Self {
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,
/// Width and height of app at last render
pub size: Measure<E>,
/// Menu bar
pub menu: MenuBar<E, Self, ArrangerCommand>,
}
/// Sections in the arranger app that may be focused
#[derive(Copy, Clone, PartialEq, Eq)]
@ -141,6 +143,59 @@ impl<E: Engine> Arranger<E> {
} else {
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

View file

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

View file

@ -533,7 +533,7 @@ impl MatchInput<Tui, Arranger<Tui>> for ArrangerCommand {
key!(KeyCode::Down) => Some(Self::FocusDown),
key!(KeyCode::Left) => Some(Self::FocusLeft),
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() {
ArrangerFocus::Transport => state.transport.as_ref()
.map(|t|TransportCommand::match_input(&*t.read().unwrap(), input)

View file

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

View file

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