1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum ClipViewFocus {
6 FxPanel,
7 PianoRoll,
8}
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum FxPanelTab {
13 TrackFx,
14 Synth,
15}
16
17impl FxPanelTab {
18 pub fn label(self) -> &'static str {
19 match self {
20 Self::TrackFx => "trk fx",
21 Self::Synth => "synth",
22 }
23 }
24
25 pub fn next(self) -> Self {
26 match self {
27 Self::TrackFx => Self::Synth,
28 Self::Synth => Self::TrackFx,
29 }
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ClipTab {
36 InstConfig,
37 PianoRoll,
38 Settings,
39 Sequencer,
43}
44
45impl ClipTab {
46 pub fn label(self) -> &'static str {
47 match self {
48 Self::InstConfig => "inst",
49 Self::PianoRoll => "piano",
50 Self::Settings => "settings",
51 Self::Sequencer => "seq",
52 }
53 }
54
55 pub fn next(self) -> Self {
56 match self {
57 Self::InstConfig => Self::PianoRoll,
58 Self::PianoRoll => Self::Settings,
59 Self::Settings => Self::InstConfig,
60 Self::Sequencer => Self::InstConfig,
61 }
62 }
63
64 pub const ALL: &[ClipTab] = &[Self::InstConfig, Self::PianoRoll, Self::Settings];
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum GridResolution {
71 Quarter,
72 Eighth,
73 Sixteenth,
74 ThirtySecond,
75 QuarterT,
76 EighthT,
77 SixteenthT,
78}
79
80impl GridResolution {
81 pub fn subdivisions_per_beat(self) -> f64 {
85 match self {
86 Self::Quarter => 1.0,
87 Self::Eighth => 2.0,
88 Self::Sixteenth => 4.0,
89 Self::ThirtySecond => 8.0,
90 Self::QuarterT => 1.5, Self::EighthT => 3.0,
92 Self::SixteenthT => 6.0,
93 }
94 }
95
96 pub fn step_frac(self, total_beats: usize) -> f64 {
98 if total_beats == 0 { return 0.25; }
99 1.0 / (total_beats as f64 * self.subdivisions_per_beat())
100 }
101
102 pub fn snap(self, frac: f64, total_beats: usize) -> f64 {
104 let step = self.step_frac(total_beats);
105 if step <= 0.0 { return frac; }
106 (frac / step).round() * step
107 }
108
109 pub fn label(self) -> &'static str {
110 match self {
111 Self::Quarter => "1/4",
112 Self::Eighth => "1/8",
113 Self::Sixteenth => "1/16",
114 Self::ThirtySecond => "1/32",
115 Self::QuarterT => "1/4T",
116 Self::EighthT => "1/8T",
117 Self::SixteenthT => "1/16T",
118 }
119 }
120
121 pub fn next(self) -> Self {
122 match self {
123 Self::Quarter => Self::Eighth,
124 Self::Eighth => Self::Sixteenth,
125 Self::Sixteenth => Self::ThirtySecond,
126 Self::ThirtySecond => Self::QuarterT,
127 Self::QuarterT => Self::EighthT,
128 Self::EighthT => Self::SixteenthT,
129 Self::SixteenthT => Self::Quarter,
130 }
131 }
132
133 pub fn prev(self) -> Self {
134 match self {
135 Self::Quarter => Self::SixteenthT,
136 Self::Eighth => Self::Quarter,
137 Self::Sixteenth => Self::Eighth,
138 Self::ThirtySecond => Self::Sixteenth,
139 Self::QuarterT => Self::ThirtySecond,
140 Self::EighthT => Self::QuarterT,
141 Self::SixteenthT => Self::EighthT,
142 }
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum EditSubMode {
150 Navigate,
152 Selecting,
154 Moving,
156}
157
158#[derive(Debug)]
159pub struct ClipViewState {
160 pub focus: ClipViewFocus,
161 pub fx_panel_tab: FxPanelTab,
162 pub clip_tab: ClipTab,
163 pub piano_roll: PianoRollState,
164 pub fx_cursor: usize,
165 pub synth_param_cursor: usize,
166 pub inst_config_cursor: usize,
168 pub sequencer: SequencerView,
172}
173
174impl Default for ClipViewState {
175 fn default() -> Self { Self::new() }
176}
177
178impl ClipViewState {
179 pub fn new() -> Self {
180 Self {
181 focus: ClipViewFocus::PianoRoll,
182 fx_panel_tab: FxPanelTab::TrackFx,
183 clip_tab: ClipTab::PianoRoll,
184 piano_roll: PianoRollState::new(),
185 fx_cursor: 0,
186 synth_param_cursor: 0,
187 inst_config_cursor: 0,
188 sequencer: SequencerView::new(),
189 }
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
202pub enum SeqBand {
203 #[default]
205 Grid,
206 Step,
208 Pattern,
210 Slots,
212}
213
214impl SeqBand {
215 pub const ALL: [SeqBand; 4] = [Self::Grid, Self::Step, Self::Pattern, Self::Slots];
216
217 pub fn index(self) -> usize {
218 match self {
219 Self::Grid => 0,
220 Self::Step => 1,
221 Self::Pattern => 2,
222 Self::Slots => 3,
223 }
224 }
225
226 pub fn label(self) -> &'static str {
227 match self {
228 Self::Grid => "grid",
229 Self::Step => "step",
230 Self::Pattern => "pattern",
231 Self::Slots => "slots",
232 }
233 }
234
235 pub fn stepped(self, delta: i32) -> Self {
239 let target = (self.index() as i32 + delta).clamp(0, Self::ALL.len() as i32 - 1);
240 Self::ALL[target as usize]
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub enum SeqKnob {
252 Pitch,
255 Chord,
256 Voicing,
257 RootBelow,
259 Gate,
260 Voice,
263 Mute,
264 Solo,
265 Length,
267 Rate,
268 Swing,
269 DefaultGate,
271 BaseVelocity,
272 AccentVelocity,
273 Mode,
274 Tonic,
275 Switch,
277 Child,
279}
280
281impl SeqKnob {
282 pub fn label(self) -> &'static str {
283 match self {
284 Self::Pitch => "pitch",
285 Self::Chord => "chord",
286 Self::Voicing => "voicing",
287 Self::RootBelow => "root\u{2193}",
288 Self::Gate | Self::DefaultGate => "gate",
289 Self::Voice => "sound",
290 Self::Mute => "mute",
291 Self::Solo => "solo",
292 Self::Length => "steps",
293 Self::Rate => "rate",
294 Self::Swing => "swing",
295 Self::BaseVelocity => "base",
296 Self::AccentVelocity => "accent",
297 Self::Mode => "mode",
298 Self::Tonic => "key",
299 Self::Switch => "switch",
300 Self::Child => "child",
301 }
302 }
303}
304
305#[derive(Debug, Clone, PartialEq, Eq, Default)]
312pub struct SequencerView {
313 pub band: SeqBand,
314 pub knob: usize,
316 pub locked: bool,
319 pub copy_from: Option<u8>,
321 pub digits: String,
323}
324
325impl SequencerView {
326 pub fn new() -> Self {
327 Self::default()
328 }
329
330 pub fn move_band(&mut self, delta: i32) {
332 if self.locked {
333 return;
334 }
335 let next = self.band.stepped(delta);
336 if next != self.band {
337 self.band = next;
338 self.knob = 0;
339 self.digits.clear();
340 }
341 }
342
343 pub fn focus_band(&mut self, band: SeqBand) {
345 self.band = band;
346 self.knob = 0;
347 self.locked = false;
348 self.digits.clear();
349 }
350
351 pub fn move_knob(&mut self, delta: i32, count: usize) {
355 if count == 0 {
356 self.knob = 0;
357 return;
358 }
359 self.knob = (self.knob as i32 + delta).clamp(0, count as i32 - 1) as usize;
360 }
361
362 pub fn type_digit(&mut self, ch: char, max: usize) -> Option<usize> {
368 self.digits.push(ch);
369 let Ok(number) = self.digits.parse::<usize>() else {
370 self.digits.clear();
371 return None;
372 };
373 if number == 0 || number > max {
374 self.digits.clear();
375 return None;
376 }
377 if number * 10 > max || self.digits.len() >= 2 {
378 self.digits.clear();
379 return Some(number);
380 }
381 None
382 }
383}
384
385#[derive(Debug, Clone, Copy, PartialEq, Eq)]
400pub enum PianoRollFocus {
401 Navigation,
404 Selected,
407 Row,
410}
411
412#[derive(Debug)]
413pub struct PianoRollState {
414 pub cursor_note: u8,
415 pub scroll_x: usize,
416 pub view_bottom_note: u8,
417 pub view_height: u8,
418 pub focus: PianoRollFocus,
420 pub column: usize,
422 pub column_count: usize,
424 pub total_beats: usize,
426 pub selected_note_indices: Vec<usize>,
429 column_digits: String,
431 pub highlight_start: Option<usize>,
434 pub highlight_end: Option<usize>,
435 pub visible_columns: usize,
437 pub yank_buffer: Vec<phosphor_core::clip::NoteSnapshot>,
440 pub yank_columns: usize,
442 pub row_highlight_low: Option<u8>,
444 pub row_highlight_high: Option<u8>,
445 pub highlight_locked: bool,
447 pub edit_mode: bool,
449 pub edit_cursor: usize,
451 pub edit_selected: Vec<usize>,
453 pub edit_sub: EditSubMode,
454 pub grid: GridResolution,
456 pub snap_enabled: bool,
457 pub default_velocity: u8,
458 pub settings_cursor: usize,
460}
461
462impl Default for PianoRollState {
463 fn default() -> Self { Self::new() }
464}
465
466impl PianoRollState {
467 pub fn new() -> Self {
468 Self {
469 cursor_note: 60,
470 scroll_x: 0,
471 view_bottom_note: 48,
472 view_height: 24,
473 focus: PianoRollFocus::Navigation,
474 column: 0,
475 column_count: 16,
476 total_beats: 4,
477 selected_note_indices: Vec::new(),
478 column_digits: String::new(),
479 highlight_start: None,
480 highlight_end: None,
481 visible_columns: 16,
482 row_highlight_low: None,
483 row_highlight_high: None,
484 yank_buffer: Vec::new(),
485 yank_columns: 0,
486 highlight_locked: false,
487 edit_mode: false,
488 edit_cursor: 0,
489 edit_selected: Vec::new(),
490 edit_sub: EditSubMode::Navigate,
491 grid: GridResolution::Eighth,
492 snap_enabled: true,
493 default_velocity: 100,
494 settings_cursor: 0,
495 }
496 }
497
498 pub fn enter(&mut self, note_indices: Vec<usize>) {
503 match self.focus {
504 PianoRollFocus::Navigation => {
505 self.focus = PianoRollFocus::Selected;
506 self.selected_note_indices = note_indices;
507 }
508 PianoRollFocus::Selected | PianoRollFocus::Row => {}
509 }
510 }
511
512 pub fn enter_row(&mut self) {
514 self.focus = PianoRollFocus::Row;
515 }
516
517 pub fn escape(&mut self) {
518 match self.focus {
519 PianoRollFocus::Row => {
520 self.focus = PianoRollFocus::Selected;
521 }
522 PianoRollFocus::Selected => {
523 self.focus = PianoRollFocus::Navigation;
524 self.column_digits.clear();
525 }
526 PianoRollFocus::Navigation => {
527 }
529 }
530 }
531
532 pub fn can_escape(&self) -> bool {
534 self.focus != PianoRollFocus::Navigation
535 }
536
537 pub fn move_up(&mut self) {
540 if self.cursor_note < 127 {
541 self.cursor_note += 1;
542 let top = self.view_bottom_note.saturating_add(self.view_height);
543 if self.cursor_note >= top {
544 self.view_bottom_note = self.cursor_note - self.view_height + 1;
545 }
546 }
547 }
548
549 pub fn move_down(&mut self) {
550 if self.cursor_note > 0 {
551 self.cursor_note -= 1;
552 if self.cursor_note < self.view_bottom_note {
553 self.view_bottom_note = self.cursor_note;
554 }
555 }
556 }
557
558 pub fn move_column_left(&mut self) {
561 if self.column > 0 {
562 self.column -= 1;
563 if self.column < self.scroll_x {
565 self.scroll_x = self.column;
566 }
567 }
568 }
569
570 pub fn move_column_right(&mut self) {
571 if self.column + 1 < self.column_count {
572 self.column += 1;
573 if self.column >= self.scroll_x + self.visible_columns && self.visible_columns > 0 {
575 self.scroll_x = self.column + 1 - self.visible_columns;
576 }
577 }
578 }
579
580 pub fn type_digit(&mut self, ch: char) -> bool {
582 self.column_digits.push(ch);
583 if let Ok(num) = self.column_digits.parse::<usize>() {
584 if num >= 1 && num <= self.column_count {
585 let could_grow = num * 10 <= self.column_count;
587 if !could_grow || self.column_digits.len() >= 2 {
588 self.column = num - 1;
589 self.column_digits.clear();
590 self.ensure_column_visible();
592 return true;
593 }
594 return false;
596 }
597 }
598 self.column_digits.clear();
600 false
601 }
602
603 pub fn commit_digits(&mut self) -> bool {
605 if let Ok(num) = self.column_digits.parse::<usize>() {
606 if num >= 1 && num <= self.column_count {
607 self.column = num - 1;
608 self.column_digits.clear();
609 self.ensure_column_visible();
610 return true;
611 }
612 }
613 self.column_digits.clear();
614 false
615 }
616
617 pub fn ensure_column_visible(&mut self) {
619 if self.visible_columns == 0 { return; }
620 if self.column < self.scroll_x {
621 self.scroll_x = self.column;
622 } else if self.column >= self.scroll_x + self.visible_columns {
623 self.scroll_x = self.column + 1 - self.visible_columns;
624 }
625 }
626
627 pub fn column_digits_display(&self) -> &str {
628 &self.column_digits
629 }
630
631 pub fn start_highlight(&mut self) {
636 if let (Some(s), Some(e)) = (self.highlight_start, self.highlight_end) {
637 if s == e && s == self.column {
638 self.clear_highlight();
640 return;
641 }
642 }
643 if self.highlight_start.is_none() {
644 self.highlight_start = Some(self.column);
645 self.highlight_end = Some(self.column);
646 }
647 }
648
649 pub fn highlight_left(&mut self) {
651 if let (Some(start), Some(end)) = (self.highlight_start, self.highlight_end) {
652 if self.column > 0 {
653 self.column -= 1;
654 }
655 let new_start = self.column.min(start);
657 let new_end = self.column.max(end);
658 self.highlight_start = Some(new_start);
659 self.highlight_end = Some(new_end);
660 if self.column >= start {
662 self.highlight_end = Some(self.column);
663 } else {
664 self.highlight_start = Some(self.column);
665 }
666 }
667 }
668
669 pub fn highlight_right(&mut self) {
671 if let (Some(start), Some(end)) = (self.highlight_start, self.highlight_end) {
672 if self.column + 1 < self.column_count {
673 self.column += 1;
674 }
675 let new_start = self.column.min(start);
676 let new_end = self.column.max(end);
677 self.highlight_start = Some(new_start);
678 self.highlight_end = Some(new_end);
679 if self.column <= end {
680 self.highlight_start = Some(self.column);
681 } else {
682 self.highlight_end = Some(self.column);
683 }
684 }
685 }
686
687 pub fn clear_highlight(&mut self) {
689 self.highlight_start = None;
690 self.highlight_end = None;
691 }
692
693 pub fn start_row_highlight(&mut self) {
697 if let (Some(lo), Some(hi)) = (self.row_highlight_low, self.row_highlight_high) {
698 if lo == hi && lo == self.cursor_note {
699 self.clear_row_highlight();
700 return;
701 }
702 }
703 if self.row_highlight_low.is_none() {
704 self.row_highlight_low = Some(self.cursor_note);
705 self.row_highlight_high = Some(self.cursor_note);
706 }
707 }
708
709 pub fn highlight_down(&mut self) {
711 self.start_row_highlight();
712 if self.cursor_note > 0 {
713 self.cursor_note -= 1;
714 if self.cursor_note < self.view_bottom_note {
715 self.view_bottom_note = self.cursor_note;
716 }
717 }
718 if let Some(lo) = self.row_highlight_low {
719 self.row_highlight_low = Some(self.cursor_note.min(lo));
720 }
721 if let Some(hi) = self.row_highlight_high {
722 self.row_highlight_high = Some(self.cursor_note.max(hi));
723 }
724 }
725
726 pub fn highlight_up(&mut self) {
728 self.start_row_highlight();
729 if self.cursor_note < 127 {
730 self.cursor_note += 1;
731 let top = self.view_bottom_note.saturating_add(self.view_height);
732 if self.cursor_note >= top {
733 self.view_bottom_note = self.cursor_note - self.view_height + 1;
734 }
735 }
736 if let Some(lo) = self.row_highlight_low {
737 self.row_highlight_low = Some(self.cursor_note.min(lo));
738 }
739 if let Some(hi) = self.row_highlight_high {
740 self.row_highlight_high = Some(self.cursor_note.max(hi));
741 }
742 }
743
744 pub fn clear_row_highlight(&mut self) {
745 self.row_highlight_low = None;
746 self.row_highlight_high = None;
747 }
748
749 pub fn is_row_highlighted(&self, note: u8) -> bool {
751 if let (Some(lo), Some(hi)) = (self.row_highlight_low, self.row_highlight_high) {
752 note >= lo && note <= hi
753 } else {
754 false
755 }
756 }
757
758 pub fn row_highlight_range(&self) -> Option<(u8, u8)> {
760 match (self.row_highlight_low, self.row_highlight_high) {
761 (Some(lo), Some(hi)) => Some((lo, hi)),
762 _ => None,
763 }
764 }
765
766 pub fn clear_all_highlights(&mut self) {
768 self.clear_highlight();
769 self.clear_row_highlight();
770 self.highlight_locked = false;
771 }
772
773 pub fn is_highlighted(&self, col: usize) -> bool {
775 if let (Some(start), Some(end)) = (self.highlight_start, self.highlight_end) {
776 col >= start && col <= end
777 } else {
778 false
779 }
780 }
781
782 pub fn highlight_range(&self) -> Option<(usize, usize)> {
784 match (self.highlight_start, self.highlight_end) {
785 (Some(s), Some(e)) => Some((s.min(e), s.max(e))),
786 _ => None,
787 }
788 }
789
790 pub fn set_view_height(&mut self, h: u8) {
791 self.view_height = h.max(1);
792 }
793
794 pub fn set_column_count(&mut self, count: usize) {
795 self.column_count = count.max(1);
796 if self.column >= self.column_count {
797 self.column = self.column_count - 1;
798 }
799 }
800
801 pub fn update_column_count(&mut self) {
803 let cols = (self.total_beats as f64 * self.grid.subdivisions_per_beat()).round() as usize;
804 self.column_count = cols.max(1);
805 if self.column >= self.column_count {
806 self.column = self.column_count.saturating_sub(1);
807 }
808 }
809
810 pub fn has_highlights(&self) -> bool {
812 self.highlight_start.is_some() || self.row_highlight_low.is_some()
813 }
814
815 pub fn column_display(&self) -> usize {
817 self.column + 1
818 }
819}
820
821#[cfg(test)]
822mod tests {
823 use super::*;
824
825 #[test]
826 fn focus_hierarchy() {
827 let mut pr = PianoRollState::new();
828 assert_eq!(pr.focus, PianoRollFocus::Navigation);
829
830 pr.enter(vec![]);
831 assert_eq!(pr.focus, PianoRollFocus::Selected);
832
833 pr.enter(vec![]);
835 assert_eq!(pr.focus, PianoRollFocus::Selected);
836
837 pr.enter_row();
839 assert_eq!(pr.focus, PianoRollFocus::Row);
840
841 pr.escape();
842 assert_eq!(pr.focus, PianoRollFocus::Selected);
843
844 pr.escape();
845 assert_eq!(pr.focus, PianoRollFocus::Navigation);
846 }
847
848 #[test]
849 fn column_navigation() {
850 let mut pr = PianoRollState::new();
851 pr.column_count = 16;
852 pr.column = 0;
853
854 pr.move_column_right();
855 assert_eq!(pr.column, 1);
856
857 pr.move_column_left();
858 assert_eq!(pr.column, 0);
859
860 pr.move_column_left();
861 assert_eq!(pr.column, 0); pr.column = 15;
864 pr.move_column_right();
865 assert_eq!(pr.column, 15); }
867
868 #[test]
869 fn digit_jump() {
870 let mut pr = PianoRollState::new();
871 pr.column_count = 16;
872
873 assert!(pr.type_digit('5'));
876 assert_eq!(pr.column, 4); assert!(!pr.type_digit('1'));
880 assert!(pr.type_digit('2'));
882 assert_eq!(pr.column, 11); assert!(pr.type_digit('9'));
886 assert_eq!(pr.column, 8);
887
888 pr.type_digit('1');
890 assert!(pr.commit_digits());
891 assert_eq!(pr.column, 0);
892 }
893
894 #[test]
895 fn can_escape() {
896 let mut pr = PianoRollState::new();
897 assert!(!pr.can_escape()); pr.enter(vec![]);
900 assert!(pr.can_escape()); pr.enter(vec![]);
903 assert!(pr.can_escape()); }
905
906 #[test]
907 fn note_scroll() {
908 let mut pr = PianoRollState::new();
909 pr.view_height = 10;
910 pr.view_bottom_note = 50;
911 pr.cursor_note = 55;
912
913 for _ in 0..10 {
915 pr.move_up();
916 }
917 assert!(pr.cursor_note >= pr.view_bottom_note);
919 assert!(pr.cursor_note < pr.view_bottom_note + pr.view_height);
920 }
921}