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}
278
279impl SeqKnob {
280 pub fn label(self) -> &'static str {
281 match self {
282 Self::Pitch => "pitch",
283 Self::Chord => "chord",
284 Self::Voicing => "voicing",
285 Self::RootBelow => "root\u{2193}",
286 Self::Gate | Self::DefaultGate => "gate",
287 Self::Voice => "sound",
288 Self::Mute => "mute",
289 Self::Solo => "solo",
290 Self::Length => "steps",
291 Self::Rate => "rate",
292 Self::Swing => "swing",
293 Self::BaseVelocity => "base",
294 Self::AccentVelocity => "accent",
295 Self::Mode => "mode",
296 Self::Tonic => "key",
297 Self::Switch => "switch",
298 }
299 }
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Default)]
309pub struct SequencerView {
310 pub band: SeqBand,
311 pub knob: usize,
313 pub locked: bool,
316 pub copy_from: Option<u8>,
318 pub digits: String,
320}
321
322impl SequencerView {
323 pub fn new() -> Self {
324 Self::default()
325 }
326
327 pub fn move_band(&mut self, delta: i32) {
329 if self.locked {
330 return;
331 }
332 let next = self.band.stepped(delta);
333 if next != self.band {
334 self.band = next;
335 self.knob = 0;
336 self.digits.clear();
337 }
338 }
339
340 pub fn focus_band(&mut self, band: SeqBand) {
342 self.band = band;
343 self.knob = 0;
344 self.locked = false;
345 self.digits.clear();
346 }
347
348 pub fn move_knob(&mut self, delta: i32, count: usize) {
352 if count == 0 {
353 self.knob = 0;
354 return;
355 }
356 self.knob = (self.knob as i32 + delta).clamp(0, count as i32 - 1) as usize;
357 }
358
359 pub fn type_digit(&mut self, ch: char, max: usize) -> Option<usize> {
365 self.digits.push(ch);
366 let Ok(number) = self.digits.parse::<usize>() else {
367 self.digits.clear();
368 return None;
369 };
370 if number == 0 || number > max {
371 self.digits.clear();
372 return None;
373 }
374 if number * 10 > max || self.digits.len() >= 2 {
375 self.digits.clear();
376 return Some(number);
377 }
378 None
379 }
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397pub enum PianoRollFocus {
398 Navigation,
401 Selected,
404 Row,
407}
408
409#[derive(Debug)]
410pub struct PianoRollState {
411 pub cursor_note: u8,
412 pub scroll_x: usize,
413 pub view_bottom_note: u8,
414 pub view_height: u8,
415 pub focus: PianoRollFocus,
417 pub column: usize,
419 pub column_count: usize,
421 pub total_beats: usize,
423 pub selected_note_indices: Vec<usize>,
426 column_digits: String,
428 pub highlight_start: Option<usize>,
431 pub highlight_end: Option<usize>,
432 pub visible_columns: usize,
434 pub yank_buffer: Vec<phosphor_core::clip::NoteSnapshot>,
437 pub yank_columns: usize,
439 pub row_highlight_low: Option<u8>,
441 pub row_highlight_high: Option<u8>,
442 pub highlight_locked: bool,
444 pub edit_mode: bool,
446 pub edit_cursor: usize,
448 pub edit_selected: Vec<usize>,
450 pub edit_sub: EditSubMode,
451 pub grid: GridResolution,
453 pub snap_enabled: bool,
454 pub default_velocity: u8,
455 pub settings_cursor: usize,
457}
458
459impl Default for PianoRollState {
460 fn default() -> Self { Self::new() }
461}
462
463impl PianoRollState {
464 pub fn new() -> Self {
465 Self {
466 cursor_note: 60,
467 scroll_x: 0,
468 view_bottom_note: 48,
469 view_height: 24,
470 focus: PianoRollFocus::Navigation,
471 column: 0,
472 column_count: 16,
473 total_beats: 4,
474 selected_note_indices: Vec::new(),
475 column_digits: String::new(),
476 highlight_start: None,
477 highlight_end: None,
478 visible_columns: 16,
479 row_highlight_low: None,
480 row_highlight_high: None,
481 yank_buffer: Vec::new(),
482 yank_columns: 0,
483 highlight_locked: false,
484 edit_mode: false,
485 edit_cursor: 0,
486 edit_selected: Vec::new(),
487 edit_sub: EditSubMode::Navigate,
488 grid: GridResolution::Eighth,
489 snap_enabled: true,
490 default_velocity: 100,
491 settings_cursor: 0,
492 }
493 }
494
495 pub fn enter(&mut self, note_indices: Vec<usize>) {
500 match self.focus {
501 PianoRollFocus::Navigation => {
502 self.focus = PianoRollFocus::Selected;
503 self.selected_note_indices = note_indices;
504 }
505 PianoRollFocus::Selected | PianoRollFocus::Row => {}
506 }
507 }
508
509 pub fn enter_row(&mut self) {
511 self.focus = PianoRollFocus::Row;
512 }
513
514 pub fn escape(&mut self) {
515 match self.focus {
516 PianoRollFocus::Row => {
517 self.focus = PianoRollFocus::Selected;
518 }
519 PianoRollFocus::Selected => {
520 self.focus = PianoRollFocus::Navigation;
521 self.column_digits.clear();
522 }
523 PianoRollFocus::Navigation => {
524 }
526 }
527 }
528
529 pub fn can_escape(&self) -> bool {
531 self.focus != PianoRollFocus::Navigation
532 }
533
534 pub fn move_up(&mut self) {
537 if self.cursor_note < 127 {
538 self.cursor_note += 1;
539 let top = self.view_bottom_note.saturating_add(self.view_height);
540 if self.cursor_note >= top {
541 self.view_bottom_note = self.cursor_note - self.view_height + 1;
542 }
543 }
544 }
545
546 pub fn move_down(&mut self) {
547 if self.cursor_note > 0 {
548 self.cursor_note -= 1;
549 if self.cursor_note < self.view_bottom_note {
550 self.view_bottom_note = self.cursor_note;
551 }
552 }
553 }
554
555 pub fn move_column_left(&mut self) {
558 if self.column > 0 {
559 self.column -= 1;
560 if self.column < self.scroll_x {
562 self.scroll_x = self.column;
563 }
564 }
565 }
566
567 pub fn move_column_right(&mut self) {
568 if self.column + 1 < self.column_count {
569 self.column += 1;
570 if self.column >= self.scroll_x + self.visible_columns && self.visible_columns > 0 {
572 self.scroll_x = self.column + 1 - self.visible_columns;
573 }
574 }
575 }
576
577 pub fn type_digit(&mut self, ch: char) -> bool {
579 self.column_digits.push(ch);
580 if let Ok(num) = self.column_digits.parse::<usize>() {
581 if num >= 1 && num <= self.column_count {
582 let could_grow = num * 10 <= self.column_count;
584 if !could_grow || self.column_digits.len() >= 2 {
585 self.column = num - 1;
586 self.column_digits.clear();
587 self.ensure_column_visible();
589 return true;
590 }
591 return false;
593 }
594 }
595 self.column_digits.clear();
597 false
598 }
599
600 pub fn commit_digits(&mut self) -> bool {
602 if let Ok(num) = self.column_digits.parse::<usize>() {
603 if num >= 1 && num <= self.column_count {
604 self.column = num - 1;
605 self.column_digits.clear();
606 self.ensure_column_visible();
607 return true;
608 }
609 }
610 self.column_digits.clear();
611 false
612 }
613
614 pub fn ensure_column_visible(&mut self) {
616 if self.visible_columns == 0 { return; }
617 if self.column < self.scroll_x {
618 self.scroll_x = self.column;
619 } else if self.column >= self.scroll_x + self.visible_columns {
620 self.scroll_x = self.column + 1 - self.visible_columns;
621 }
622 }
623
624 pub fn column_digits_display(&self) -> &str {
625 &self.column_digits
626 }
627
628 pub fn start_highlight(&mut self) {
633 if let (Some(s), Some(e)) = (self.highlight_start, self.highlight_end) {
634 if s == e && s == self.column {
635 self.clear_highlight();
637 return;
638 }
639 }
640 if self.highlight_start.is_none() {
641 self.highlight_start = Some(self.column);
642 self.highlight_end = Some(self.column);
643 }
644 }
645
646 pub fn highlight_left(&mut self) {
648 if let (Some(start), Some(end)) = (self.highlight_start, self.highlight_end) {
649 if self.column > 0 {
650 self.column -= 1;
651 }
652 let new_start = self.column.min(start);
654 let new_end = self.column.max(end);
655 self.highlight_start = Some(new_start);
656 self.highlight_end = Some(new_end);
657 if self.column >= start {
659 self.highlight_end = Some(self.column);
660 } else {
661 self.highlight_start = Some(self.column);
662 }
663 }
664 }
665
666 pub fn highlight_right(&mut self) {
668 if let (Some(start), Some(end)) = (self.highlight_start, self.highlight_end) {
669 if self.column + 1 < self.column_count {
670 self.column += 1;
671 }
672 let new_start = self.column.min(start);
673 let new_end = self.column.max(end);
674 self.highlight_start = Some(new_start);
675 self.highlight_end = Some(new_end);
676 if self.column <= end {
677 self.highlight_start = Some(self.column);
678 } else {
679 self.highlight_end = Some(self.column);
680 }
681 }
682 }
683
684 pub fn clear_highlight(&mut self) {
686 self.highlight_start = None;
687 self.highlight_end = None;
688 }
689
690 pub fn start_row_highlight(&mut self) {
694 if let (Some(lo), Some(hi)) = (self.row_highlight_low, self.row_highlight_high) {
695 if lo == hi && lo == self.cursor_note {
696 self.clear_row_highlight();
697 return;
698 }
699 }
700 if self.row_highlight_low.is_none() {
701 self.row_highlight_low = Some(self.cursor_note);
702 self.row_highlight_high = Some(self.cursor_note);
703 }
704 }
705
706 pub fn highlight_down(&mut self) {
708 self.start_row_highlight();
709 if self.cursor_note > 0 {
710 self.cursor_note -= 1;
711 if self.cursor_note < self.view_bottom_note {
712 self.view_bottom_note = self.cursor_note;
713 }
714 }
715 if let Some(lo) = self.row_highlight_low {
716 self.row_highlight_low = Some(self.cursor_note.min(lo));
717 }
718 if let Some(hi) = self.row_highlight_high {
719 self.row_highlight_high = Some(self.cursor_note.max(hi));
720 }
721 }
722
723 pub fn highlight_up(&mut self) {
725 self.start_row_highlight();
726 if self.cursor_note < 127 {
727 self.cursor_note += 1;
728 let top = self.view_bottom_note.saturating_add(self.view_height);
729 if self.cursor_note >= top {
730 self.view_bottom_note = self.cursor_note - self.view_height + 1;
731 }
732 }
733 if let Some(lo) = self.row_highlight_low {
734 self.row_highlight_low = Some(self.cursor_note.min(lo));
735 }
736 if let Some(hi) = self.row_highlight_high {
737 self.row_highlight_high = Some(self.cursor_note.max(hi));
738 }
739 }
740
741 pub fn clear_row_highlight(&mut self) {
742 self.row_highlight_low = None;
743 self.row_highlight_high = None;
744 }
745
746 pub fn is_row_highlighted(&self, note: u8) -> bool {
748 if let (Some(lo), Some(hi)) = (self.row_highlight_low, self.row_highlight_high) {
749 note >= lo && note <= hi
750 } else {
751 false
752 }
753 }
754
755 pub fn row_highlight_range(&self) -> Option<(u8, u8)> {
757 match (self.row_highlight_low, self.row_highlight_high) {
758 (Some(lo), Some(hi)) => Some((lo, hi)),
759 _ => None,
760 }
761 }
762
763 pub fn clear_all_highlights(&mut self) {
765 self.clear_highlight();
766 self.clear_row_highlight();
767 self.highlight_locked = false;
768 }
769
770 pub fn is_highlighted(&self, col: usize) -> bool {
772 if let (Some(start), Some(end)) = (self.highlight_start, self.highlight_end) {
773 col >= start && col <= end
774 } else {
775 false
776 }
777 }
778
779 pub fn highlight_range(&self) -> Option<(usize, usize)> {
781 match (self.highlight_start, self.highlight_end) {
782 (Some(s), Some(e)) => Some((s.min(e), s.max(e))),
783 _ => None,
784 }
785 }
786
787 pub fn set_view_height(&mut self, h: u8) {
788 self.view_height = h.max(1);
789 }
790
791 pub fn set_column_count(&mut self, count: usize) {
792 self.column_count = count.max(1);
793 if self.column >= self.column_count {
794 self.column = self.column_count - 1;
795 }
796 }
797
798 pub fn update_column_count(&mut self) {
800 let cols = (self.total_beats as f64 * self.grid.subdivisions_per_beat()).round() as usize;
801 self.column_count = cols.max(1);
802 if self.column >= self.column_count {
803 self.column = self.column_count.saturating_sub(1);
804 }
805 }
806
807 pub fn has_highlights(&self) -> bool {
809 self.highlight_start.is_some() || self.row_highlight_low.is_some()
810 }
811
812 pub fn column_display(&self) -> usize {
814 self.column + 1
815 }
816}
817
818#[cfg(test)]
819mod tests {
820 use super::*;
821
822 #[test]
823 fn focus_hierarchy() {
824 let mut pr = PianoRollState::new();
825 assert_eq!(pr.focus, PianoRollFocus::Navigation);
826
827 pr.enter(vec![]);
828 assert_eq!(pr.focus, PianoRollFocus::Selected);
829
830 pr.enter(vec![]);
832 assert_eq!(pr.focus, PianoRollFocus::Selected);
833
834 pr.enter_row();
836 assert_eq!(pr.focus, PianoRollFocus::Row);
837
838 pr.escape();
839 assert_eq!(pr.focus, PianoRollFocus::Selected);
840
841 pr.escape();
842 assert_eq!(pr.focus, PianoRollFocus::Navigation);
843 }
844
845 #[test]
846 fn column_navigation() {
847 let mut pr = PianoRollState::new();
848 pr.column_count = 16;
849 pr.column = 0;
850
851 pr.move_column_right();
852 assert_eq!(pr.column, 1);
853
854 pr.move_column_left();
855 assert_eq!(pr.column, 0);
856
857 pr.move_column_left();
858 assert_eq!(pr.column, 0); pr.column = 15;
861 pr.move_column_right();
862 assert_eq!(pr.column, 15); }
864
865 #[test]
866 fn digit_jump() {
867 let mut pr = PianoRollState::new();
868 pr.column_count = 16;
869
870 assert!(pr.type_digit('5'));
873 assert_eq!(pr.column, 4); assert!(!pr.type_digit('1'));
877 assert!(pr.type_digit('2'));
879 assert_eq!(pr.column, 11); assert!(pr.type_digit('9'));
883 assert_eq!(pr.column, 8);
884
885 pr.type_digit('1');
887 assert!(pr.commit_digits());
888 assert_eq!(pr.column, 0);
889 }
890
891 #[test]
892 fn can_escape() {
893 let mut pr = PianoRollState::new();
894 assert!(!pr.can_escape()); pr.enter(vec![]);
897 assert!(pr.can_escape()); pr.enter(vec![]);
900 assert!(pr.can_escape()); }
902
903 #[test]
904 fn note_scroll() {
905 let mut pr = PianoRollState::new();
906 pr.view_height = 10;
907 pr.view_bottom_note = 50;
908 pr.cursor_note = 55;
909
910 for _ in 0..10 {
912 pr.move_up();
913 }
914 assert!(pr.cursor_note >= pr.view_bottom_note);
916 assert!(pr.cursor_note < pr.view_bottom_note + pr.view_height);
917 }
918}