mod clip_view;
mod input;
mod loop_editor;
mod menu;
mod track;
mod transport_ui;
pub use clip_view::*;
pub use input::*;
pub use loop_editor::*;
pub use menu::*;
pub use track::*;
pub use transport_ui::*;
use phosphor_core::project::TrackKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Pane {
Transport,
Tracks,
ClipView,
}
impl Pane {
pub fn number(self) -> u8 {
match self {
Self::Transport => 1,
Self::Tracks => 2,
Self::ClipView => 3,
}
}
pub fn from_number(n: u8) -> Option<Self> {
match n {
1 => Some(Self::Transport),
2 => Some(Self::Tracks),
3 => Some(Self::ClipView),
_ => None,
}
}
pub fn next(self) -> Self {
match self {
Self::Transport => Self::Tracks,
Self::Tracks => Self::ClipView,
Self::ClipView => Self::Transport,
}
}
pub fn prev(self) -> Self {
match self {
Self::Transport => Self::ClipView,
Self::Tracks => Self::Transport,
Self::ClipView => Self::Tracks,
}
}
pub fn label(self) -> &'static str {
match self {
Self::Transport => "transport",
Self::Tracks => "tracks",
Self::ClipView => "clip",
}
}
}
pub const MAX_VISIBLE_TRACKS: usize = 5;
pub const INST_CONFIG_PARAM_COUNT: usize = 15;
#[derive(Debug)]
pub struct NavState {
pub focused_pane: Pane,
pub track_cursor: usize,
pub track_scroll: usize,
pub track_selected: bool,
pub track_element: TrackElement,
pub number_buf: NumberBuffer,
pub space_menu: SpaceMenu,
pub clip_view: ClipViewState,
pub clip_view_visible: bool,
pub clip_view_target: Option<(usize, usize)>,
pub fx_menu: FxMenu,
pub instrument_modal: InstrumentModal,
pub loop_editor: LoopEditor,
pub transport_ui: TransportUiState,
pub tracks: Vec<TrackState>,
pub input_modal: InputModal,
}
impl NavState {
pub fn new(tracks: Vec<TrackState>) -> Self {
Self {
focused_pane: Pane::Tracks,
track_cursor: 0,
track_scroll: 0,
track_selected: false,
track_element: TrackElement::Label,
number_buf: NumberBuffer::new(),
space_menu: SpaceMenu::new(),
clip_view: ClipViewState::new(),
clip_view_visible: false,
clip_view_target: None,
fx_menu: FxMenu::new(),
instrument_modal: InstrumentModal::new(),
loop_editor: LoopEditor::new(),
transport_ui: TransportUiState::new(),
tracks,
input_modal: InputModal::new(),
}
}
pub fn toggle_space_menu(&mut self) {
self.space_menu.toggle();
}
pub fn space_menu_handle(&mut self, ch: char) -> Option<SpaceAction> {
self.space_menu.open = false;
match ch {
'1' => { self.focus_pane(Pane::Transport); None }
'2' => { self.focus_pane(Pane::Tracks); None }
'3' => { self.focus_pane(Pane::ClipView); None }
'p' => Some(SpaceAction::PlayPause),
'r' => Some(SpaceAction::ToggleRecord),
'l' => Some(SpaceAction::ToggleLoop),
'm' => Some(SpaceAction::ToggleMetronome),
'!' => Some(SpaceAction::Panic),
'a' => Some(SpaceAction::AddInstrument),
's' => Some(SpaceAction::Save),
'o' => Some(SpaceAction::Open),
'n' => Some(SpaceAction::NewTrack),
'h' => {
self.space_menu.open = true;
self.space_menu.section = SpaceMenuSection::Help;
self.space_menu.cursor = 0;
None
}
_ => None,
}
}
pub fn focus_pane(&mut self, pane: Pane) {
if self.focused_pane == Pane::Tracks { self.track_selected = false; }
self.focused_pane = pane;
crate::debug_log::system(&format!("focused pane: {:?}", pane));
}
pub fn focus_next_pane(&mut self) {
self.focus_pane(self.focused_pane.next());
}
pub fn move_up(&mut self) {
if self.instrument_modal.open { self.instrument_modal.move_up(); return; }
if self.space_menu.open { self.space_menu.move_up(); return; }
if self.fx_menu.open { self.fx_menu.move_up(); return; }
match self.focused_pane {
Pane::Transport => {} Pane::Tracks if !self.track_selected => {
if self.track_cursor > 0 {
self.track_cursor -= 1;
if self.track_cursor < self.track_scroll {
self.track_scroll = self.track_cursor;
}
}
}
Pane::Tracks => {
}
Pane::ClipView => {
match self.clip_view.focus {
ClipViewFocus::PianoRoll if self.clip_view.clip_tab == ClipTab::InstConfig => {
if self.clip_view.inst_config_cursor > 0 {
self.clip_view.inst_config_cursor -= 1;
}
}
ClipViewFocus::PianoRoll => self.clip_view.piano_roll.move_up(),
ClipViewFocus::FxPanel => {
if self.clip_view.fx_panel_tab == FxPanelTab::Synth {
if self.clip_view.synth_param_cursor > 0 {
self.clip_view.synth_param_cursor -= 1;
}
} else if self.clip_view.fx_cursor > 0 {
self.clip_view.fx_cursor -= 1;
}
}
}
}
}
}
pub fn move_down(&mut self) {
if self.instrument_modal.open { self.instrument_modal.move_down(); return; }
if self.space_menu.open { self.space_menu.move_down(); return; }
if self.fx_menu.open { self.fx_menu.move_down(); return; }
match self.focused_pane {
Pane::Transport => {}
Pane::Tracks if !self.track_selected => {
if self.track_cursor + 1 < self.tracks.len() {
self.track_cursor += 1;
if self.track_cursor >= self.track_scroll + MAX_VISIBLE_TRACKS {
self.track_scroll = self.track_cursor + 1 - MAX_VISIBLE_TRACKS;
}
}
}
Pane::Tracks => {
}
Pane::ClipView => {
match self.clip_view.focus {
ClipViewFocus::PianoRoll if self.clip_view.clip_tab == ClipTab::InstConfig => {
if self.clip_view.inst_config_cursor + 1 < INST_CONFIG_PARAM_COUNT {
self.clip_view.inst_config_cursor += 1;
}
}
ClipViewFocus::PianoRoll => self.clip_view.piano_roll.move_down(),
ClipViewFocus::FxPanel => {
if self.clip_view.fx_panel_tab == FxPanelTab::Synth {
let max = self.current_track().map(|t| t.synth_params.len()).unwrap_or(0);
if self.clip_view.synth_param_cursor + 1 < max {
self.clip_view.synth_param_cursor += 1;
}
} else {
let max = self.active_fx_chain_len();
if self.clip_view.fx_cursor + 1 < max {
self.clip_view.fx_cursor += 1;
}
}
}
}
}
}
}
pub fn move_left(&mut self) {
if self.focused_pane == Pane::Tracks && self.track_selected {
self.track_element = self.track_element.move_left();
} else if self.focused_pane == Pane::ClipView {
match self.clip_view.focus {
ClipViewFocus::PianoRoll if self.clip_view.clip_tab == ClipTab::InstConfig => {
}
ClipViewFocus::PianoRoll => {
self.clip_view.focus = ClipViewFocus::FxPanel;
}
ClipViewFocus::FxPanel if self.clip_view.fx_panel_tab == FxPanelTab::Synth => {
self.adjust_synth_param(-0.05);
}
_ => {}
}
}
}
pub fn move_right(&mut self) {
if self.focused_pane == Pane::Tracks && self.track_selected {
let num_clips = self.current_track().map(|t| t.clips.len()).unwrap_or(0);
self.track_element = self.track_element.move_right(num_clips);
} else if self.focused_pane == Pane::ClipView {
match self.clip_view.focus {
ClipViewFocus::PianoRoll if self.clip_view.clip_tab == ClipTab::InstConfig => {
}
ClipViewFocus::FxPanel if self.clip_view.fx_panel_tab == FxPanelTab::Synth => {
self.adjust_synth_param(0.05);
}
ClipViewFocus::FxPanel => {
self.clip_view.focus = ClipViewFocus::PianoRoll;
}
_ => {}
}
}
}
pub fn adjust_synth_param(&mut self, delta: f32) -> Option<(usize, usize, f32)> {
let idx = self.clip_view.synth_param_cursor;
if let Some(track) = self.tracks.get_mut(self.track_cursor) {
if idx < track.synth_params.len() {
let is_jupiter = track.instrument_type == Some(InstrumentType::Jupiter8);
let is_odyssey = track.instrument_type == Some(InstrumentType::Odyssey);
let is_juno = track.instrument_type == Some(InstrumentType::Juno60);
let is_discrete = if is_jupiter {
phosphor_dsp::jupiter::is_discrete(idx)
} else if is_odyssey {
phosphor_dsp::odyssey::is_discrete(idx)
} else if is_juno {
phosphor_dsp::juno::is_discrete(idx)
} else {
idx == 0
};
let actual_delta = if is_discrete {
let step = if is_jupiter {
match idx {
0 => 1.0 / (phosphor_dsp::jupiter::PATCH_COUNT as f32 - 0.01),
_ => 0.25,
}
} else if is_odyssey {
match idx {
0 => 1.0 / (phosphor_dsp::odyssey::PATCH_COUNT as f32 - 0.01),
6 => 0.34, _ => 0.5,
}
} else if is_juno {
match idx {
0 => 1.0 / (phosphor_dsp::juno::PATCH_COUNT as f32 - 0.01),
12 => 0.25, _ => 0.5, }
} else {
match track.instrument_type {
Some(InstrumentType::DrumRack) => 0.1, Some(InstrumentType::DX7) => 1.0 / (phosphor_dsp::dx7::PATCH_COUNT as f32 - 0.01),
_ => 0.25,
}
};
if delta > 0.0 { step } else { -step }
} else {
delta
};
let new_val = (track.synth_params[idx] + actual_delta).clamp(0.0, 1.0);
track.synth_params[idx] = new_val;
if idx == 0 {
let new_params = match track.instrument_type {
Some(InstrumentType::Jupiter8) => {
Some(phosphor_dsp::jupiter::Jupiter8Synth::params_for_patch(new_val))
}
Some(InstrumentType::Odyssey) => {
Some(phosphor_dsp::odyssey::OdysseySynth::params_for_patch(new_val))
}
Some(InstrumentType::Juno60) => {
Some(phosphor_dsp::juno::Juno60Synth::params_for_patch(new_val))
}
_ => None,
};
if let Some(preset_params) = new_params {
for (i, &v) in preset_params.iter().enumerate() {
track.synth_params[i] = v;
}
}
}
if let Some(mixer_id) = track.mixer_id {
return Some((mixer_id, idx, new_val));
}
}
}
None
}
pub fn enter(&mut self) -> Option<SpaceAction> {
if self.space_menu.open {
match self.space_menu.section {
SpaceMenuSection::Actions => {
if let Some((key, _, _)) = SPACE_ACTIONS.get(self.space_menu.cursor) {
if let Some(ch) = key.strip_prefix("spc+").and_then(|s| s.chars().next()) {
return self.space_menu_handle(ch);
}
}
self.space_menu.open = false;
return None;
}
SpaceMenuSection::Help => {
return None;
}
}
}
if self.fx_menu.open {
self.fx_menu_select();
return None;
}
match self.focused_pane {
Pane::Transport => {} Pane::Tracks => {
if !self.track_selected {
self.track_selected = true;
self.track_element = TrackElement::Label;
self.show_current_track_controls();
} else {
self.activate_element();
}
}
Pane::ClipView => {}
}
None
}
pub fn escape(&mut self) {
if self.instrument_modal.open {
self.instrument_modal.open = false;
return;
}
if self.space_menu.open {
self.space_menu.open = false;
return;
}
if self.fx_menu.open {
self.fx_menu.open = false;
return;
}
match self.focused_pane {
Pane::Transport => {} Pane::Tracks => {
if self.track_selected {
self.track_selected = false;
self.track_element = TrackElement::Label;
self.clip_view_visible = false;
self.clip_view_target = None;
}
}
Pane::ClipView => self.focus_pane(Pane::Tracks),
}
}
pub fn cycle_tab(&mut self) {
if self.focused_pane != Pane::ClipView { return; }
match (self.clip_view.focus, self.clip_view.fx_panel_tab, self.clip_view.clip_tab) {
(ClipViewFocus::FxPanel, FxPanelTab::TrackFx, _) => {
self.clip_view.fx_panel_tab = FxPanelTab::Synth;
}
(ClipViewFocus::FxPanel, FxPanelTab::Synth, _) => {
self.clip_view.focus = ClipViewFocus::PianoRoll;
self.clip_view.clip_tab = ClipTab::InstConfig;
self.clip_view.inst_config_cursor = 0;
}
(ClipViewFocus::PianoRoll, _, ClipTab::InstConfig) => {
self.clip_view.clip_tab = ClipTab::PianoRoll;
self.clip_view.piano_roll.focus = PianoRollFocus::Navigation;
self.clip_view.piano_roll.column = 0;
}
(ClipViewFocus::PianoRoll, _, ClipTab::PianoRoll) => {
self.clip_view.clip_tab = ClipTab::Automation;
}
(ClipViewFocus::PianoRoll, _, ClipTab::Automation) => {
self.clip_view.focus = ClipViewFocus::FxPanel;
self.clip_view.fx_panel_tab = FxPanelTab::TrackFx;
self.clip_view.fx_cursor = 0;
}
}
}
pub fn toggle_mute(&mut self) {
if let Some(t) = self.current_track_mut() {
t.muted = !t.muted;
t.sync_to_audio();
}
}
pub fn toggle_solo(&mut self) {
if let Some(t) = self.current_track_mut() {
t.soloed = !t.soloed;
t.sync_to_audio();
}
}
pub fn toggle_arm(&mut self) {
if let Some(t) = self.current_track_mut() {
t.armed = !t.armed;
t.sync_to_audio();
}
}
pub fn digit_input(&mut self, ch: char) {
if self.focused_pane == Pane::Tracks && self.track_selected {
self.number_buf.push_digit(ch);
}
}
pub fn tick(&mut self) {
if let Some(clip_num) = self.number_buf.check_timeout() {
self.jump_to_clip(clip_num);
}
}
pub fn jump_to_clip(&mut self, clip_number: usize) {
if let Some(track) = self.current_track() {
if let Some(idx) = track.clips.iter().position(|c| c.number == clip_number) {
self.track_element = TrackElement::Clip(idx);
self.open_clip_view(self.track_cursor, idx);
}
}
}
fn activate_element(&mut self) {
match self.track_element {
TrackElement::Mute => self.toggle_mute(),
TrackElement::Solo => self.toggle_solo(),
TrackElement::RecordArm => self.toggle_arm(),
TrackElement::Fx => {
self.fx_menu.open = true;
self.fx_menu.cursor = 0;
}
TrackElement::Volume => { }
TrackElement::Clip(idx) => {
self.open_clip_view(self.track_cursor, idx);
self.clip_view.clip_tab = ClipTab::PianoRoll;
self.clip_view.focus = ClipViewFocus::PianoRoll;
}
_ => {}
}
}
pub fn add_instrument_track(
&mut self,
instrument: InstrumentType,
mixer_id: usize,
handle: std::sync::Arc<phosphor_core::project::TrackHandle>,
) {
let name = match instrument {
InstrumentType::Synth => "synth",
InstrumentType::DrumRack => "drums",
InstrumentType::DX7 => "dx7",
InstrumentType::Jupiter8 => "jup8",
InstrumentType::Odyssey => "odyss",
InstrumentType::Juno60 => "juno",
InstrumentType::Sampler => "smplr",
};
let insert_pos = self.tracks.iter().position(|t| {
matches!(t.kind, TrackKind::SendA | TrackKind::SendB | TrackKind::Master)
}).unwrap_or(self.tracks.len());
let color = insert_pos % 8;
let mut track = TrackState::new(name, color, true, TrackKind::Instrument, vec![]);
track.mixer_id = Some(mixer_id);
track.handle = Some(handle);
track.instrument_type = Some(instrument);
track.synth_params = match instrument {
InstrumentType::Synth | InstrumentType::Sampler => {
phosphor_dsp::synth::PARAM_DEFAULTS.to_vec()
}
InstrumentType::DrumRack => {
phosphor_dsp::drum_rack::PARAM_DEFAULTS.to_vec()
}
InstrumentType::DX7 => {
phosphor_dsp::dx7::PARAM_DEFAULTS.to_vec()
}
InstrumentType::Jupiter8 => {
phosphor_dsp::jupiter::PARAM_DEFAULTS.to_vec()
}
InstrumentType::Odyssey => {
phosphor_dsp::odyssey::PARAM_DEFAULTS.to_vec()
}
InstrumentType::Juno60 => {
phosphor_dsp::juno::PARAM_DEFAULTS.to_vec()
}
};
track.sync_to_audio();
self.tracks.insert(insert_pos, track);
self.track_cursor = insert_pos;
if self.track_cursor >= self.track_scroll + MAX_VISIBLE_TRACKS {
self.track_scroll = self.track_cursor + 1 - MAX_VISIBLE_TRACKS;
}
self.track_selected = true;
self.track_element = TrackElement::Label;
self.show_current_track_controls();
}
fn open_clip_view(&mut self, track_idx: usize, clip_idx: usize) {
self.clip_view_visible = true;
self.clip_view_target = Some((track_idx, clip_idx));
self.clip_view.fx_cursor = 0;
}
pub fn show_current_track_controls(&mut self) {
for track in &self.tracks {
if let Some(ref h) = track.handle {
h.config.midi_active.store(false, std::sync::atomic::Ordering::Relaxed);
}
}
if let Some(track) = self.tracks.get(self.track_cursor) {
if track.is_live() {
if let Some(ref h) = track.handle {
h.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
}
self.clip_view_visible = true;
self.clip_view_target = Some((self.track_cursor, 0));
if !track.clips.is_empty() {
self.clip_view.clip_tab = ClipTab::PianoRoll;
self.clip_view.focus = ClipViewFocus::PianoRoll;
self.clip_view.piano_roll.focus = PianoRollFocus::Navigation;
self.clip_view.piano_roll.column = 0;
} else {
self.clip_view.fx_panel_tab = FxPanelTab::Synth;
self.clip_view.focus = ClipViewFocus::FxPanel;
self.clip_view.synth_param_cursor = 0;
}
} else {
self.clip_view_visible = false;
self.clip_view_target = None;
}
}
}
fn fx_menu_select(&mut self) {
if let Some(fx_type) = FxType::ALL.get(self.fx_menu.cursor) {
let inst = FxInstance::new(*fx_type);
if let Some(t) = self.current_track_mut() {
t.fx_chain.push(inst);
}
}
self.fx_menu.open = false;
}
fn active_fx_chain_len(&self) -> usize {
match self.clip_view.fx_panel_tab {
FxPanelTab::TrackFx | FxPanelTab::Synth => {
self.current_track().map(|t| t.fx_chain.len().max(1)).unwrap_or(1)
}
}
}
pub fn visible_tracks(&self) -> &[TrackState] {
let end = (self.track_scroll + MAX_VISIBLE_TRACKS).min(self.tracks.len());
&self.tracks[self.track_scroll..end]
}
pub fn can_scroll_up(&self) -> bool { self.track_scroll > 0 }
pub fn can_scroll_down(&self) -> bool {
self.track_scroll + MAX_VISIBLE_TRACKS < self.tracks.len()
}
pub fn current_track(&self) -> Option<&TrackState> { self.tracks.get(self.track_cursor) }
fn current_track_mut(&mut self) -> Option<&mut TrackState> {
self.tracks.get_mut(self.track_cursor)
}
pub fn active_clip(&self) -> Option<&Clip> {
let (ti, ci) = self.clip_view_target?;
self.tracks.get(ti)?.clips.get(ci)
}
pub fn active_clip_mut(&mut self) -> Option<&mut Clip> {
let (ti, ci) = self.clip_view_target?;
self.tracks.get_mut(ti)?.clips.get_mut(ci)
}
pub fn active_clip_track(&self) -> Option<&TrackState> {
let (ti, _) = self.clip_view_target?;
self.tracks.get(ti)
}
pub fn receive_clip_snapshot(&mut self, snap: phosphor_core::clip::ClipSnapshot) {
crate::debug_log::system(&format!(
"clip received: track={} events={} notes={} ticks={}..{}",
snap.track_id, snap.event_count, snap.notes.len(),
snap.start_tick, snap.start_tick + snap.length_ticks,
));
if let Some(track) = self.tracks.iter_mut().find(|t| t.mixer_id == Some(snap.track_id)) {
let ppq = phosphor_core::transport::Transport::PPQ;
let beats = (snap.length_ticks as f64 / ppq as f64).ceil() as u16;
let width = beats.max(2);
let clip_number = track.clips.len() + 1;
track.clips.push(Clip {
number: clip_number,
width,
has_content: true,
start_tick: snap.start_tick,
length_ticks: snap.length_ticks,
notes: snap.notes,
});
}
}
}
pub fn initial_tracks() -> Vec<TrackState> {
vec![
TrackState::new("snd a", 5, false, TrackKind::SendA, vec![]),
TrackState::new("snd b", 6, false, TrackKind::SendB, vec![]),
TrackState::new("mstr", 7, false, TrackKind::Master, vec![]),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pane_numbers() {
assert_eq!(Pane::Transport.number(), 1);
assert_eq!(Pane::Tracks.number(), 2);
assert_eq!(Pane::ClipView.number(), 3);
assert_eq!(Pane::from_number(1), Some(Pane::Transport));
assert_eq!(Pane::from_number(2), Some(Pane::Tracks));
assert_eq!(Pane::from_number(3), Some(Pane::ClipView));
assert_eq!(Pane::from_number(9), None);
}
#[test]
fn track_element_navigation_full() {
let e = TrackElement::Label;
assert_eq!(e.move_right(3), TrackElement::Fx);
assert_eq!(TrackElement::Fx.move_right(3), TrackElement::Volume);
assert_eq!(TrackElement::Volume.move_right(3), TrackElement::Mute);
assert_eq!(TrackElement::Mute.move_right(3), TrackElement::Solo);
assert_eq!(TrackElement::Solo.move_right(3), TrackElement::RecordArm);
assert_eq!(TrackElement::RecordArm.move_right(3), TrackElement::Clip(0));
assert_eq!(TrackElement::Clip(2).move_right(3), TrackElement::Clip(2));
}
#[test]
fn track_element_left_full() {
assert_eq!(TrackElement::Clip(0).move_left(), TrackElement::RecordArm);
assert_eq!(TrackElement::RecordArm.move_left(), TrackElement::Solo);
assert_eq!(TrackElement::Solo.move_left(), TrackElement::Mute);
assert_eq!(TrackElement::Mute.move_left(), TrackElement::Volume);
assert_eq!(TrackElement::Volume.move_left(), TrackElement::Fx);
assert_eq!(TrackElement::Fx.move_left(), TrackElement::Label);
assert_eq!(TrackElement::Label.move_left(), TrackElement::Label);
}
#[test]
fn initial_tracks_has_sends_and_master() {
let tracks = initial_tracks();
assert_eq!(tracks.len(), 3); assert_eq!(tracks[0].kind, TrackKind::SendA);
assert_eq!(tracks[1].kind, TrackKind::SendB);
assert_eq!(tracks[2].kind, TrackKind::Master);
}
#[test]
fn sends_are_at_end() {
let mut nav = NavState::new(initial_tracks());
nav.move_down();
nav.move_down();
assert_eq!(nav.track_cursor, 2);
assert_eq!(nav.tracks[nav.track_cursor].kind, TrackKind::Master);
}
#[test]
fn fx_menu_opens_and_closes() {
let mut nav = NavState::new(initial_tracks());
nav.enter(); nav.move_right(); assert_eq!(nav.track_element, TrackElement::Fx);
nav.enter(); assert!(nav.fx_menu.open);
nav.escape(); assert!(!nav.fx_menu.open);
}
#[test]
fn fx_menu_add_effect() {
let mut nav = NavState::new(initial_tracks());
let initial_count = nav.tracks[0].fx_chain.len();
nav.enter();
nav.move_right(); nav.enter(); nav.enter(); assert!(!nav.fx_menu.open);
assert_eq!(nav.tracks[0].fx_chain.len(), initial_count + 1);
assert_eq!(nav.tracks[0].fx_chain.last().unwrap().fx_type, FxType::Reverb);
}
#[test]
fn clip_view_focus_toggle() {
let mut nav = NavState::new(initial_tracks());
nav.clip_view_visible = true;
nav.clip_view_target = Some((0, 0));
nav.focus_pane(Pane::ClipView);
assert_eq!(nav.clip_view.focus, ClipViewFocus::PianoRoll);
nav.move_left(); assert_eq!(nav.clip_view.focus, ClipViewFocus::FxPanel);
}
#[test]
fn clip_view_tabs_cycle() {
let mut nav = NavState::new(initial_tracks());
nav.focused_pane = Pane::ClipView;
nav.clip_view.focus = ClipViewFocus::FxPanel;
assert_eq!(nav.clip_view.fx_panel_tab, FxPanelTab::TrackFx);
nav.cycle_tab();
assert_eq!(nav.clip_view.fx_panel_tab, FxPanelTab::Synth);
nav.cycle_tab();
assert_eq!(nav.clip_view.focus, ClipViewFocus::PianoRoll);
assert_eq!(nav.clip_view.clip_tab, ClipTab::InstConfig);
nav.cycle_tab();
assert_eq!(nav.clip_view.clip_tab, ClipTab::PianoRoll);
nav.cycle_tab();
assert_eq!(nav.clip_view.clip_tab, ClipTab::Automation);
nav.cycle_tab();
assert_eq!(nav.clip_view.focus, ClipViewFocus::FxPanel);
assert_eq!(nav.clip_view.fx_panel_tab, FxPanelTab::TrackFx);
}
#[test]
fn arm_toggle() {
let mut nav = NavState::new(initial_tracks());
assert!(!nav.tracks[0].armed); nav.toggle_arm();
assert!(nav.tracks[0].armed);
nav.toggle_arm();
assert!(!nav.tracks[0].armed);
}
#[test]
fn space_menu_toggle() {
let mut nav = NavState::new(initial_tracks());
assert!(!nav.space_menu.open);
nav.toggle_space_menu();
assert!(nav.space_menu.open);
nav.toggle_space_menu();
assert!(!nav.space_menu.open);
}
#[test]
fn space_menu_handle_pane_jump() {
let mut nav = NavState::new(initial_tracks());
nav.toggle_space_menu();
let action = nav.space_menu_handle('2');
assert_eq!(nav.focused_pane, Pane::Tracks);
assert!(action.is_none());
assert!(!nav.space_menu.open);
nav.toggle_space_menu();
let action = nav.space_menu_handle('1');
assert_eq!(nav.focused_pane, Pane::Transport);
assert!(action.is_none());
}
#[test]
fn space_menu_handle_play_pause() {
let mut nav = NavState::new(initial_tracks());
nav.toggle_space_menu();
let action = nav.space_menu_handle('p');
assert_eq!(action, Some(SpaceAction::PlayPause));
assert!(!nav.space_menu.open);
}
#[test]
fn space_menu_enter_select() {
let mut nav = NavState::new(initial_tracks());
nav.toggle_space_menu();
let action = nav.enter();
assert!(action.is_none()); assert!(!nav.space_menu.open);
}
#[test]
fn space_menu_nav_and_help() {
let mut nav = NavState::new(initial_tracks());
nav.toggle_space_menu();
assert_eq!(nav.space_menu.section, SpaceMenuSection::Actions);
nav.space_menu.switch_section();
assert_eq!(nav.space_menu.section, SpaceMenuSection::Help);
assert_eq!(nav.space_menu.cursor, 0);
}
#[test]
fn number_buffer_commit() {
let mut buf = NumberBuffer::new();
buf.push_digit('1');
assert_eq!(buf.commit(), Some(1));
buf.push_digit('1');
buf.push_digit('2');
assert_eq!(buf.commit(), Some(12));
}
#[test]
fn number_buffer_empty_commit() {
assert_eq!(NumberBuffer::new().commit(), None);
}
#[test]
fn nav_cursor_bounds() {
let mut nav = NavState::new(initial_tracks());
for _ in 0..20 { nav.move_down(); }
assert_eq!(nav.track_cursor, 2); }
#[test]
fn enter_escape_track() {
let mut nav = NavState::new(initial_tracks());
nav.enter();
assert!(nav.track_selected);
nav.escape();
assert!(!nav.track_selected);
}
#[test]
fn mute_solo_toggle() {
let mut nav = NavState::new(initial_tracks());
nav.toggle_mute();
assert!(nav.tracks[0].muted);
nav.toggle_solo();
assert!(nav.tracks[0].soloed);
}
#[test]
fn volume_element_in_chain() {
let e = TrackElement::Fx;
assert_eq!(e.move_right(1), TrackElement::Volume);
assert_eq!(TrackElement::Volume.move_left(), TrackElement::Fx);
}
}