Files
Cagire/src/input/mod.rs
Raphaël Forment b728b38d6e
Some checks failed
Deploy Website / deploy (push) Failing after 29s
Feat: add hidden mode and new documentation
2026-02-26 12:31:56 +01:00

236 lines
7.4 KiB
Rust

//! Keyboard and mouse input handling — dispatches events to page-specific or modal handlers.
pub(crate) mod engine_page;
mod help_page;
mod main_page;
mod modal;
mod mouse;
pub(crate) mod options_page;
mod panel;
mod patterns_page;
mod script_page;
use arc_swap::ArcSwap;
use crossbeam_channel::Sender;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent};
use ratatui::layout::Rect;
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::{MinimapMode, 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_mouse(ctx: &mut InputContext, mouse: MouseEvent, term: Rect) {
mouse::handle_mouse(ctx, mouse, term);
}
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 !(matches!(ctx.app.ui.minimap, MinimapMode::Hidden) || ctrl && is_arrow) {
ctx.dispatch(AppCommand::ClearMinimap);
}
if ctx.app.ui.show_title {
ctx.dispatch(AppCommand::HideTitle);
if matches!(key.code, KeyCode::Char('q') | KeyCode::Esc) {
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,
_ if ctx.app.page == Page::Script && ctx.app.script_editor.focused => 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_timed = MinimapMode::Timed(Instant::now() + Duration::from_millis(250));
match key.code {
KeyCode::Left => {
ctx.app.ui.minimap = minimap_timed;
ctx.dispatch(AppCommand::PageLeft);
return InputResult::Continue;
}
KeyCode::Right => {
ctx.app.ui.minimap = minimap_timed;
ctx.dispatch(AppCommand::PageRight);
return InputResult::Continue;
}
KeyCode::Up => {
ctx.app.ui.minimap = minimap_timed;
ctx.dispatch(AppCommand::PageUp);
return InputResult::Continue;
}
KeyCode::Down => {
ctx.app.ui.minimap = minimap_timed;
ctx.dispatch(AppCommand::PageDown);
return InputResult::Continue;
}
_ => {}
}
}
if let Some(page) = match key.code {
KeyCode::F(1) => Some(Page::Dict),
KeyCode::F(2) => Some(Page::Patterns),
KeyCode::F(3) => Some(Page::Options),
KeyCode::F(4) => Some(Page::Help),
KeyCode::F(5) => Some(Page::Main),
KeyCode::F(6) => Some(Page::Engine),
KeyCode::F(7) => Some(Page::Script),
_ => None,
} {
ctx.app.ui.minimap = MinimapMode::Timed(Instant::now() + Duration::from_millis(250));
ctx.dispatch(AppCommand::GoToPage(page));
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),
Page::Script => script_page::handle_script_page(ctx, key),
}
}
fn open_save(ctx: &mut InputContext) {
use crate::state::file_browser::FileBrowserState;
let initial = ctx
.app
.project_state
.file_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_default();
let state = FileBrowserState::new_save(initial);
ctx.dispatch(AppCommand::OpenModal(Modal::FileBrowser(Box::new(state))));
}
fn open_load(ctx: &mut InputContext) {
use crate::state::file_browser::FileBrowserState;
let default_dir = ctx
.app
.project_state
.file_path
.as_ref()
.and_then(|p| p.parent())
.map(|p| {
let mut s = p.display().to_string();
if !s.ends_with('/') {
s.push('/');
}
s
})
.unwrap_or_default();
let state = FileBrowserState::new_load(default_dir);
ctx.dispatch(AppCommand::OpenModal(Modal::FileBrowser(Box::new(state))));
}
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;
for path in &ctx.app.audio.config.sample_paths {
if let Some(sf2_path) = doux::soundfont::find_sf2_file(path) {
let _ = ctx.audio_tx.load().send(AudioCommand::LoadSoundfont(sf2_path));
break;
}
}
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::engine::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"
)));
}
}