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}
40
41impl ClipTab {
42 pub fn label(self) -> &'static str {
43 match self {
44 Self::InstConfig => "inst",
45 Self::PianoRoll => "piano",
46 Self::Settings => "settings",
47 }
48 }
49
50 pub fn next(self) -> Self {
51 match self {
52 Self::InstConfig => Self::PianoRoll,
53 Self::PianoRoll => Self::Settings,
54 Self::Settings => Self::InstConfig,
55 }
56 }
57
58 pub const ALL: &[ClipTab] = &[Self::InstConfig, Self::PianoRoll, Self::Settings];
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum GridResolution {
65 Quarter,
66 Eighth,
67 Sixteenth,
68 ThirtySecond,
69 QuarterT,
70 EighthT,
71 SixteenthT,
72}
73
74impl GridResolution {
75 pub fn subdivisions_per_beat(self) -> f64 {
79 match self {
80 Self::Quarter => 1.0,
81 Self::Eighth => 2.0,
82 Self::Sixteenth => 4.0,
83 Self::ThirtySecond => 8.0,
84 Self::QuarterT => 1.5, Self::EighthT => 3.0,
86 Self::SixteenthT => 6.0,
87 }
88 }
89
90 pub fn step_frac(self, total_beats: usize) -> f64 {
92 if total_beats == 0 { return 0.25; }
93 1.0 / (total_beats as f64 * self.subdivisions_per_beat())
94 }
95
96 pub fn snap(self, frac: f64, total_beats: usize) -> f64 {
98 let step = self.step_frac(total_beats);
99 if step <= 0.0 { return frac; }
100 (frac / step).round() * step
101 }
102
103 pub fn label(self) -> &'static str {
104 match self {
105 Self::Quarter => "1/4",
106 Self::Eighth => "1/8",
107 Self::Sixteenth => "1/16",
108 Self::ThirtySecond => "1/32",
109 Self::QuarterT => "1/4T",
110 Self::EighthT => "1/8T",
111 Self::SixteenthT => "1/16T",
112 }
113 }
114
115 pub fn next(self) -> Self {
116 match self {
117 Self::Quarter => Self::Eighth,
118 Self::Eighth => Self::Sixteenth,
119 Self::Sixteenth => Self::ThirtySecond,
120 Self::ThirtySecond => Self::QuarterT,
121 Self::QuarterT => Self::EighthT,
122 Self::EighthT => Self::SixteenthT,
123 Self::SixteenthT => Self::Quarter,
124 }
125 }
126
127 pub fn prev(self) -> Self {
128 match self {
129 Self::Quarter => Self::SixteenthT,
130 Self::Eighth => Self::Quarter,
131 Self::Sixteenth => Self::Eighth,
132 Self::ThirtySecond => Self::Sixteenth,
133 Self::QuarterT => Self::ThirtySecond,
134 Self::EighthT => Self::QuarterT,
135 Self::SixteenthT => Self::EighthT,
136 }
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum EditSubMode {
144 Navigate,
146 Selecting,
148 Moving,
150}
151
152#[derive(Debug)]
153pub struct ClipViewState {
154 pub focus: ClipViewFocus,
155 pub fx_panel_tab: FxPanelTab,
156 pub clip_tab: ClipTab,
157 pub piano_roll: PianoRollState,
158 pub fx_cursor: usize,
159 pub synth_param_cursor: usize,
160 pub inst_config_cursor: usize,
162}
163
164impl Default for ClipViewState {
165 fn default() -> Self { Self::new() }
166}
167
168impl ClipViewState {
169 pub fn new() -> Self {
170 Self {
171 focus: ClipViewFocus::PianoRoll,
172 fx_panel_tab: FxPanelTab::TrackFx,
173 clip_tab: ClipTab::PianoRoll,
174 piano_roll: PianoRollState::new(),
175 fx_cursor: 0,
176 synth_param_cursor: 0,
177 inst_config_cursor: 0,
178 }
179 }
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum PianoRollFocus {
198 Navigation,
201 Selected,
204 Row,
207}
208
209#[derive(Debug)]
210pub struct PianoRollState {
211 pub cursor_note: u8,
212 pub scroll_x: usize,
213 pub view_bottom_note: u8,
214 pub view_height: u8,
215 pub focus: PianoRollFocus,
217 pub column: usize,
219 pub column_count: usize,
221 pub total_beats: usize,
223 pub selected_note_indices: Vec<usize>,
226 column_digits: String,
228 pub highlight_start: Option<usize>,
231 pub highlight_end: Option<usize>,
232 pub visible_columns: usize,
234 pub yank_buffer: Vec<phosphor_core::clip::NoteSnapshot>,
237 pub yank_columns: usize,
239 pub row_highlight_low: Option<u8>,
241 pub row_highlight_high: Option<u8>,
242 pub highlight_locked: bool,
244 pub edit_mode: bool,
246 pub edit_cursor: usize,
248 pub edit_selected: Vec<usize>,
250 pub edit_sub: EditSubMode,
251 pub grid: GridResolution,
253 pub snap_enabled: bool,
254 pub default_velocity: u8,
255 pub settings_cursor: usize,
257}
258
259impl Default for PianoRollState {
260 fn default() -> Self { Self::new() }
261}
262
263impl PianoRollState {
264 pub fn new() -> Self {
265 Self {
266 cursor_note: 60,
267 scroll_x: 0,
268 view_bottom_note: 48,
269 view_height: 24,
270 focus: PianoRollFocus::Navigation,
271 column: 0,
272 column_count: 16,
273 total_beats: 4,
274 selected_note_indices: Vec::new(),
275 column_digits: String::new(),
276 highlight_start: None,
277 highlight_end: None,
278 visible_columns: 16,
279 row_highlight_low: None,
280 row_highlight_high: None,
281 yank_buffer: Vec::new(),
282 yank_columns: 0,
283 highlight_locked: false,
284 edit_mode: false,
285 edit_cursor: 0,
286 edit_selected: Vec::new(),
287 edit_sub: EditSubMode::Navigate,
288 grid: GridResolution::Eighth,
289 snap_enabled: true,
290 default_velocity: 100,
291 settings_cursor: 0,
292 }
293 }
294
295 pub fn enter(&mut self, note_indices: Vec<usize>) {
300 match self.focus {
301 PianoRollFocus::Navigation => {
302 self.focus = PianoRollFocus::Selected;
303 self.selected_note_indices = note_indices;
304 }
305 PianoRollFocus::Selected | PianoRollFocus::Row => {}
306 }
307 }
308
309 pub fn enter_row(&mut self) {
311 self.focus = PianoRollFocus::Row;
312 }
313
314 pub fn escape(&mut self) {
315 match self.focus {
316 PianoRollFocus::Row => {
317 self.focus = PianoRollFocus::Selected;
318 }
319 PianoRollFocus::Selected => {
320 self.focus = PianoRollFocus::Navigation;
321 self.column_digits.clear();
322 }
323 PianoRollFocus::Navigation => {
324 }
326 }
327 }
328
329 pub fn can_escape(&self) -> bool {
331 self.focus != PianoRollFocus::Navigation
332 }
333
334 pub fn move_up(&mut self) {
337 if self.cursor_note < 127 {
338 self.cursor_note += 1;
339 let top = self.view_bottom_note.saturating_add(self.view_height);
340 if self.cursor_note >= top {
341 self.view_bottom_note = self.cursor_note - self.view_height + 1;
342 }
343 }
344 }
345
346 pub fn move_down(&mut self) {
347 if self.cursor_note > 0 {
348 self.cursor_note -= 1;
349 if self.cursor_note < self.view_bottom_note {
350 self.view_bottom_note = self.cursor_note;
351 }
352 }
353 }
354
355 pub fn move_column_left(&mut self) {
358 if self.column > 0 {
359 self.column -= 1;
360 if self.column < self.scroll_x {
362 self.scroll_x = self.column;
363 }
364 }
365 }
366
367 pub fn move_column_right(&mut self) {
368 if self.column + 1 < self.column_count {
369 self.column += 1;
370 if self.column >= self.scroll_x + self.visible_columns && self.visible_columns > 0 {
372 self.scroll_x = self.column + 1 - self.visible_columns;
373 }
374 }
375 }
376
377 pub fn type_digit(&mut self, ch: char) -> bool {
379 self.column_digits.push(ch);
380 if let Ok(num) = self.column_digits.parse::<usize>() {
381 if num >= 1 && num <= self.column_count {
382 let could_grow = num * 10 <= self.column_count;
384 if !could_grow || self.column_digits.len() >= 2 {
385 self.column = num - 1;
386 self.column_digits.clear();
387 self.ensure_column_visible();
389 return true;
390 }
391 return false;
393 }
394 }
395 self.column_digits.clear();
397 false
398 }
399
400 pub fn commit_digits(&mut self) -> bool {
402 if let Ok(num) = self.column_digits.parse::<usize>() {
403 if num >= 1 && num <= self.column_count {
404 self.column = num - 1;
405 self.column_digits.clear();
406 self.ensure_column_visible();
407 return true;
408 }
409 }
410 self.column_digits.clear();
411 false
412 }
413
414 pub fn ensure_column_visible(&mut self) {
416 if self.visible_columns == 0 { return; }
417 if self.column < self.scroll_x {
418 self.scroll_x = self.column;
419 } else if self.column >= self.scroll_x + self.visible_columns {
420 self.scroll_x = self.column + 1 - self.visible_columns;
421 }
422 }
423
424 pub fn column_digits_display(&self) -> &str {
425 &self.column_digits
426 }
427
428 pub fn start_highlight(&mut self) {
433 if let (Some(s), Some(e)) = (self.highlight_start, self.highlight_end) {
434 if s == e && s == self.column {
435 self.clear_highlight();
437 return;
438 }
439 }
440 if self.highlight_start.is_none() {
441 self.highlight_start = Some(self.column);
442 self.highlight_end = Some(self.column);
443 }
444 }
445
446 pub fn highlight_left(&mut self) {
448 if let (Some(start), Some(end)) = (self.highlight_start, self.highlight_end) {
449 if self.column > 0 {
450 self.column -= 1;
451 }
452 let new_start = self.column.min(start);
454 let new_end = self.column.max(end);
455 self.highlight_start = Some(new_start);
456 self.highlight_end = Some(new_end);
457 if self.column >= start {
459 self.highlight_end = Some(self.column);
460 } else {
461 self.highlight_start = Some(self.column);
462 }
463 }
464 }
465
466 pub fn highlight_right(&mut self) {
468 if let (Some(start), Some(end)) = (self.highlight_start, self.highlight_end) {
469 if self.column + 1 < self.column_count {
470 self.column += 1;
471 }
472 let new_start = self.column.min(start);
473 let new_end = self.column.max(end);
474 self.highlight_start = Some(new_start);
475 self.highlight_end = Some(new_end);
476 if self.column <= end {
477 self.highlight_start = Some(self.column);
478 } else {
479 self.highlight_end = Some(self.column);
480 }
481 }
482 }
483
484 pub fn clear_highlight(&mut self) {
486 self.highlight_start = None;
487 self.highlight_end = None;
488 }
489
490 pub fn start_row_highlight(&mut self) {
494 if let (Some(lo), Some(hi)) = (self.row_highlight_low, self.row_highlight_high) {
495 if lo == hi && lo == self.cursor_note {
496 self.clear_row_highlight();
497 return;
498 }
499 }
500 if self.row_highlight_low.is_none() {
501 self.row_highlight_low = Some(self.cursor_note);
502 self.row_highlight_high = Some(self.cursor_note);
503 }
504 }
505
506 pub fn highlight_down(&mut self) {
508 self.start_row_highlight();
509 if self.cursor_note > 0 {
510 self.cursor_note -= 1;
511 if self.cursor_note < self.view_bottom_note {
512 self.view_bottom_note = self.cursor_note;
513 }
514 }
515 if let Some(lo) = self.row_highlight_low {
516 self.row_highlight_low = Some(self.cursor_note.min(lo));
517 }
518 if let Some(hi) = self.row_highlight_high {
519 self.row_highlight_high = Some(self.cursor_note.max(hi));
520 }
521 }
522
523 pub fn highlight_up(&mut self) {
525 self.start_row_highlight();
526 if self.cursor_note < 127 {
527 self.cursor_note += 1;
528 let top = self.view_bottom_note.saturating_add(self.view_height);
529 if self.cursor_note >= top {
530 self.view_bottom_note = self.cursor_note - self.view_height + 1;
531 }
532 }
533 if let Some(lo) = self.row_highlight_low {
534 self.row_highlight_low = Some(self.cursor_note.min(lo));
535 }
536 if let Some(hi) = self.row_highlight_high {
537 self.row_highlight_high = Some(self.cursor_note.max(hi));
538 }
539 }
540
541 pub fn clear_row_highlight(&mut self) {
542 self.row_highlight_low = None;
543 self.row_highlight_high = None;
544 }
545
546 pub fn is_row_highlighted(&self, note: u8) -> bool {
548 if let (Some(lo), Some(hi)) = (self.row_highlight_low, self.row_highlight_high) {
549 note >= lo && note <= hi
550 } else {
551 false
552 }
553 }
554
555 pub fn row_highlight_range(&self) -> Option<(u8, u8)> {
557 match (self.row_highlight_low, self.row_highlight_high) {
558 (Some(lo), Some(hi)) => Some((lo, hi)),
559 _ => None,
560 }
561 }
562
563 pub fn clear_all_highlights(&mut self) {
565 self.clear_highlight();
566 self.clear_row_highlight();
567 self.highlight_locked = false;
568 }
569
570 pub fn is_highlighted(&self, col: usize) -> bool {
572 if let (Some(start), Some(end)) = (self.highlight_start, self.highlight_end) {
573 col >= start && col <= end
574 } else {
575 false
576 }
577 }
578
579 pub fn highlight_range(&self) -> Option<(usize, usize)> {
581 match (self.highlight_start, self.highlight_end) {
582 (Some(s), Some(e)) => Some((s.min(e), s.max(e))),
583 _ => None,
584 }
585 }
586
587 pub fn set_view_height(&mut self, h: u8) {
588 self.view_height = h.max(1);
589 }
590
591 pub fn set_column_count(&mut self, count: usize) {
592 self.column_count = count.max(1);
593 if self.column >= self.column_count {
594 self.column = self.column_count - 1;
595 }
596 }
597
598 pub fn update_column_count(&mut self) {
600 let cols = (self.total_beats as f64 * self.grid.subdivisions_per_beat()).round() as usize;
601 self.column_count = cols.max(1);
602 if self.column >= self.column_count {
603 self.column = self.column_count.saturating_sub(1);
604 }
605 }
606
607 pub fn has_highlights(&self) -> bool {
609 self.highlight_start.is_some() || self.row_highlight_low.is_some()
610 }
611
612 pub fn column_display(&self) -> usize {
614 self.column + 1
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621
622 #[test]
623 fn focus_hierarchy() {
624 let mut pr = PianoRollState::new();
625 assert_eq!(pr.focus, PianoRollFocus::Navigation);
626
627 pr.enter(vec![]);
628 assert_eq!(pr.focus, PianoRollFocus::Selected);
629
630 pr.enter(vec![]);
632 assert_eq!(pr.focus, PianoRollFocus::Selected);
633
634 pr.enter_row();
636 assert_eq!(pr.focus, PianoRollFocus::Row);
637
638 pr.escape();
639 assert_eq!(pr.focus, PianoRollFocus::Selected);
640
641 pr.escape();
642 assert_eq!(pr.focus, PianoRollFocus::Navigation);
643 }
644
645 #[test]
646 fn column_navigation() {
647 let mut pr = PianoRollState::new();
648 pr.column_count = 16;
649 pr.column = 0;
650
651 pr.move_column_right();
652 assert_eq!(pr.column, 1);
653
654 pr.move_column_left();
655 assert_eq!(pr.column, 0);
656
657 pr.move_column_left();
658 assert_eq!(pr.column, 0); pr.column = 15;
661 pr.move_column_right();
662 assert_eq!(pr.column, 15); }
664
665 #[test]
666 fn digit_jump() {
667 let mut pr = PianoRollState::new();
668 pr.column_count = 16;
669
670 assert!(pr.type_digit('5'));
673 assert_eq!(pr.column, 4); assert!(!pr.type_digit('1'));
677 assert!(pr.type_digit('2'));
679 assert_eq!(pr.column, 11); assert!(pr.type_digit('9'));
683 assert_eq!(pr.column, 8);
684
685 pr.type_digit('1');
687 assert!(pr.commit_digits());
688 assert_eq!(pr.column, 0);
689 }
690
691 #[test]
692 fn can_escape() {
693 let mut pr = PianoRollState::new();
694 assert!(!pr.can_escape()); pr.enter(vec![]);
697 assert!(pr.can_escape()); pr.enter(vec![]);
700 assert!(pr.can_escape()); }
702
703 #[test]
704 fn note_scroll() {
705 let mut pr = PianoRollState::new();
706 pr.view_height = 10;
707 pr.view_bottom_note = 50;
708 pr.cursor_note = 55;
709
710 for _ in 0..10 {
712 pr.move_up();
713 }
714 assert!(pr.cursor_note >= pr.view_bottom_note);
716 assert!(pr.cursor_note < pr.view_bottom_note + pr.view_height);
717 }
718}