Feat: UI/UX fixes + removing clones from places

This commit is contained in:
2026-03-05 00:15:51 +01:00
parent 35370a6f2c
commit 60fb62829f
17 changed files with 1817 additions and 290 deletions

View File

@@ -79,9 +79,6 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
let current = format!("{:.1}", ctx.link.tempo());
ctx.dispatch(AppCommand::OpenModal(Modal::SetTempo(current)));
}
KeyCode::Char(':') => {
ctx.dispatch(AppCommand::OpenModal(Modal::JumpToStep(String::new())));
}
KeyCode::Char('<') | KeyCode::Char(',') => ctx.dispatch(AppCommand::LengthDecrease),
KeyCode::Char('>') | KeyCode::Char('.') => ctx.dispatch(AppCommand::LengthIncrease),
KeyCode::Char('[') => ctx.dispatch(AppCommand::SpeedDecrease),

View File

@@ -179,6 +179,19 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
return InputResult::Continue;
}
if key.code == KeyCode::Char(':') {
let in_search = ctx.app.ui.dict_search_active || ctx.app.ui.help_search_active;
let in_script = ctx.app.page == Page::Script && ctx.app.script_editor.focused;
if !in_search && !in_script {
ctx.dispatch(AppCommand::OpenModal(Modal::CommandPalette {
input: String::new(),
cursor: 0,
scroll: 0,
}));
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),

View File

@@ -10,6 +10,49 @@ use crate::state::{
};
pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
// Handle CommandPalette before the main match to avoid borrow conflicts
// (Enter needs &App for palette_entries while the match borrows &mut modal)
if let Modal::CommandPalette { input, cursor, scroll } = &mut ctx.app.ui.modal {
match key.code {
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
KeyCode::Char(c) => {
input.push(c);
*cursor = 0;
*scroll = 0;
}
KeyCode::Backspace => {
input.pop();
*cursor = 0;
*scroll = 0;
}
KeyCode::Up if key.modifiers.contains(KeyModifiers::CONTROL) => {
*cursor = cursor.saturating_sub(5);
}
KeyCode::Down if key.modifiers.contains(KeyModifiers::CONTROL) => {
*cursor += 5;
}
KeyCode::Up => {
*cursor = cursor.saturating_sub(1);
}
KeyCode::Down => {
*cursor += 1;
}
KeyCode::PageUp => {
*cursor = cursor.saturating_sub(10);
}
KeyCode::PageDown => {
*cursor += 10;
}
KeyCode::Enter => {
let query = input.clone();
let cursor_val = *cursor;
handle_palette_enter(ctx, &query, cursor_val);
}
_ => {}
}
return InputResult::Continue;
}
match &mut ctx.app.ui.modal {
Modal::Confirm { action, selected } => {
let confirmed = *selected;
@@ -179,22 +222,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
KeyCode::Char(c) => input.push(c),
_ => {}
},
Modal::JumpToStep(input) => match key.code {
KeyCode::Enter => {
if let Ok(step) = input.parse::<usize>() {
if step > 0 {
ctx.dispatch(AppCommand::GoToStep(step - 1));
}
}
ctx.dispatch(AppCommand::CloseModal);
}
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
KeyCode::Backspace => {
input.pop();
}
KeyCode::Char(c) if c.is_ascii_digit() => input.push(c),
_ => {}
},
Modal::SetTempo(input) => match key.code {
KeyCode::Enter => {
if let Ok(tempo) = input.parse::<f64>() {
@@ -514,7 +541,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
}
}
Modal::KeybindingsHelp { scroll } => {
let bindings_count = crate::views::keybindings::bindings_for(ctx.app.page, ctx.app.plugin_mode).len();
let bindings_count = crate::model::palette::bindings_for(ctx.app.page, ctx.app.plugin_mode).len();
match key.code {
KeyCode::Esc | KeyCode::Char('?') => ctx.dispatch(AppCommand::CloseModal),
KeyCode::Up | KeyCode::Char('k') => {
@@ -636,6 +663,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
_ => ctx.dispatch(AppCommand::CloseModal),
}
}
Modal::CommandPalette { .. } => unreachable!(),
Modal::None => unreachable!(),
}
InputResult::Continue
@@ -670,6 +698,97 @@ fn execute_confirm(ctx: &mut InputContext, action: &ConfirmAction) -> InputResul
InputResult::Continue
}
fn handle_palette_enter(ctx: &mut InputContext, query: &str, cursor: usize) {
let page = ctx.app.page;
let plugin_mode = ctx.app.plugin_mode;
// Numeric input on Main page → jump to step
if page == crate::page::Page::Main
&& !query.is_empty()
&& query.chars().all(|c| c.is_ascii_digit())
{
if let Ok(step) = query.parse::<usize>() {
if step > 0 {
ctx.dispatch(AppCommand::GoToStep(step - 1));
}
}
ctx.dispatch(AppCommand::CloseModal);
return;
}
let entries = crate::model::palette::palette_entries(query, plugin_mode, ctx.app);
if let Some(entry) = entries.get(cursor) {
ctx.dispatch(AppCommand::CloseModal);
execute_palette_entry(ctx, entry);
} else {
ctx.dispatch(AppCommand::CloseModal);
}
}
fn execute_palette_entry(
ctx: &mut InputContext,
entry: &crate::model::palette::CommandEntry,
) {
use crate::model::palette::PaletteAction;
use std::sync::atomic::Ordering;
match &entry.action {
Some(PaletteAction::Resolve(f)) => {
if let Some(cmd) = f(ctx.app) {
ctx.dispatch(cmd);
}
}
Some(PaletteAction::Save) => super::open_save(ctx),
Some(PaletteAction::Load) => super::open_load(ctx),
Some(PaletteAction::TogglePlaying) => {
ctx.dispatch(AppCommand::TogglePlaying);
ctx.playing
.store(ctx.app.playback.playing, Ordering::Relaxed);
}
Some(PaletteAction::MuteToggle) => {
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
ctx.app.playback.toggle_mute(bank, pattern);
ctx.app.send_mute_state(ctx.seq_cmd_tx);
}
Some(PaletteAction::SoloToggle) => {
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
ctx.app.playback.toggle_solo(bank, pattern);
ctx.app.send_mute_state(ctx.seq_cmd_tx);
}
Some(PaletteAction::ClearMutes) => {
ctx.dispatch(AppCommand::ClearMutes);
ctx.app.send_mute_state(ctx.seq_cmd_tx);
}
Some(PaletteAction::ClearSolos) => {
ctx.dispatch(AppCommand::ClearSolos);
ctx.app.send_mute_state(ctx.seq_cmd_tx);
}
Some(PaletteAction::Hush) => {
let _ = ctx
.audio_tx
.load()
.send(crate::engine::AudioCommand::Hush);
}
Some(PaletteAction::Panic) => {
let _ = ctx
.audio_tx
.load()
.send(crate::engine::AudioCommand::Panic);
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
}
Some(PaletteAction::TestTone) => {
let _ = ctx
.audio_tx
.load()
.send(crate::engine::AudioCommand::Evaluate {
cmd: "/sound/sine/dur/0.5/decay/0.2".into(),
time: None,
});
}
None => {}
}
}
fn rename_command(target: &RenameTarget, name: Option<String>) -> AppCommand {
match target {
RenameTarget::Bank { bank } => AppCommand::RenameBank { bank: *bank, name },

View File

@@ -986,7 +986,8 @@ fn handle_modal_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
Modal::FileBrowser(_) | Modal::AddSamplePath(_) => (60, 18),
Modal::Rename { .. } => (40, 5),
Modal::SetPattern { .. } | Modal::SetScript { .. } => (45, 5),
Modal::SetTempo(_) | Modal::JumpToStep(_) => (30, 5),
Modal::SetTempo(_) => (30, 5),
Modal::CommandPalette { .. } => (55, 20),
_ => return,
};
let modal_area = centered_rect(term, w, h);