#[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,
Settings,
Sequencer,
}
impl ClipTab {
pub fn label(self) -> &'static str {
match self {
Self::InstConfig => "inst",
Self::PianoRoll => "piano",
Self::Settings => "settings",
Self::Sequencer => "seq",
}
}
pub fn next(self) -> Self {
match self {
Self::InstConfig => Self::PianoRoll,
Self::PianoRoll => Self::Settings,
Self::Settings => Self::InstConfig,
Self::Sequencer => Self::InstConfig,
}
}
pub const ALL: &[ClipTab] = &[Self::InstConfig, Self::PianoRoll, Self::Settings];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GridResolution {
Quarter,
Eighth,
Sixteenth,
ThirtySecond,
QuarterT,
EighthT,
SixteenthT,
}
impl GridResolution {
pub fn subdivisions_per_beat(self) -> f64 {
match self {
Self::Quarter => 1.0,
Self::Eighth => 2.0,
Self::Sixteenth => 4.0,
Self::ThirtySecond => 8.0,
Self::QuarterT => 1.5, Self::EighthT => 3.0,
Self::SixteenthT => 6.0,
}
}
pub fn step_frac(self, total_beats: usize) -> f64 {
if total_beats == 0 { return 0.25; }
1.0 / (total_beats as f64 * self.subdivisions_per_beat())
}
pub fn snap(self, frac: f64, total_beats: usize) -> f64 {
let step = self.step_frac(total_beats);
if step <= 0.0 { return frac; }
(frac / step).round() * step
}
pub fn label(self) -> &'static str {
match self {
Self::Quarter => "1/4",
Self::Eighth => "1/8",
Self::Sixteenth => "1/16",
Self::ThirtySecond => "1/32",
Self::QuarterT => "1/4T",
Self::EighthT => "1/8T",
Self::SixteenthT => "1/16T",
}
}
pub fn next(self) -> Self {
match self {
Self::Quarter => Self::Eighth,
Self::Eighth => Self::Sixteenth,
Self::Sixteenth => Self::ThirtySecond,
Self::ThirtySecond => Self::QuarterT,
Self::QuarterT => Self::EighthT,
Self::EighthT => Self::SixteenthT,
Self::SixteenthT => Self::Quarter,
}
}
pub fn prev(self) -> Self {
match self {
Self::Quarter => Self::SixteenthT,
Self::Eighth => Self::Quarter,
Self::Sixteenth => Self::Eighth,
Self::ThirtySecond => Self::Sixteenth,
Self::QuarterT => Self::ThirtySecond,
Self::EighthT => Self::QuarterT,
Self::SixteenthT => Self::EighthT,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EditSubMode {
Navigate,
Selecting,
Moving,
}
#[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,
pub sequencer: SequencerView,
}
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,
sequencer: SequencerView::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SeqBand {
#[default]
Grid,
Step,
Pattern,
Slots,
}
impl SeqBand {
pub const ALL: [SeqBand; 4] = [Self::Grid, Self::Step, Self::Pattern, Self::Slots];
pub fn index(self) -> usize {
match self {
Self::Grid => 0,
Self::Step => 1,
Self::Pattern => 2,
Self::Slots => 3,
}
}
pub fn label(self) -> &'static str {
match self {
Self::Grid => "grid",
Self::Step => "step",
Self::Pattern => "pattern",
Self::Slots => "slots",
}
}
pub fn stepped(self, delta: i32) -> Self {
let target = (self.index() as i32 + delta).clamp(0, Self::ALL.len() as i32 - 1);
Self::ALL[target as usize]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeqKnob {
Pitch,
Chord,
Voicing,
RootBelow,
Gate,
Voice,
Mute,
Solo,
Length,
Rate,
Swing,
DefaultGate,
BaseVelocity,
AccentVelocity,
Mode,
Tonic,
Switch,
Child,
}
impl SeqKnob {
pub fn label(self) -> &'static str {
match self {
Self::Pitch => "pitch",
Self::Chord => "chord",
Self::Voicing => "voicing",
Self::RootBelow => "root\u{2193}",
Self::Gate | Self::DefaultGate => "gate",
Self::Voice => "sound",
Self::Mute => "mute",
Self::Solo => "solo",
Self::Length => "steps",
Self::Rate => "rate",
Self::Swing => "swing",
Self::BaseVelocity => "base",
Self::AccentVelocity => "accent",
Self::Mode => "mode",
Self::Tonic => "key",
Self::Switch => "switch",
Self::Child => "child",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SequencerView {
pub band: SeqBand,
pub knob: usize,
pub locked: bool,
pub copy_from: Option<u8>,
pub digits: String,
}
impl SequencerView {
pub fn new() -> Self {
Self::default()
}
pub fn move_band(&mut self, delta: i32) {
if self.locked {
return;
}
let next = self.band.stepped(delta);
if next != self.band {
self.band = next;
self.knob = 0;
self.digits.clear();
}
}
pub fn focus_band(&mut self, band: SeqBand) {
self.band = band;
self.knob = 0;
self.locked = false;
self.digits.clear();
}
pub fn move_knob(&mut self, delta: i32, count: usize) {
if count == 0 {
self.knob = 0;
return;
}
self.knob = (self.knob as i32 + delta).clamp(0, count as i32 - 1) as usize;
}
pub fn type_digit(&mut self, ch: char, max: usize) -> Option<usize> {
self.digits.push(ch);
let Ok(number) = self.digits.parse::<usize>() else {
self.digits.clear();
return None;
};
if number == 0 || number > max {
self.digits.clear();
return None;
}
if number * 10 > max || self.digits.len() >= 2 {
self.digits.clear();
return Some(number);
}
None
}
}
#[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 total_beats: 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>,
pub highlight_locked: bool,
pub edit_mode: bool,
pub edit_cursor: usize,
pub edit_selected: Vec<usize>,
pub edit_sub: EditSubMode,
pub grid: GridResolution,
pub snap_enabled: bool,
pub default_velocity: u8,
pub settings_cursor: usize,
}
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,
total_beats: 4,
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,
highlight_locked: false,
edit_mode: false,
edit_cursor: 0,
edit_selected: Vec::new(),
edit_sub: EditSubMode::Navigate,
grid: GridResolution::Eighth,
snap_enabled: true,
default_velocity: 100,
settings_cursor: 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();
self.highlight_locked = false;
}
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 update_column_count(&mut self) {
let cols = (self.total_beats as f64 * self.grid.subdivisions_per_beat()).round() as usize;
self.column_count = cols.max(1);
if self.column >= self.column_count {
self.column = self.column_count.saturating_sub(1);
}
}
pub fn has_highlights(&self) -> bool {
self.highlight_start.is_some() || self.row_highlight_low.is_some()
}
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);
}
}