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 selected_note_indices: Vec<usize>,
224 column_digits: String,
226 pub highlight_start: Option<usize>,
229 pub highlight_end: Option<usize>,
230 pub visible_columns: usize,
232 pub yank_buffer: Vec<phosphor_core::clip::NoteSnapshot>,
235 pub yank_columns: usize,
237 pub row_highlight_low: Option<u8>,
239 pub row_highlight_high: Option<u8>,
240 pub highlight_locked: bool,
242 pub edit_mode: bool,
244 pub edit_cursor: usize,
246 pub edit_selected: Vec<usize>,
248 pub edit_sub: EditSubMode,
249 pub grid: GridResolution,
251 pub snap_enabled: bool,
252 pub default_velocity: u8,
253 pub settings_cursor: usize,
255}
256
257impl Default for PianoRollState {
258 fn default() -> Self { Self::new() }
259}
260
261impl PianoRollState {
262 pub fn new() -> Self {
263 Self {
264 cursor_note: 60,
265 scroll_x: 0,
266 view_bottom_note: 48,
267 view_height: 24,
268 focus: PianoRollFocus::Navigation,
269 column: 0,
270 column_count: 16,
271 selected_note_indices: Vec::new(),
272 column_digits: String::new(),
273 highlight_start: None,
274 highlight_end: None,
275 visible_columns: 16,
276 row_highlight_low: None,
277 row_highlight_high: None,
278 yank_buffer: Vec::new(),
279 yank_columns: 0,
280 highlight_locked: false,
281 edit_mode: false,
282 edit_cursor: 0,
283 edit_selected: Vec::new(),
284 edit_sub: EditSubMode::Navigate,
285 grid: GridResolution::Eighth,
286 snap_enabled: true,
287 default_velocity: 100,
288 settings_cursor: 0,
289 }
290 }
291
292 pub fn enter(&mut self, note_indices: Vec<usize>) {
297 match self.focus {
298 PianoRollFocus::Navigation => {
299 self.focus = PianoRollFocus::Selected;
300 self.selected_note_indices = note_indices;
301 }
302 PianoRollFocus::Selected | PianoRollFocus::Row => {}
303 }
304 }
305
306 pub fn enter_row(&mut self) {
308 self.focus = PianoRollFocus::Row;
309 }
310
311 pub fn escape(&mut self) {
312 match self.focus {
313 PianoRollFocus::Row => {
314 self.focus = PianoRollFocus::Selected;
315 }
316 PianoRollFocus::Selected => {
317 self.focus = PianoRollFocus::Navigation;
318 self.column_digits.clear();
319 }
320 PianoRollFocus::Navigation => {
321 }
323 }
324 }
325
326 pub fn can_escape(&self) -> bool {
328 self.focus != PianoRollFocus::Navigation
329 }
330
331 pub fn move_up(&mut self) {
334 if self.cursor_note < 127 {
335 self.cursor_note += 1;
336 let top = self.view_bottom_note.saturating_add(self.view_height);
337 if self.cursor_note >= top {
338 self.view_bottom_note = self.cursor_note - self.view_height + 1;
339 }
340 }
341 }
342
343 pub fn move_down(&mut self) {
344 if self.cursor_note > 0 {
345 self.cursor_note -= 1;
346 if self.cursor_note < self.view_bottom_note {
347 self.view_bottom_note = self.cursor_note;
348 }
349 }
350 }
351
352 pub fn move_column_left(&mut self) {
355 if self.column > 0 {
356 self.column -= 1;
357 if self.column < self.scroll_x {
359 self.scroll_x = self.column;
360 }
361 }
362 }
363
364 pub fn move_column_right(&mut self) {
365 if self.column + 1 < self.column_count {
366 self.column += 1;
367 if self.column >= self.scroll_x + self.visible_columns && self.visible_columns > 0 {
369 self.scroll_x = self.column + 1 - self.visible_columns;
370 }
371 }
372 }
373
374 pub fn type_digit(&mut self, ch: char) -> bool {
376 self.column_digits.push(ch);
377 if let Ok(num) = self.column_digits.parse::<usize>() {
378 if num >= 1 && num <= self.column_count {
379 let could_grow = num * 10 <= self.column_count;
381 if !could_grow || self.column_digits.len() >= 2 {
382 self.column = num - 1;
383 self.column_digits.clear();
384 self.ensure_column_visible();
386 return true;
387 }
388 return false;
390 }
391 }
392 self.column_digits.clear();
394 false
395 }
396
397 pub fn commit_digits(&mut self) -> bool {
399 if let Ok(num) = self.column_digits.parse::<usize>() {
400 if num >= 1 && num <= self.column_count {
401 self.column = num - 1;
402 self.column_digits.clear();
403 self.ensure_column_visible();
404 return true;
405 }
406 }
407 self.column_digits.clear();
408 false
409 }
410
411 pub fn ensure_column_visible(&mut self) {
413 if self.visible_columns == 0 { return; }
414 if self.column < self.scroll_x {
415 self.scroll_x = self.column;
416 } else if self.column >= self.scroll_x + self.visible_columns {
417 self.scroll_x = self.column + 1 - self.visible_columns;
418 }
419 }
420
421 pub fn column_digits_display(&self) -> &str {
422 &self.column_digits
423 }
424
425 pub fn start_highlight(&mut self) {
430 if let (Some(s), Some(e)) = (self.highlight_start, self.highlight_end) {
431 if s == e && s == self.column {
432 self.clear_highlight();
434 return;
435 }
436 }
437 if self.highlight_start.is_none() {
438 self.highlight_start = Some(self.column);
439 self.highlight_end = Some(self.column);
440 }
441 }
442
443 pub fn highlight_left(&mut self) {
445 if let (Some(start), Some(end)) = (self.highlight_start, self.highlight_end) {
446 if self.column > 0 {
447 self.column -= 1;
448 }
449 let new_start = self.column.min(start);
451 let new_end = self.column.max(end);
452 self.highlight_start = Some(new_start);
453 self.highlight_end = Some(new_end);
454 if self.column >= start {
456 self.highlight_end = Some(self.column);
457 } else {
458 self.highlight_start = Some(self.column);
459 }
460 }
461 }
462
463 pub fn highlight_right(&mut self) {
465 if let (Some(start), Some(end)) = (self.highlight_start, self.highlight_end) {
466 if self.column + 1 < self.column_count {
467 self.column += 1;
468 }
469 let new_start = self.column.min(start);
470 let new_end = self.column.max(end);
471 self.highlight_start = Some(new_start);
472 self.highlight_end = Some(new_end);
473 if self.column <= end {
474 self.highlight_start = Some(self.column);
475 } else {
476 self.highlight_end = Some(self.column);
477 }
478 }
479 }
480
481 pub fn clear_highlight(&mut self) {
483 self.highlight_start = None;
484 self.highlight_end = None;
485 }
486
487 pub fn start_row_highlight(&mut self) {
491 if let (Some(lo), Some(hi)) = (self.row_highlight_low, self.row_highlight_high) {
492 if lo == hi && lo == self.cursor_note {
493 self.clear_row_highlight();
494 return;
495 }
496 }
497 if self.row_highlight_low.is_none() {
498 self.row_highlight_low = Some(self.cursor_note);
499 self.row_highlight_high = Some(self.cursor_note);
500 }
501 }
502
503 pub fn highlight_down(&mut self) {
505 self.start_row_highlight();
506 if self.cursor_note > 0 {
507 self.cursor_note -= 1;
508 if self.cursor_note < self.view_bottom_note {
509 self.view_bottom_note = self.cursor_note;
510 }
511 }
512 if let Some(lo) = self.row_highlight_low {
513 self.row_highlight_low = Some(self.cursor_note.min(lo));
514 }
515 if let Some(hi) = self.row_highlight_high {
516 self.row_highlight_high = Some(self.cursor_note.max(hi));
517 }
518 }
519
520 pub fn highlight_up(&mut self) {
522 self.start_row_highlight();
523 if self.cursor_note < 127 {
524 self.cursor_note += 1;
525 let top = self.view_bottom_note.saturating_add(self.view_height);
526 if self.cursor_note >= top {
527 self.view_bottom_note = self.cursor_note - self.view_height + 1;
528 }
529 }
530 if let Some(lo) = self.row_highlight_low {
531 self.row_highlight_low = Some(self.cursor_note.min(lo));
532 }
533 if let Some(hi) = self.row_highlight_high {
534 self.row_highlight_high = Some(self.cursor_note.max(hi));
535 }
536 }
537
538 pub fn clear_row_highlight(&mut self) {
539 self.row_highlight_low = None;
540 self.row_highlight_high = None;
541 }
542
543 pub fn is_row_highlighted(&self, note: u8) -> bool {
545 if let (Some(lo), Some(hi)) = (self.row_highlight_low, self.row_highlight_high) {
546 note >= lo && note <= hi
547 } else {
548 false
549 }
550 }
551
552 pub fn row_highlight_range(&self) -> Option<(u8, u8)> {
554 match (self.row_highlight_low, self.row_highlight_high) {
555 (Some(lo), Some(hi)) => Some((lo, hi)),
556 _ => None,
557 }
558 }
559
560 pub fn clear_all_highlights(&mut self) {
562 self.clear_highlight();
563 self.clear_row_highlight();
564 self.highlight_locked = false;
565 }
566
567 pub fn is_highlighted(&self, col: usize) -> bool {
569 if let (Some(start), Some(end)) = (self.highlight_start, self.highlight_end) {
570 col >= start && col <= end
571 } else {
572 false
573 }
574 }
575
576 pub fn highlight_range(&self) -> Option<(usize, usize)> {
578 match (self.highlight_start, self.highlight_end) {
579 (Some(s), Some(e)) => Some((s.min(e), s.max(e))),
580 _ => None,
581 }
582 }
583
584 pub fn set_view_height(&mut self, h: u8) {
585 self.view_height = h.max(1);
586 }
587
588 pub fn set_column_count(&mut self, count: usize) {
589 self.column_count = count.max(1);
590 if self.column >= self.column_count {
591 self.column = self.column_count - 1;
592 }
593 }
594
595 pub fn has_highlights(&self) -> bool {
597 self.highlight_start.is_some() || self.row_highlight_low.is_some()
598 }
599
600 pub fn column_display(&self) -> usize {
602 self.column + 1
603 }
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609
610 #[test]
611 fn focus_hierarchy() {
612 let mut pr = PianoRollState::new();
613 assert_eq!(pr.focus, PianoRollFocus::Navigation);
614
615 pr.enter(vec![]);
616 assert_eq!(pr.focus, PianoRollFocus::Selected);
617
618 pr.enter(vec![]);
620 assert_eq!(pr.focus, PianoRollFocus::Selected);
621
622 pr.enter_row();
624 assert_eq!(pr.focus, PianoRollFocus::Row);
625
626 pr.escape();
627 assert_eq!(pr.focus, PianoRollFocus::Selected);
628
629 pr.escape();
630 assert_eq!(pr.focus, PianoRollFocus::Navigation);
631 }
632
633 #[test]
634 fn column_navigation() {
635 let mut pr = PianoRollState::new();
636 pr.column_count = 16;
637 pr.column = 0;
638
639 pr.move_column_right();
640 assert_eq!(pr.column, 1);
641
642 pr.move_column_left();
643 assert_eq!(pr.column, 0);
644
645 pr.move_column_left();
646 assert_eq!(pr.column, 0); pr.column = 15;
649 pr.move_column_right();
650 assert_eq!(pr.column, 15); }
652
653 #[test]
654 fn digit_jump() {
655 let mut pr = PianoRollState::new();
656 pr.column_count = 16;
657
658 assert!(pr.type_digit('5'));
661 assert_eq!(pr.column, 4); assert!(!pr.type_digit('1'));
665 assert!(pr.type_digit('2'));
667 assert_eq!(pr.column, 11); assert!(pr.type_digit('9'));
671 assert_eq!(pr.column, 8);
672
673 pr.type_digit('1');
675 assert!(pr.commit_digits());
676 assert_eq!(pr.column, 0);
677 }
678
679 #[test]
680 fn can_escape() {
681 let mut pr = PianoRollState::new();
682 assert!(!pr.can_escape()); pr.enter(vec![]);
685 assert!(pr.can_escape()); pr.enter(vec![]);
688 assert!(pr.can_escape()); }
690
691 #[test]
692 fn note_scroll() {
693 let mut pr = PianoRollState::new();
694 pr.view_height = 10;
695 pr.view_bottom_note = 50;
696 pr.cursor_note = 55;
697
698 for _ in 0..10 {
700 pr.move_up();
701 }
702 assert!(pr.cursor_note >= pr.view_bottom_note);
704 assert!(pr.cursor_note < pr.view_bottom_note + pr.view_height);
705 }
706}