This commit is contained in:
2026-02-10 23:51:17 +01:00
parent 7d670dacb9
commit 8ba89f91a0
12 changed files with 2312 additions and 2280 deletions

167
src/input/mod.rs Normal file
View File

@@ -0,0 +1,167 @@
mod engine_page;
mod help_page;
mod main_page;
mod modal;
mod options_page;
mod panel;
mod patterns_page;
use arc_swap::ArcSwap;
use crossbeam_channel::Sender;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use std::sync::atomic::{AtomicBool, AtomicI64};
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::app::App;
use crate::commands::AppCommand;
use crate::engine::{AudioCommand, LinkState, SeqCommand, SequencerSnapshot};
use crate::page::Page;
use crate::state::{Modal, PanelFocus};
pub enum InputResult {
Continue,
Quit,
}
pub struct InputContext<'a> {
pub app: &'a mut App,
pub link: &'a LinkState,
pub snapshot: &'a SequencerSnapshot,
pub playing: &'a Arc<AtomicBool>,
pub audio_tx: &'a ArcSwap<Sender<AudioCommand>>,
pub seq_cmd_tx: &'a Sender<SeqCommand>,
pub nudge_us: &'a Arc<AtomicI64>,
}
impl<'a> InputContext<'a> {
fn dispatch(&mut self, cmd: AppCommand) {
self.app.dispatch(cmd, self.link, self.snapshot);
}
}
pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
if handle_live_keys(ctx, &key) {
return InputResult::Continue;
}
if key.kind == KeyEventKind::Release {
return InputResult::Continue;
}
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let is_arrow = matches!(
key.code,
KeyCode::Left | KeyCode::Right | KeyCode::Up | KeyCode::Down
);
if ctx.app.ui.minimap_until.is_some() && !(ctrl && is_arrow) {
ctx.dispatch(AppCommand::ClearMinimap);
}
if ctx.app.ui.show_title {
ctx.dispatch(AppCommand::HideTitle);
return InputResult::Continue;
}
ctx.dispatch(AppCommand::ClearStatus);
if matches!(ctx.app.ui.modal, Modal::None) {
handle_normal_input(ctx, key)
} else {
modal::handle_modal_input(ctx, key)
}
}
fn handle_live_keys(ctx: &mut InputContext, key: &KeyEvent) -> bool {
match (key.code, key.kind) {
_ if !matches!(ctx.app.ui.modal, Modal::None) => false,
(KeyCode::Char('f'), KeyEventKind::Press) => {
ctx.dispatch(AppCommand::ToggleLiveKeysFill);
true
}
_ => false,
}
}
fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
if ctx.app.panel.visible && ctx.app.panel.focus == PanelFocus::Side {
return panel::handle_panel_input(ctx, key);
}
if ctrl {
let minimap_timeout = Some(Instant::now() + Duration::from_millis(250));
match key.code {
KeyCode::Left => {
ctx.app.ui.minimap_until = minimap_timeout;
ctx.dispatch(AppCommand::PageLeft);
return InputResult::Continue;
}
KeyCode::Right => {
ctx.app.ui.minimap_until = minimap_timeout;
ctx.dispatch(AppCommand::PageRight);
return InputResult::Continue;
}
KeyCode::Up => {
ctx.app.ui.minimap_until = minimap_timeout;
ctx.dispatch(AppCommand::PageUp);
return InputResult::Continue;
}
KeyCode::Down => {
ctx.app.ui.minimap_until = minimap_timeout;
ctx.dispatch(AppCommand::PageDown);
return InputResult::Continue;
}
_ => {}
}
}
match ctx.app.page {
Page::Main => main_page::handle_main_page(ctx, key, ctrl),
Page::Patterns => patterns_page::handle_patterns_page(ctx, key),
Page::Engine => engine_page::handle_engine_page(ctx, key),
Page::Options => options_page::handle_options_page(ctx, key),
Page::Help => help_page::handle_help_page(ctx, key),
Page::Dict => help_page::handle_dict_page(ctx, key),
}
}
fn load_project_samples(ctx: &mut InputContext) {
let paths = ctx.app.project_state.project.sample_paths.clone();
if paths.is_empty() {
return;
}
let mut total_count = 0;
let mut all_preload_entries = Vec::new();
for path in &paths {
if path.is_dir() {
let index = doux::sampling::scan_samples_dir(path);
let count = index.len();
total_count += count;
for e in &index {
all_preload_entries.push((e.name.clone(), e.path.clone()));
}
let _ = ctx.audio_tx.load().send(AudioCommand::LoadSamples(index));
}
}
ctx.app.audio.config.sample_paths = paths;
ctx.app.audio.config.sample_count = total_count;
if total_count > 0 {
if let Some(registry) = ctx.app.audio.sample_registry.clone() {
let sr = ctx.app.audio.config.sample_rate;
std::thread::Builder::new()
.name("sample-preload".into())
.spawn(move || {
crate::init::preload_sample_heads(all_preload_entries, sr, &registry);
})
.expect("failed to spawn preload thread");
}
ctx.dispatch(AppCommand::SetStatus(format!(
"Loaded {total_count} samples from project"
)));
}
}