use crossterm::event::{KeyCode, KeyEvent}; use std::sync::atomic::Ordering; use super::{InputContext, InputResult}; use crate::commands::AppCommand; use crate::state::{ConfirmAction, Modal, OptionsFocus}; pub(crate) fn cycle_option_value(ctx: &mut InputContext, right: bool) { match ctx.app.options.focus { OptionsFocus::ColorScheme => { let new_scheme = if right { ctx.app.ui.color_scheme.next() } else { ctx.app.ui.color_scheme.prev() }; ctx.dispatch(AppCommand::SetColorScheme(new_scheme)); } OptionsFocus::HueRotation => { let delta = if right { 5.0 } else { -5.0 }; let new_rotation = (ctx.app.ui.hue_rotation + delta).rem_euclid(360.0); ctx.dispatch(AppCommand::SetHueRotation(new_rotation)); } OptionsFocus::RefreshRate => ctx.dispatch(AppCommand::ToggleRefreshRate), OptionsFocus::RuntimeHighlight => ctx.dispatch(AppCommand::ToggleRuntimeHighlight), OptionsFocus::ShowScope => ctx.dispatch(AppCommand::ToggleScope), OptionsFocus::ShowSpectrum => ctx.dispatch(AppCommand::ToggleSpectrum), OptionsFocus::ShowLissajous => ctx.dispatch(AppCommand::ToggleLissajous), OptionsFocus::ShowCompletion => ctx.dispatch(AppCommand::ToggleCompletion), OptionsFocus::ShowPreview => ctx.dispatch(AppCommand::TogglePreview), OptionsFocus::PerformanceMode => ctx.dispatch(AppCommand::TogglePerformanceMode), OptionsFocus::Font => { const FONTS: &[&str] = &["6x13", "7x13", "8x13", "9x15", "9x18", "10x20"]; let pos = FONTS.iter().position(|f| *f == ctx.app.ui.font).unwrap_or(2); let new_pos = if right { (pos + 1) % FONTS.len() } else { (pos + FONTS.len() - 1) % FONTS.len() }; ctx.dispatch(AppCommand::SetFont(FONTS[new_pos].to_string())); } OptionsFocus::ZoomFactor => { const ZOOMS: &[f32] = &[0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0]; let pos = ZOOMS .iter() .position(|z| (z - ctx.app.ui.zoom_factor).abs() < 0.01) .unwrap_or(4); let new_pos = if right { (pos + 1) % ZOOMS.len() } else { (pos + ZOOMS.len() - 1) % ZOOMS.len() }; ctx.dispatch(AppCommand::SetZoomFactor(ZOOMS[new_pos])); } OptionsFocus::WindowSize => { const WINDOW_SIZES: &[(u32, u32)] = &[ (900, 600), (1050, 700), (1200, 800), (1350, 900), (1500, 1000), ]; let pos = WINDOW_SIZES .iter() .position(|&(w, h)| w == ctx.app.ui.window_width && h == ctx.app.ui.window_height) .unwrap_or_else(|| { WINDOW_SIZES .iter() .enumerate() .min_by_key(|&(_, &(w, h))| { let dw = w as i64 - ctx.app.ui.window_width as i64; let dh = h as i64 - ctx.app.ui.window_height as i64; dw * dw + dh * dh }) .map(|(i, _)| i) .unwrap_or(2) }); let new_pos = if right { (pos + 1) % WINDOW_SIZES.len() } else { (pos + WINDOW_SIZES.len() - 1) % WINDOW_SIZES.len() }; let (w, h) = WINDOW_SIZES[new_pos]; ctx.dispatch(AppCommand::SetWindowSize(w, h)); } OptionsFocus::LinkEnabled => ctx.link.set_enabled(!ctx.link.is_enabled()), OptionsFocus::StartStopSync => ctx .link .set_start_stop_sync_enabled(!ctx.link.is_start_stop_sync_enabled()), OptionsFocus::Quantum => { let delta = if right { 1.0 } else { -1.0 }; ctx.link.set_quantum(ctx.link.quantum() + delta); } OptionsFocus::MidiOutput0 | OptionsFocus::MidiOutput1 | OptionsFocus::MidiOutput2 | OptionsFocus::MidiOutput3 => { let slot = match ctx.app.options.focus { OptionsFocus::MidiOutput0 => 0, OptionsFocus::MidiOutput1 => 1, OptionsFocus::MidiOutput2 => 2, OptionsFocus::MidiOutput3 => 3, _ => 0, }; let all_devices = crate::midi::list_midi_outputs(); let available: Vec<(usize, &crate::midi::MidiDeviceInfo)> = all_devices .iter() .enumerate() .filter(|(idx, _)| { ctx.app.midi.selected_outputs[slot] == Some(*idx) || !ctx .app .midi .selected_outputs .iter() .enumerate() .any(|(s, sel)| s != slot && *sel == Some(*idx)) }) .collect(); let total_options = available.len() + 1; let current_pos = ctx.app.midi.selected_outputs[slot] .and_then(|idx| available.iter().position(|(i, _)| *i == idx)) .map(|p| p + 1) .unwrap_or(0); let new_pos = if right { (current_pos + 1) % total_options } else if current_pos == 0 { total_options - 1 } else { current_pos - 1 }; if new_pos == 0 { ctx.app.midi.disconnect_output(slot); ctx.dispatch(AppCommand::SetStatus(format!( "MIDI output {slot}: disconnected" ))); } else { let (device_idx, device) = available[new_pos - 1]; if ctx.app.midi.connect_output(slot, device_idx).is_ok() { ctx.dispatch(AppCommand::SetStatus(format!( "MIDI output {}: {}", slot, device.name ))); } } } OptionsFocus::ResetOnboarding => { ctx.dispatch(AppCommand::ResetOnboarding); } OptionsFocus::LoadDemoOnStartup => { ctx.app.ui.load_demo_on_startup = !ctx.app.ui.load_demo_on_startup; } OptionsFocus::MidiInput0 | OptionsFocus::MidiInput1 | OptionsFocus::MidiInput2 | OptionsFocus::MidiInput3 => { let slot = match ctx.app.options.focus { OptionsFocus::MidiInput0 => 0, OptionsFocus::MidiInput1 => 1, OptionsFocus::MidiInput2 => 2, OptionsFocus::MidiInput3 => 3, _ => 0, }; let all_devices = crate::midi::list_midi_inputs(); let available: Vec<(usize, &crate::midi::MidiDeviceInfo)> = all_devices .iter() .enumerate() .filter(|(idx, _)| { ctx.app.midi.selected_inputs[slot] == Some(*idx) || !ctx .app .midi .selected_inputs .iter() .enumerate() .any(|(s, sel)| s != slot && *sel == Some(*idx)) }) .collect(); let total_options = available.len() + 1; let current_pos = ctx.app.midi.selected_inputs[slot] .and_then(|idx| available.iter().position(|(i, _)| *i == idx)) .map(|p| p + 1) .unwrap_or(0); let new_pos = if right { (current_pos + 1) % total_options } else if current_pos == 0 { total_options - 1 } else { current_pos - 1 }; if new_pos == 0 { ctx.app.midi.disconnect_input(slot); ctx.dispatch(AppCommand::SetStatus(format!( "MIDI input {slot}: disconnected" ))); } else { let (device_idx, device) = available[new_pos - 1]; if ctx.app.midi.connect_input(slot, device_idx).is_ok() { ctx.dispatch(AppCommand::SetStatus(format!( "MIDI input {}: {}", slot, device.name ))); } } } } ctx.app.save_settings(ctx.link); } pub(super) fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult { match key.code { KeyCode::Char('q') if !ctx.app.plugin_mode => { ctx.dispatch(AppCommand::OpenModal(Modal::Confirm { action: ConfirmAction::Quit, selected: false, })); } KeyCode::Down | KeyCode::Tab => ctx.dispatch(AppCommand::OptionsNextFocus), KeyCode::Up | KeyCode::BackTab => ctx.dispatch(AppCommand::OptionsPrevFocus), KeyCode::Left | KeyCode::Right => { cycle_option_value(ctx, key.code == KeyCode::Right); } KeyCode::Char(' ') if !ctx.app.plugin_mode => { ctx.dispatch(AppCommand::TogglePlaying); ctx.playing .store(ctx.app.playback.playing, Ordering::Relaxed); } KeyCode::Char('s') => super::open_save(ctx), KeyCode::Char('l') => super::open_load(ctx), KeyCode::Char('?') => { ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 })); } _ => {} } InputResult::Continue }