#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClipViewFocus {
FxPanel,
PianoRoll,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FxPanelTab {
TrackFx,
Synth,
}
impl FxPanelTab {
pub fn label(self) -> &'static str {
match self {
Self::TrackFx => "trk fx",
Self::Synth => "synth",
}
}
pub fn next(self) -> Self {
match self {
Self::TrackFx => Self::Synth,
Self::Synth => Self::TrackFx,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClipTab {
InstConfig,
PianoRoll,
Automation,
}
impl ClipTab {
pub fn label(self) -> &'static str {
match self {
Self::InstConfig => "inst",
Self::PianoRoll => "piano",
Self::Automation => "auto",
}
}
pub fn next(self) -> Self {
match self {
Self::InstConfig => Self::PianoRoll,
Self::PianoRoll => Self::Automation,
Self::Automation => Self::InstConfig,
}
}
pub const ALL: &[ClipTab] = &[Self::InstConfig, Self::PianoRoll, Self::Automation];
}
#[derive(Debug)]
pub struct ClipViewState {
pub focus: ClipViewFocus,
pub fx_panel_tab: FxPanelTab,
pub clip_tab: ClipTab,
pub piano_roll: PianoRollState,
pub fx_cursor: usize,
pub synth_param_cursor: usize,
pub inst_config_cursor: usize,
}
impl Default for ClipViewState {
fn default() -> Self { Self::new() }
}
impl ClipViewState {
pub fn new() -> Self {
Self {
focus: ClipViewFocus::PianoRoll,
fx_panel_tab: FxPanelTab::TrackFx,
clip_tab: ClipTab::PianoRoll,
piano_roll: PianoRollState::new(),
fx_cursor: 0,
synth_param_cursor: 0,
inst_config_cursor: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PianoRollFocus {
Navigation,
Selected,
Row,
}
#[derive(Debug)]
pub struct PianoRollState {
pub cursor_note: u8,
pub scroll_x: usize,
pub view_bottom_note: u8,
pub view_height: u8,
pub focus: PianoRollFocus,
pub column: usize,
pub column_count: usize,
pub selected_note_indices: Vec<usize>,
column_digits: String,
pub highlight_start: Option<usize>,
pub highlight_end: Option<usize>,
pub visible_columns: usize,
pub yank_buffer: Vec<phosphor_core::clip::NoteSnapshot>,
pub yank_columns: usize,
pub row_highlight_low: Option<u8>,
pub row_highlight_high: Option<u8>,
}
impl Default for PianoRollState {
fn default() -> Self { Self::new() }
}
impl PianoRollState {
pub fn new() -> Self {
Self {
cursor_note: 60,
scroll_x: 0,
view_bottom_note: 48,
view_height: 24,
focus: PianoRollFocus::Navigation,
column: 0,
column_count: 16,
selected_note_indices: Vec::new(),
column_digits: String::new(),
highlight_start: None,
highlight_end: None,
visible_columns: 16,
row_highlight_low: None,
row_highlight_high: None,
yank_buffer: Vec::new(),
yank_columns: 0,
}
}
pub fn enter(&mut self, note_indices: Vec<usize>) {
match self.focus {
PianoRollFocus::Navigation => {
self.focus = PianoRollFocus::Selected;
self.selected_note_indices = note_indices;
}
PianoRollFocus::Selected | PianoRollFocus::Row => {}
}
}
pub fn enter_row(&mut self) {
self.focus = PianoRollFocus::Row;
}
pub fn escape(&mut self) {
match self.focus {
PianoRollFocus::Row => {
self.focus = PianoRollFocus::Selected;
}
PianoRollFocus::Selected => {
self.focus = PianoRollFocus::Navigation;
self.column_digits.clear();
}
PianoRollFocus::Navigation => {
}
}
}
pub fn can_escape(&self) -> bool {
self.focus != PianoRollFocus::Navigation
}
pub fn move_up(&mut self) {
if self.cursor_note < 127 {
self.cursor_note += 1;
let top = self.view_bottom_note.saturating_add(self.view_height);
if self.cursor_note >= top {
self.view_bottom_note = self.cursor_note - self.view_height + 1;
}
}
}
pub fn move_down(&mut self) {
if self.cursor_note > 0 {
self.cursor_note -= 1;
if self.cursor_note < self.view_bottom_note {
self.view_bottom_note = self.cursor_note;
}
}
}
pub fn move_column_left(&mut self) {
if self.column > 0 {
self.column -= 1;
if self.column < self.scroll_x {
self.scroll_x = self.column;
}
}
}
pub fn move_column_right(&mut self) {
if self.column + 1 < self.column_count {
self.column += 1;
if self.column >= self.scroll_x + self.visible_columns && self.visible_columns > 0 {
self.scroll_x = self.column + 1 - self.visible_columns;
}
}
}
pub fn type_digit(&mut self, ch: char) -> bool {
self.column_digits.push(ch);
if let Ok(num) = self.column_digits.parse::<usize>() {
if num >= 1 && num <= self.column_count {
let could_grow = num * 10 <= self.column_count;
if !could_grow || self.column_digits.len() >= 2 {
self.column = num - 1;
self.column_digits.clear();
self.ensure_column_visible();
return true;
}
return false;
}
}
self.column_digits.clear();
false
}
pub fn commit_digits(&mut self) -> bool {
if let Ok(num) = self.column_digits.parse::<usize>() {
if num >= 1 && num <= self.column_count {
self.column = num - 1;
self.column_digits.clear();
self.ensure_column_visible();
return true;
}
}
self.column_digits.clear();
false
}
pub fn ensure_column_visible(&mut self) {
if self.visible_columns == 0 { return; }
if self.column < self.scroll_x {
self.scroll_x = self.column;
} else if self.column >= self.scroll_x + self.visible_columns {
self.scroll_x = self.column + 1 - self.visible_columns;
}
}
pub fn column_digits_display(&self) -> &str {
&self.column_digits
}
pub fn start_highlight(&mut self) {
if let (Some(s), Some(e)) = (self.highlight_start, self.highlight_end) {
if s == e && s == self.column {
self.clear_highlight();
return;
}
}
if self.highlight_start.is_none() {
self.highlight_start = Some(self.column);
self.highlight_end = Some(self.column);
}
}
pub fn highlight_left(&mut self) {
if let (Some(start), Some(end)) = (self.highlight_start, self.highlight_end) {
if self.column > 0 {
self.column -= 1;
}
let new_start = self.column.min(start);
let new_end = self.column.max(end);
self.highlight_start = Some(new_start);
self.highlight_end = Some(new_end);
if self.column >= start {
self.highlight_end = Some(self.column);
} else {
self.highlight_start = Some(self.column);
}
}
}
pub fn highlight_right(&mut self) {
if let (Some(start), Some(end)) = (self.highlight_start, self.highlight_end) {
if self.column + 1 < self.column_count {
self.column += 1;
}
let new_start = self.column.min(start);
let new_end = self.column.max(end);
self.highlight_start = Some(new_start);
self.highlight_end = Some(new_end);
if self.column <= end {
self.highlight_start = Some(self.column);
} else {
self.highlight_end = Some(self.column);
}
}
}
pub fn clear_highlight(&mut self) {
self.highlight_start = None;
self.highlight_end = None;
}
pub fn start_row_highlight(&mut self) {
if let (Some(lo), Some(hi)) = (self.row_highlight_low, self.row_highlight_high) {
if lo == hi && lo == self.cursor_note {
self.clear_row_highlight();
return;
}
}
if self.row_highlight_low.is_none() {
self.row_highlight_low = Some(self.cursor_note);
self.row_highlight_high = Some(self.cursor_note);
}
}
pub fn highlight_down(&mut self) {
self.start_row_highlight();
if self.cursor_note > 0 {
self.cursor_note -= 1;
if self.cursor_note < self.view_bottom_note {
self.view_bottom_note = self.cursor_note;
}
}
if let Some(lo) = self.row_highlight_low {
self.row_highlight_low = Some(self.cursor_note.min(lo));
}
if let Some(hi) = self.row_highlight_high {
self.row_highlight_high = Some(self.cursor_note.max(hi));
}
}
pub fn highlight_up(&mut self) {
self.start_row_highlight();
if self.cursor_note < 127 {
self.cursor_note += 1;
let top = self.view_bottom_note.saturating_add(self.view_height);
if self.cursor_note >= top {
self.view_bottom_note = self.cursor_note - self.view_height + 1;
}
}
if let Some(lo) = self.row_highlight_low {
self.row_highlight_low = Some(self.cursor_note.min(lo));
}
if let Some(hi) = self.row_highlight_high {
self.row_highlight_high = Some(self.cursor_note.max(hi));
}
}
pub fn clear_row_highlight(&mut self) {
self.row_highlight_low = None;
self.row_highlight_high = None;
}
pub fn is_row_highlighted(&self, note: u8) -> bool {
if let (Some(lo), Some(hi)) = (self.row_highlight_low, self.row_highlight_high) {
note >= lo && note <= hi
} else {
false
}
}
pub fn row_highlight_range(&self) -> Option<(u8, u8)> {
match (self.row_highlight_low, self.row_highlight_high) {
(Some(lo), Some(hi)) => Some((lo, hi)),
_ => None,
}
}
pub fn clear_all_highlights(&mut self) {
self.clear_highlight();
self.clear_row_highlight();
}
pub fn is_highlighted(&self, col: usize) -> bool {
if let (Some(start), Some(end)) = (self.highlight_start, self.highlight_end) {
col >= start && col <= end
} else {
false
}
}
pub fn highlight_range(&self) -> Option<(usize, usize)> {
match (self.highlight_start, self.highlight_end) {
(Some(s), Some(e)) => Some((s.min(e), s.max(e))),
_ => None,
}
}
pub fn set_view_height(&mut self, h: u8) {
self.view_height = h.max(1);
}
pub fn set_column_count(&mut self, count: usize) {
self.column_count = count.max(1);
if self.column >= self.column_count {
self.column = self.column_count - 1;
}
}
pub fn column_display(&self) -> usize {
self.column + 1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn focus_hierarchy() {
let mut pr = PianoRollState::new();
assert_eq!(pr.focus, PianoRollFocus::Navigation);
pr.enter(vec![]);
assert_eq!(pr.focus, PianoRollFocus::Selected);
pr.enter(vec![]);
assert_eq!(pr.focus, PianoRollFocus::Selected);
pr.enter_row();
assert_eq!(pr.focus, PianoRollFocus::Row);
pr.escape();
assert_eq!(pr.focus, PianoRollFocus::Selected);
pr.escape();
assert_eq!(pr.focus, PianoRollFocus::Navigation);
}
#[test]
fn column_navigation() {
let mut pr = PianoRollState::new();
pr.column_count = 16;
pr.column = 0;
pr.move_column_right();
assert_eq!(pr.column, 1);
pr.move_column_left();
assert_eq!(pr.column, 0);
pr.move_column_left();
assert_eq!(pr.column, 0);
pr.column = 15;
pr.move_column_right();
assert_eq!(pr.column, 15); }
#[test]
fn digit_jump() {
let mut pr = PianoRollState::new();
pr.column_count = 16;
assert!(pr.type_digit('5'));
assert_eq!(pr.column, 4);
assert!(!pr.type_digit('1'));
assert!(pr.type_digit('2'));
assert_eq!(pr.column, 11);
assert!(pr.type_digit('9'));
assert_eq!(pr.column, 8);
pr.type_digit('1');
assert!(pr.commit_digits());
assert_eq!(pr.column, 0);
}
#[test]
fn can_escape() {
let mut pr = PianoRollState::new();
assert!(!pr.can_escape());
pr.enter(vec![]);
assert!(pr.can_escape());
pr.enter(vec![]);
assert!(pr.can_escape()); }
#[test]
fn note_scroll() {
let mut pr = PianoRollState::new();
pr.view_height = 10;
pr.view_bottom_note = 50;
pr.cursor_note = 55;
for _ in 0..10 {
pr.move_up();
}
assert!(pr.cursor_note >= pr.view_bottom_note);
assert!(pr.cursor_note < pr.view_bottom_note + pr.view_height);
}
}