1use crate::input::{self, Action, BottomPane, Editor, InputContext, Mode};
6use crate::project::ProjectData;
7use crate::view::graph::GraphViewWidget;
8use crate::view::help::HelpOverlay;
9use crate::view::info::InfoView;
10use crate::view::pattern::PatternView;
11use crate::view::perf::HostStatsSnapshot;
12use crate::view::scope::ScopeView;
13use crate::view::spectrum::{SpectrumAnalyzerState, SpectrumView};
14use crate::view::transport::TransportView;
15
16use trem::event::NoteEvent;
17use trem::graph::{Edge, GraphSnapshot, ParamDescriptor};
18use trem::math::Rational;
19use trem::pitch::Pitch;
20use trem_cpal::{Bridge, Command, Notification, ScopeFocus};
21
22use crossterm::event::{self, Event, KeyEventKind};
23use ratatui::layout::{Constraint, Direction, Layout};
24use std::collections::{HashMap, HashSet};
25use std::time::{Duration, Instant};
26
27use sysinfo::{Pid, ProcessesToUpdate, System};
28
29const GATE_PRESETS: [(i64, u64); 4] = [(1, 4), (1, 2), (3, 4), (7, 8)];
30
31const MAIN_EDITOR_MIN_WIDTH: u16 = 14;
33
34fn info_sidebar_width(middle_width: u16) -> u16 {
36 const MIN_SIDEBAR: u16 = 18;
37 let w = middle_width.max(MAIN_EDITOR_MIN_WIDTH + MIN_SIDEBAR);
38 let target = (u32::from(w) * 36 / 100).clamp(u32::from(MIN_SIDEBAR), 30) as u16;
39 target.min(w.saturating_sub(MAIN_EDITOR_MIN_WIDTH))
40}
41
42fn cycle_gate(current: Rational) -> Rational {
43 for (i, &(n, d)) in GATE_PRESETS.iter().enumerate() {
44 if current == Rational::new(n, d) {
45 let next = GATE_PRESETS[(i + 1) % GATE_PRESETS.len()];
46 return Rational::new(next.0, next.1);
47 }
48 }
49 Rational::new(1, 4)
50}
51
52pub struct App {
54 pub grid: trem::grid::Grid,
55 pub cursor_row: u32,
56 pub cursor_col: u32,
57 pub mode: Mode,
58 pub editor: Editor,
59 pub help_open: bool,
61 pub bpm: f64,
62 pub playing: bool,
63 engine_pattern_active: bool,
66 pub beat_position: f64,
67 pub current_play_row: Option<u32>,
68 pub scale: trem::pitch::Scale,
69 pub scale_name: String,
70 pub octave: i32,
71 pub bridge: Bridge,
72 pub scope_master: Vec<f32>,
74 pub scope_graph_in: Vec<f32>,
76 pub host_stats: HostStatsSnapshot,
78 sys: System,
79 host_stats_last_refresh: Instant,
80 pub spectrum_fall_ms: f64,
82 spectrum_analyzer_in: SpectrumAnalyzerState,
83 spectrum_analyzer_out: SpectrumAnalyzerState,
84 pub peak_l: f32,
85 pub peak_r: f32,
86 pub should_quit: bool,
87 pub instrument_names: Vec<String>,
88 pub voice_ids: Vec<u32>,
89 pub graph_nodes: Vec<(u32, String)>,
90 pub graph_node_descriptions: Vec<String>,
91 pub graph_edges: Vec<Edge>,
92 pub graph_cursor: usize,
93 pub graph_depths: Vec<usize>,
94 pub graph_layers: Vec<Vec<usize>>,
95 pub graph_params: Vec<Vec<ParamDescriptor>>,
96 pub graph_param_values: Vec<Vec<f64>>,
97 pub graph_param_groups: Vec<Vec<trem::graph::ParamGroup>>,
98 pub param_cursor: usize,
99 pub swing: f64,
100 pub euclidean_k: u32,
101 pub undo_stack: Vec<Vec<Option<NoteEvent>>>,
102 pub redo_stack: Vec<Vec<Option<NoteEvent>>>,
103 rng_state: u64,
104 preview_note_off: Option<(u32, Instant)>,
105 pub bottom_pane: BottomPane,
106 pub graph_path: Vec<u32>,
108 pub graph_stack: Vec<GraphFrame>,
110 pub graph_has_children: Vec<bool>,
112 pub graph_breadcrumb: Vec<String>,
114 nested_graph_snapshots: HashMap<Vec<u32>, GraphSnapshot>,
117}
118
119#[derive(Clone, Debug)]
121pub struct GraphFrame {
122 pub nodes: Vec<(u32, String)>,
123 pub edges: Vec<Edge>,
124 pub cursor: usize,
125 pub params: Vec<Vec<ParamDescriptor>>,
126 pub param_values: Vec<Vec<f64>>,
127 pub param_groups: Vec<Vec<trem::graph::ParamGroup>>,
128 pub depths: Vec<usize>,
129 pub layers: Vec<Vec<usize>>,
130 pub has_children: Vec<bool>,
131 pub node_descriptions: Vec<String>,
132}
133
134impl App {
135 pub fn new(
137 grid: trem::grid::Grid,
138 scale: trem::pitch::Scale,
139 scale_name: String,
140 bridge: Bridge,
141 instrument_names: Vec<String>,
142 voice_ids: Vec<u32>,
143 ) -> Self {
144 Self {
145 grid,
146 cursor_row: 0,
147 cursor_col: 0,
148 mode: Mode::Normal,
149 editor: Editor::Pattern,
150 help_open: false,
151 bpm: 120.0,
152 playing: false,
153 engine_pattern_active: false,
154 beat_position: 0.0,
155 current_play_row: None,
156 scale,
157 scale_name,
158 octave: 0,
159 bridge,
160 scope_master: Vec::new(),
161 scope_graph_in: Vec::new(),
162 host_stats: HostStatsSnapshot::default(),
163 sys: System::new(),
164 host_stats_last_refresh: Instant::now()
165 .checked_sub(Duration::from_secs(1))
166 .unwrap_or_else(Instant::now),
167 spectrum_fall_ms: 18.0,
168 spectrum_analyzer_in: SpectrumAnalyzerState::new(18.0),
169 spectrum_analyzer_out: SpectrumAnalyzerState::new(18.0),
170 peak_l: 0.0,
171 peak_r: 0.0,
172 should_quit: false,
173 instrument_names,
174 voice_ids,
175 graph_nodes: Vec::new(),
176 graph_node_descriptions: Vec::new(),
177 graph_edges: Vec::new(),
178 graph_cursor: 0,
179 graph_depths: Vec::new(),
180 graph_layers: Vec::new(),
181 graph_params: Vec::new(),
182 graph_param_values: Vec::new(),
183 graph_param_groups: Vec::new(),
184 param_cursor: 0,
185 swing: 0.0,
186 euclidean_k: 0,
187 undo_stack: Vec::new(),
188 redo_stack: Vec::new(),
189 rng_state: std::time::SystemTime::now()
190 .duration_since(std::time::UNIX_EPOCH)
191 .unwrap_or_default()
192 .as_nanos() as u64,
193 preview_note_off: None,
194 bottom_pane: BottomPane::Spectrum,
195 graph_path: Vec::new(),
196 graph_stack: Vec::new(),
197 graph_has_children: Vec::new(),
198 graph_breadcrumb: Vec::new(),
199 nested_graph_snapshots: HashMap::new(),
200 }
201 }
202
203 pub fn with_graph_info(
205 mut self,
206 nodes: Vec<(u32, String)>,
207 edges: Vec<Edge>,
208 params: Vec<(Vec<ParamDescriptor>, Vec<f64>, Vec<trem::graph::ParamGroup>)>,
209 ) -> Self {
210 let (depths, layers) = crate::view::graph::compute_graph_nav(&nodes, &edges);
211 self.graph_nodes = nodes;
212 self.graph_edges = edges;
213 self.graph_depths = depths;
214 self.graph_layers = layers;
215 self.graph_params = params.iter().map(|(d, _, _)| d.clone()).collect();
216 self.graph_param_values = params.iter().map(|(_, v, _)| v.clone()).collect();
217 self.graph_param_groups = params.into_iter().map(|(_, _, g)| g).collect();
218 self.graph_has_children = vec![false; self.graph_nodes.len()];
219 self
220 }
221
222 pub fn set_node_descriptions(&mut self, descriptions: Vec<String>) {
224 self.graph_node_descriptions = descriptions;
225 }
226
227 pub fn set_node_children(&mut self, has_children: Vec<bool>) {
229 self.graph_has_children = has_children;
230 }
231
232 pub fn with_nested_graph_snapshots(
234 mut self,
235 snapshots: HashMap<Vec<u32>, GraphSnapshot>,
236 ) -> Self {
237 self.nested_graph_snapshots = snapshots;
238 self
239 }
240
241 fn refresh_host_stats(&mut self) {
242 if self.host_stats_last_refresh.elapsed() < Duration::from_millis(520) {
243 return;
244 }
245 self.host_stats_last_refresh = Instant::now();
246 self.sys.refresh_cpu_usage();
247 let pid = Pid::from_u32(std::process::id());
248 self.sys
249 .refresh_processes(ProcessesToUpdate::Some(&[pid]), false);
250 if let Some(p) = self.sys.process(pid) {
251 let raw = p.cpu_usage();
253 let prev = self.host_stats.process_cpu_pct;
254 const SMOOTH: f32 = 0.22;
255 self.host_stats.process_cpu_pct = if prev <= f32::EPSILON {
256 raw
257 } else {
258 prev * (1.0 - SMOOTH) + raw * SMOOTH
259 };
260 self.host_stats.process_rss_mb = p.memory() / 1024 / 1024;
261 } else {
262 self.host_stats.process_cpu_pct = 0.0;
263 self.host_stats.process_rss_mb = 0;
264 }
265 }
266
267 fn load_graph_from_snapshot(&mut self, snap: &GraphSnapshot) {
269 let nodes: Vec<(u32, String)> = snap.nodes.iter().map(|n| (n.id, n.name.clone())).collect();
270 let edges = snap.edges.clone();
271 let (depths, layers) = crate::view::graph::compute_graph_nav(&nodes, &edges);
272 self.graph_nodes = nodes;
273 self.graph_edges = edges;
274 self.graph_depths = depths;
275 self.graph_layers = layers;
276 self.graph_params = snap.nodes.iter().map(|n| n.params.clone()).collect();
277 self.graph_param_values = snap.nodes.iter().map(|n| n.param_values.clone()).collect();
278 self.graph_param_groups = snap.nodes.iter().map(|n| n.param_groups.clone()).collect();
279 self.graph_has_children = snap.nodes.iter().map(|n| n.has_children).collect();
280 self.graph_node_descriptions = vec![String::new(); self.graph_nodes.len()];
281 self.graph_cursor = 0;
282 self.param_cursor = 0;
283 }
284
285 pub fn sync_scope_focus(&mut self) {
287 match self.editor {
288 Editor::Pattern => {
289 self.bridge
290 .send(Command::SetScopeFocus(ScopeFocus::PatchBuses));
291 }
292 Editor::Graph => {
293 if let Some(&(nid, _)) = self.graph_nodes.get(self.graph_cursor) {
294 self.bridge
295 .send(Command::SetScopeFocus(ScopeFocus::GraphNode {
296 graph_path: self.graph_path.clone(),
297 node: nid,
298 }));
299 } else {
300 self.bridge
301 .send(Command::SetScopeFocus(ScopeFocus::PatchBuses));
302 }
303 }
304 }
305 }
306
307 pub fn handle_action(&mut self, action: Action) {
309 let sync_scope = matches!(
310 &action,
311 Action::CycleEditor
312 | Action::EnterGraph
313 | Action::ExitGraph
314 | Action::MoveUp
315 | Action::MoveDown
316 | Action::MoveLeft
317 | Action::MoveRight
318 | Action::LoadProject
319 );
320 match action {
321 Action::Quit => self.should_quit = true,
322 Action::ToggleHelp => {
323 self.help_open = !self.help_open;
324 if self.help_open {
325 self.mode = Mode::Normal;
326 }
327 }
328 Action::CycleEditor => {
329 self.editor = self.editor.next();
330 self.mode = Mode::Normal;
331 }
332 Action::ToggleEdit => {
333 self.mode = match self.mode {
334 Mode::Normal => {
335 self.param_cursor = 0;
336 Mode::Edit
337 }
338 Mode::Edit => Mode::Normal,
339 };
340 }
341 Action::TogglePlay => {
342 self.playing = !self.playing;
343 if self.playing {
344 self.engine_pattern_active = true;
345 self.send_pattern();
346 self.bridge.send(Command::Play);
347 } else {
348 self.bridge.send(Command::Pause);
349 }
350 }
351 Action::MoveUp => match (&self.editor, &self.mode) {
352 (Editor::Pattern, _) => {
353 self.cursor_col = self.cursor_col.saturating_sub(1);
354 }
355 (Editor::Graph, Mode::Normal) => self.graph_move_up(),
356 (Editor::Graph, Mode::Edit) => {
357 self.param_cursor = self.param_cursor.saturating_sub(1);
358 }
359 },
360 Action::MoveDown => match (&self.editor, &self.mode) {
361 (Editor::Pattern, _) => {
362 if self.cursor_col < self.grid.columns.saturating_sub(1) {
363 self.cursor_col += 1;
364 }
365 }
366 (Editor::Graph, Mode::Normal) => self.graph_move_down(),
367 (Editor::Graph, Mode::Edit) => {
368 let max = self.current_node_param_count().saturating_sub(1);
369 if self.param_cursor < max {
370 self.param_cursor += 1;
371 }
372 }
373 },
374 Action::MoveLeft => match (&self.editor, &self.mode) {
375 (Editor::Pattern, _) => {
376 self.cursor_row = self.cursor_row.saturating_sub(1);
377 }
378 (Editor::Graph, Mode::Normal) => self.graph_move_left(),
379 (Editor::Graph, Mode::Edit) => self.adjust_param_coarse(-1),
380 },
381 Action::MoveRight => match (&self.editor, &self.mode) {
382 (Editor::Pattern, _) => {
383 if self.cursor_row < self.grid.rows.saturating_sub(1) {
384 self.cursor_row += 1;
385 }
386 }
387 (Editor::Graph, Mode::Normal) => self.graph_move_right(),
388 (Editor::Graph, Mode::Edit) => self.adjust_param_coarse(1),
389 },
390 Action::Undo => self.undo(),
391 Action::Redo => self.redo(),
392 Action::SaveProject => {
393 let path = crate::project::default_project_path();
394 let data = ProjectData::from_app(self);
395 if let Err(e) = crate::project::save(&path, &data) {
396 eprintln!("save error: {e}");
397 }
398 }
399 Action::LoadProject => {
400 let path = crate::project::default_project_path();
401 match crate::project::load(&path) {
402 Ok(data) => {
403 self.push_undo();
404 data.apply_to_app(self);
405 if self.should_sync_pattern() {
406 self.send_pattern();
407 }
408 }
409 Err(e) => eprintln!("load error: {e}"),
410 }
411 }
412 Action::SwingUp => {
413 self.swing = (self.swing + 0.05).min(0.9);
414 if self.should_sync_pattern() {
415 self.send_pattern();
416 }
417 }
418 Action::SwingDown => {
419 self.swing = (self.swing - 0.05).max(0.0);
420 if self.should_sync_pattern() {
421 self.send_pattern();
422 }
423 }
424 Action::GateCycle => {
425 if self.editor == Editor::Pattern {
426 if let Some(note) = self.grid.get(self.cursor_row, self.cursor_col).cloned() {
427 self.push_undo();
428 let new_gate = cycle_gate(note.gate);
429 let mut updated = note;
430 updated.gate = new_gate;
431 self.grid
432 .set(self.cursor_row, self.cursor_col, Some(updated));
433 if self.should_sync_pattern() {
434 self.send_pattern();
435 }
436 }
437 }
438 }
439 Action::NoteInput(degree) => {
440 if self.editor != Editor::Pattern {
441 return;
442 }
443 self.push_undo();
444 let event = NoteEvent::new(degree, self.octave, Rational::new(3, 4));
445 let voice_id = self
446 .voice_ids
447 .get(self.cursor_col as usize)
448 .copied()
449 .unwrap_or(0);
450
451 if let Some((old_voice, _)) = self.preview_note_off.take() {
452 self.bridge.send(Command::NoteOff { voice: old_voice });
453 }
454
455 let pitch = self.scale.resolve(degree);
456 let freq = Pitch(pitch.0 + self.octave as f64).to_hz(440.0);
457 let vel = event.velocity.to_f64();
458 self.bridge.send(Command::NoteOn {
459 frequency: freq,
460 velocity: vel,
461 voice: voice_id,
462 });
463 self.preview_note_off = Some((voice_id, Instant::now()));
464
465 self.grid.set(self.cursor_row, self.cursor_col, Some(event));
466
467 if self.should_sync_pattern() {
468 self.send_pattern();
469 }
470
471 if self.cursor_row < self.grid.rows.saturating_sub(1) {
472 self.cursor_row += 1;
473 } else {
474 self.cursor_row = 0;
475 if self.cursor_col < self.grid.columns.saturating_sub(1) {
476 self.cursor_col += 1;
477 }
478 }
479 }
480 Action::DeleteNote => {
481 if self.editor != Editor::Pattern {
482 return;
483 }
484 self.push_undo();
485 self.grid.set(self.cursor_row, self.cursor_col, None);
486 if self.should_sync_pattern() {
487 self.send_pattern();
488 }
489 }
490 Action::OctaveUp => {
491 self.octave = (self.octave + 1).min(9);
492 if self.should_sync_pattern() {
493 self.send_pattern();
494 }
495 }
496 Action::OctaveDown => {
497 self.octave = (self.octave - 1).max(-4);
498 if self.should_sync_pattern() {
499 self.send_pattern();
500 }
501 }
502 Action::BpmUp => {
503 if self.editor == Editor::Graph && self.mode == Mode::Edit {
504 self.adjust_param_fine(1);
505 } else {
506 self.bpm = (self.bpm + 1.0).min(300.0);
507 self.bridge.send(Command::SetBpm(self.bpm));
508 }
509 }
510 Action::BpmDown => {
511 if self.editor == Editor::Graph && self.mode == Mode::Edit {
512 self.adjust_param_fine(-1);
513 } else {
514 self.bpm = (self.bpm - 1.0).max(20.0);
515 self.bridge.send(Command::SetBpm(self.bpm));
516 }
517 }
518 Action::ParamFineUp => {
519 if self.editor == Editor::Graph && self.mode == Mode::Edit {
520 self.adjust_param_fine(1);
521 }
522 }
523 Action::ParamFineDown => {
524 if self.editor == Editor::Graph && self.mode == Mode::Edit {
525 self.adjust_param_fine(-1);
526 }
527 }
528 Action::EuclideanFill => {
529 if self.editor == Editor::Pattern {
530 self.push_undo();
531 self.euclidean_k = (self.euclidean_k + 1) % (self.grid.rows + 1);
532 let pattern = trem::euclidean::euclidean(self.euclidean_k, self.grid.rows);
533 let template = NoteEvent::new(0, self.octave, Rational::new(3, 4));
534 self.grid
535 .fill_euclidean(self.cursor_col, &pattern, template);
536 if self.should_sync_pattern() {
537 self.send_pattern();
538 }
539 }
540 }
541 Action::RandomizeVoice => {
542 if self.editor == Editor::Pattern {
543 self.push_undo();
544 self.randomize_current_voice();
545 if self.should_sync_pattern() {
546 self.send_pattern();
547 }
548 }
549 }
550 Action::ReverseVoice => {
551 if self.editor == Editor::Pattern {
552 self.push_undo();
553 self.grid.reverse_voice(self.cursor_col);
554 if self.should_sync_pattern() {
555 self.send_pattern();
556 }
557 }
558 }
559 Action::ShiftVoiceLeft => {
560 if self.editor == Editor::Pattern {
561 self.push_undo();
562 self.grid.shift_voice(self.cursor_col, -1);
563 if self.should_sync_pattern() {
564 self.send_pattern();
565 }
566 }
567 }
568 Action::ShiftVoiceRight => {
569 if self.editor == Editor::Pattern {
570 self.push_undo();
571 self.grid.shift_voice(self.cursor_col, 1);
572 if self.should_sync_pattern() {
573 self.send_pattern();
574 }
575 }
576 }
577 Action::VelocityUp => {
578 if self.editor == Editor::Pattern {
579 self.push_undo();
580 self.adjust_note_velocity(Rational::new(1, 8));
581 if self.should_sync_pattern() {
582 self.send_pattern();
583 }
584 }
585 }
586 Action::VelocityDown => {
587 if self.editor == Editor::Pattern {
588 self.push_undo();
589 self.adjust_note_velocity(Rational::new(-1, 8));
590 if self.should_sync_pattern() {
591 self.send_pattern();
592 }
593 }
594 }
595 Action::CycleBottomPane => {
596 self.bottom_pane = self.bottom_pane.next();
597 }
598 Action::EnterGraph => {
599 if self.editor != Editor::Graph || self.mode != Mode::Normal {
600 return;
601 }
602 if self.graph_cursor >= self.graph_has_children.len() {
603 return;
604 }
605 if !self.graph_has_children[self.graph_cursor] {
606 return;
607 }
608 self.enter_nested_graph();
609 }
610 Action::ExitGraph => {
611 if self.editor != Editor::Graph {
612 return;
613 }
614 self.exit_nested_graph();
615 }
616 }
617 if sync_scope {
618 self.sync_scope_focus();
619 }
620 }
621
622 fn current_node_param_count(&self) -> usize {
623 self.graph_params
624 .get(self.graph_cursor)
625 .map_or(0, |p| p.len())
626 }
627
628 fn current_node_description(&self) -> &str {
629 if self.editor != Editor::Graph {
630 return "";
631 }
632 self.graph_node_descriptions
633 .get(self.graph_cursor)
634 .map(|s| s.as_str())
635 .unwrap_or("")
636 }
637
638 fn current_param_help(&self) -> &str {
639 if self.editor != Editor::Graph || self.mode != crate::input::Mode::Edit {
640 return "";
641 }
642 self.graph_params
643 .get(self.graph_cursor)
644 .and_then(|params| params.get(self.param_cursor))
645 .map(|p| p.help)
646 .unwrap_or("")
647 }
648
649 fn adjust_param_coarse(&mut self, direction: i32) {
650 self.adjust_param_by(direction, false);
651 }
652
653 fn adjust_param_fine(&mut self, direction: i32) {
654 self.adjust_param_by(direction, true);
655 }
656
657 fn adjust_param_by(&mut self, direction: i32, fine: bool) {
658 let params = match self.graph_params.get(self.graph_cursor) {
659 Some(p) if !p.is_empty() => p,
660 _ => return,
661 };
662 let desc = match params.get(self.param_cursor) {
663 Some(d) => d,
664 None => return,
665 };
666 let values = match self.graph_param_values.get_mut(self.graph_cursor) {
667 Some(v) => v,
668 None => return,
669 };
670
671 let base_step = if desc.step > 0.0 {
672 desc.step
673 } else {
674 (desc.max - desc.min) * 0.01
675 };
676 let step = if fine { base_step * 0.1 } else { base_step };
677
678 let old = values[self.param_cursor];
679 let new_val = (old + step * direction as f64).clamp(desc.min, desc.max);
680 values[self.param_cursor] = new_val;
681
682 let node_id = self.graph_nodes[self.graph_cursor].0;
683 let mut path = self.graph_path.clone();
684 path.push(node_id);
685 self.bridge.send(Command::SetParam {
686 path,
687 param_id: desc.id,
688 value: new_val,
689 });
690
691 if !self.playing {
692 self.fire_param_preview();
693 }
694 }
695
696 fn fire_param_preview(&mut self) {
700 let voice = self.voice_ids.first().copied().unwrap_or(0);
701 if let Some((old, _)) = self.preview_note_off.take() {
702 self.bridge.send(Command::NoteOff { voice: old });
703 }
704 self.bridge.send(Command::NoteOn {
705 frequency: 440.0,
706 velocity: 0.6,
707 voice,
708 });
709 self.preview_note_off = Some((voice, Instant::now()));
710 }
711
712 fn next_rng(&mut self) -> u64 {
713 self.rng_state = self
714 .rng_state
715 .wrapping_mul(6364136223846793005)
716 .wrapping_add(1442695040888963407);
717 self.rng_state
718 }
719
720 fn randomize_current_voice(&mut self) {
721 let col = self.cursor_col;
722 let scale_len = self.scale.len() as i32;
723 for row in 0..self.grid.rows {
724 let r = self.next_rng();
725 if r % 100 < 40 {
726 let degree = (self.next_rng() % scale_len.max(1) as u64) as i32;
727 let vel_n = (self.next_rng() % 6 + 2) as i64; let event = NoteEvent::new(degree, self.octave, Rational::new(vel_n, 8));
729 self.grid.set(row, col, Some(event));
730 } else {
731 self.grid.set(row, col, None);
732 }
733 }
734 }
735
736 fn adjust_note_velocity(&mut self, delta: Rational) {
737 if let Some(note) = self.grid.get(self.cursor_row, self.cursor_col).cloned() {
738 let new_vel = note.velocity + delta;
739 let clamped = if new_vel.to_f64() < 0.0625 {
740 Rational::new(1, 16)
741 } else if new_vel.to_f64() > 1.0 {
742 Rational::new(1, 1)
743 } else {
744 new_vel
745 };
746 let mut updated = note;
747 updated.velocity = clamped;
748 self.grid
749 .set(self.cursor_row, self.cursor_col, Some(updated));
750 }
751 }
752
753 fn graph_move_up(&mut self) {
754 if self.graph_depths.is_empty() {
755 return;
756 }
757 let depth = self.graph_depths[self.graph_cursor];
758 let layer = &self.graph_layers[depth];
759 if let Some(pos) = layer.iter().position(|&i| i == self.graph_cursor) {
760 if pos > 0 {
761 self.graph_cursor = layer[pos - 1];
762 }
763 }
764 }
765
766 fn graph_move_down(&mut self) {
767 if self.graph_depths.is_empty() {
768 return;
769 }
770 let depth = self.graph_depths[self.graph_cursor];
771 let layer = &self.graph_layers[depth];
772 if let Some(pos) = layer.iter().position(|&i| i == self.graph_cursor) {
773 if pos + 1 < layer.len() {
774 self.graph_cursor = layer[pos + 1];
775 }
776 }
777 }
778
779 fn graph_move_right(&mut self) {
780 let current_id = self.graph_nodes[self.graph_cursor].0;
781 let mut seen = HashSet::new();
782 for e in &self.graph_edges {
783 if e.src_node == current_id && seen.insert(e.dst_node) {
784 if let Some(idx) = self
785 .graph_nodes
786 .iter()
787 .position(|(id, _)| *id == e.dst_node)
788 {
789 self.graph_cursor = idx;
790 return;
791 }
792 }
793 }
794 }
795
796 fn graph_move_left(&mut self) {
797 let current_id = self.graph_nodes[self.graph_cursor].0;
798 let mut seen = HashSet::new();
799 for e in &self.graph_edges {
800 if e.dst_node == current_id && seen.insert(e.src_node) {
801 if let Some(idx) = self
802 .graph_nodes
803 .iter()
804 .position(|(id, _)| *id == e.src_node)
805 {
806 self.graph_cursor = idx;
807 return;
808 }
809 }
810 }
811 }
812
813 fn enter_nested_graph(&mut self) {
814 let node_id = self.graph_nodes[self.graph_cursor].0;
815
816 self.graph_stack.push(GraphFrame {
817 nodes: self.graph_nodes.clone(),
818 edges: self.graph_edges.clone(),
819 cursor: self.graph_cursor,
820 params: self.graph_params.clone(),
821 param_values: self.graph_param_values.clone(),
822 param_groups: self.graph_param_groups.clone(),
823 depths: self.graph_depths.clone(),
824 layers: self.graph_layers.clone(),
825 has_children: self.graph_has_children.clone(),
826 node_descriptions: self.graph_node_descriptions.clone(),
827 });
828
829 let entered_name = self.graph_nodes[self.graph_cursor].1.clone();
830 self.graph_path.push(node_id);
831 self.graph_breadcrumb.push(entered_name);
832
833 if let Some(snap) = self.nested_graph_snapshots.get(&self.graph_path).cloned() {
834 self.load_graph_from_snapshot(&snap);
835 } else {
836 self.graph_nodes = vec![];
838 self.graph_edges = vec![];
839 self.graph_depths = vec![];
840 self.graph_layers = vec![];
841 self.graph_params = vec![];
842 self.graph_param_values = vec![];
843 self.graph_param_groups = vec![];
844 self.graph_has_children = vec![];
845 self.graph_cursor = 0;
846 }
847 }
848
849 fn exit_nested_graph(&mut self) {
850 if let Some(frame) = self.graph_stack.pop() {
851 self.graph_nodes = frame.nodes;
852 self.graph_edges = frame.edges;
853 self.graph_cursor = frame.cursor;
854 self.graph_params = frame.params;
855 self.graph_param_values = frame.param_values;
856 self.graph_param_groups = frame.param_groups;
857 self.graph_depths = frame.depths;
858 self.graph_layers = frame.layers;
859 self.graph_has_children = frame.has_children;
860 self.graph_node_descriptions = frame.node_descriptions;
861 self.graph_path.pop();
862 self.graph_breadcrumb.pop();
863 }
864 }
865
866 pub fn poll_audio(&mut self) {
868 if let Some((voice, time)) = self.preview_note_off {
870 if time.elapsed() > Duration::from_millis(120) {
871 self.bridge.send(Command::NoteOff { voice });
872 self.preview_note_off = None;
873 }
874 }
875
876 while let Some(notif) = self.bridge.try_recv() {
877 match notif {
878 Notification::Position { beat } => {
879 self.beat_position = beat;
880 let total_beats = self.grid.rows as f64;
881 if total_beats > 0.0 {
882 let row = (beat % total_beats) as u32;
883 self.current_play_row = Some(row.min(self.grid.rows.saturating_sub(1)));
884 }
885 }
886 Notification::ScopeData(snap) => {
887 self.scope_master = snap.master;
888 self.scope_graph_in = snap.graph_in;
889 }
890 Notification::Meter { peak_l, peak_r } => {
891 self.peak_l = peak_l;
892 self.peak_r = peak_r;
893 }
894 Notification::Stopped => {
895 self.playing = false;
896 self.engine_pattern_active = false;
897 self.current_play_row = None;
898 }
899 }
900 }
901 }
902
903 #[inline]
904 fn should_sync_pattern(&self) -> bool {
905 self.playing || self.engine_pattern_active
906 }
907
908 fn send_pattern(&mut self) {
909 let beats = Rational::integer(self.grid.rows as i64);
910 let events = trem::render::grid_to_timed_events(
911 &self.grid,
912 beats,
913 self.bpm,
914 44100.0,
915 &self.scale,
916 440.0,
917 &self.voice_ids,
918 self.swing,
919 );
920 self.bridge.send(Command::LoadEvents(events));
921 }
922
923 fn push_undo(&mut self) {
924 self.undo_stack.push(self.grid.cells.clone());
925 if self.undo_stack.len() > 100 {
926 self.undo_stack.remove(0);
927 }
928 self.redo_stack.clear();
929 }
930
931 fn undo(&mut self) {
932 if let Some(snapshot) = self.undo_stack.pop() {
933 self.redo_stack.push(self.grid.cells.clone());
934 self.grid.cells = snapshot;
935 if self.should_sync_pattern() {
936 self.send_pattern();
937 }
938 }
939 }
940
941 fn redo(&mut self) {
942 if let Some(snapshot) = self.redo_stack.pop() {
943 self.undo_stack.push(self.grid.cells.clone());
944 self.grid.cells = snapshot;
945 if self.should_sync_pattern() {
946 self.send_pattern();
947 }
948 }
949 }
950
951 pub fn draw(&mut self, frame: &mut ratatui::Frame) {
953 self.refresh_host_stats();
954
955 let bottom_h = match self.editor {
956 Editor::Graph => 6u16,
957 Editor::Pattern => 5u16,
958 };
959 let outer = Layout::default()
960 .direction(Direction::Vertical)
961 .constraints([
962 Constraint::Length(1),
963 Constraint::Min(4),
964 Constraint::Length(bottom_h),
965 ])
966 .split(frame.area());
967
968 frame.render_widget(
969 TransportView {
970 bpm: self.bpm,
971 beat_position: self.beat_position,
972 playing: self.playing,
973 mode: &self.mode,
974 editor: &self.editor,
975 scale_name: &self.scale_name,
976 octave: self.octave,
977 swing: self.swing,
978 bottom_pane: self.bottom_pane,
979 },
980 outer[0],
981 );
982
983 let sidebar_w = info_sidebar_width(outer[1].width);
984 let middle = Layout::default()
985 .direction(Direction::Horizontal)
986 .constraints([
987 Constraint::Length(sidebar_w),
988 Constraint::Min(MAIN_EDITOR_MIN_WIDTH),
989 ])
990 .split(outer[1]);
991
992 let note_at_cursor = self.grid.get(self.cursor_row, self.cursor_col);
993 let graph_node_name = match self.editor {
994 Editor::Graph => self
995 .graph_nodes
996 .get(self.graph_cursor)
997 .map(|(_, n)| n.as_str()),
998 Editor::Pattern => None,
999 };
1000 let graph_can_enter_nested = matches!(self.editor, Editor::Graph)
1001 && self
1002 .graph_has_children
1003 .get(self.graph_cursor)
1004 .copied()
1005 .unwrap_or(false);
1006 let graph_is_nested = !self.graph_path.is_empty();
1007
1008 frame.render_widget(
1009 InfoView {
1010 mode: &self.mode,
1011 editor: &self.editor,
1012 octave: self.octave,
1013 cursor_step: self.cursor_row,
1014 cursor_voice: self.cursor_col,
1015 grid_steps: self.grid.rows,
1016 grid_voices: self.grid.columns,
1017 note_at_cursor,
1018 scale: &self.scale,
1019 scale_name: &self.scale_name,
1020 instrument_names: &self.instrument_names,
1021 swing: self.swing,
1022 euclidean_k: self.euclidean_k,
1023 undo_depth: self.undo_stack.len(),
1024 node_description: self.current_node_description(),
1025 param_help: self.current_param_help(),
1026 graph_node_name,
1027 graph_can_enter_nested,
1028 graph_is_nested,
1029 host_stats: &self.host_stats,
1030 peak_l: self.peak_l,
1031 peak_r: self.peak_r,
1032 playing: self.playing,
1033 bpm: self.bpm,
1034 },
1035 middle[0],
1036 );
1037
1038 match self.editor {
1039 Editor::Pattern => {
1040 frame.render_widget(
1041 PatternView {
1042 grid: &self.grid,
1043 cursor_row: self.cursor_row,
1044 cursor_col: self.cursor_col,
1045 current_play_row: self.current_play_row,
1046 mode: &self.mode,
1047 scale: &self.scale,
1048 instrument_names: &self.instrument_names,
1049 },
1050 middle[1],
1051 );
1052 }
1053 Editor::Graph => {
1054 let params = self.graph_params.get(self.graph_cursor);
1055 let values = self.graph_param_values.get(self.graph_cursor);
1056 let groups = self.graph_param_groups.get(self.graph_cursor);
1057 frame.render_widget(
1058 GraphViewWidget {
1059 nodes: &self.graph_nodes,
1060 edges: &self.graph_edges,
1061 selected: self.graph_cursor,
1062 params: params.map(|p| p.as_slice()),
1063 param_values: values.map(|v| v.as_slice()),
1064 param_groups: groups.map(|g| g.as_slice()),
1065 param_cursor: if self.mode == Mode::Edit {
1066 Some(self.param_cursor)
1067 } else {
1068 None
1069 },
1070 breadcrumb: &self.graph_breadcrumb,
1071 has_children: &self.graph_has_children,
1072 },
1073 middle[1],
1074 );
1075 }
1076 }
1077
1078 let now = Instant::now();
1079 self.spectrum_analyzer_in.fall_ms = self.spectrum_fall_ms;
1080 self.spectrum_analyzer_out.fall_ms = self.spectrum_fall_ms;
1081 let (spec_in, nr_in) = self.spectrum_analyzer_in.analyze(&self.scope_graph_in, now);
1082 let (spec_out, nr_out) = self.spectrum_analyzer_out.analyze(&self.scope_master, now);
1083
1084 match (self.editor, self.bottom_pane) {
1085 (Editor::Graph, BottomPane::Waveform) => {
1086 let chunks = Layout::default()
1087 .direction(Direction::Horizontal)
1088 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
1089 .split(outer[2]);
1090 frame.render_widget(
1091 ScopeView {
1092 samples: &self.scope_graph_in,
1093 },
1094 chunks[0],
1095 );
1096 frame.render_widget(
1097 ScopeView {
1098 samples: &self.scope_master,
1099 },
1100 chunks[1],
1101 );
1102 }
1103 (Editor::Graph, BottomPane::Spectrum) => {
1104 let chunks = Layout::default()
1105 .direction(Direction::Horizontal)
1106 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
1107 .split(outer[2]);
1108 let fall = self.spectrum_fall_ms;
1109 frame.render_widget(
1110 SpectrumView {
1111 magnitudes: spec_in,
1112 norm_ref: nr_in,
1113 title: "IN",
1114 decay_ms_label: fall,
1115 },
1116 chunks[0],
1117 );
1118 frame.render_widget(
1119 SpectrumView {
1120 magnitudes: spec_out,
1121 norm_ref: nr_out,
1122 title: "OUT",
1123 decay_ms_label: fall,
1124 },
1125 chunks[1],
1126 );
1127 }
1128 (Editor::Pattern, BottomPane::Waveform) => {
1129 frame.render_widget(
1130 ScopeView {
1131 samples: &self.scope_master,
1132 },
1133 outer[2],
1134 );
1135 }
1136 (Editor::Pattern, BottomPane::Spectrum) => {
1137 frame.render_widget(
1138 SpectrumView {
1139 magnitudes: spec_out,
1140 norm_ref: nr_out,
1141 title: "OUT",
1142 decay_ms_label: self.spectrum_fall_ms,
1143 },
1144 outer[2],
1145 );
1146 }
1147 }
1148
1149 if self.help_open {
1150 frame.render_widget(HelpOverlay, frame.area());
1151 }
1152 }
1153
1154 pub fn run<B>(mut self, terminal: &mut ratatui::Terminal<B>) -> anyhow::Result<()>
1156 where
1157 B: ratatui::backend::Backend,
1158 B::Error: std::error::Error + Send + Sync + 'static,
1159 {
1160 self.sync_scope_focus();
1161 loop {
1162 terminal.draw(|frame| self.draw(frame))?;
1163
1164 if event::poll(Duration::from_millis(16))? {
1165 if let Event::Key(key) = event::read()? {
1166 if key.kind != KeyEventKind::Release {
1167 let ctx = InputContext {
1168 editor: self.editor,
1169 mode: &self.mode,
1170 graph_is_nested: !self.graph_path.is_empty(),
1171 help_open: self.help_open,
1172 };
1173 if let Some(action) = input::handle_key(key, &ctx) {
1174 self.handle_action(action);
1175 }
1176 }
1177 }
1178 }
1179
1180 self.poll_audio();
1181
1182 if self.should_quit {
1183 break;
1184 }
1185 }
1186 Ok(())
1187 }
1188}
1189
1190#[cfg(test)]
1191mod sidebar_width_tests {
1192 use super::{info_sidebar_width, MAIN_EDITOR_MIN_WIDTH};
1193
1194 #[test]
1195 fn narrow_middle_reserves_main_column() {
1196 let sw = info_sidebar_width(52);
1197 assert!(sw + MAIN_EDITOR_MIN_WIDTH <= 52, "sidebar={sw}");
1198 }
1199
1200 #[test]
1201 fn sidebar_caps_on_wide_terminals() {
1202 let sw = info_sidebar_width(200);
1203 assert!(sw <= 30, "sidebar={sw}");
1204 }
1205}