Feat: UI / UX

This commit is contained in:
2026-02-16 01:22:40 +01:00
parent b23dd85d0f
commit af6732db1c
37 changed files with 1045 additions and 64 deletions

View File

@@ -158,6 +158,8 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
time: None,
});
}
KeyCode::Char('s') => super::open_save(ctx),
KeyCode::Char('l') => super::open_load(ctx),
KeyCode::Char('?') => {
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
}

View File

@@ -3,7 +3,7 @@ use std::sync::atomic::Ordering;
use super::{InputContext, InputResult};
use crate::commands::AppCommand;
use crate::state::{ConfirmAction, DictFocus, HelpFocus, Modal};
use crate::state::{ConfirmAction, DictFocus, FlashKind, HelpFocus, Modal};
pub(super) fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
@@ -26,7 +26,22 @@ pub(super) fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
KeyCode::Esc if !ctx.app.ui.help_search_query.is_empty() => {
ctx.dispatch(AppCommand::HelpClearSearch);
}
KeyCode::Esc if ctx.app.ui.help_focused_block.is_some() => {
ctx.app.ui.help_focused_block = None;
}
KeyCode::Tab => ctx.dispatch(AppCommand::HelpToggleFocus),
KeyCode::Char('n') if ctx.app.ui.help_focus == HelpFocus::Content => {
navigate_code_block(ctx, true);
}
KeyCode::Char('p') if ctx.app.ui.help_focus == HelpFocus::Content => {
navigate_code_block(ctx, false);
}
KeyCode::Enter
if ctx.app.ui.help_focus == HelpFocus::Content
&& ctx.app.ui.help_focused_block.is_some() =>
{
execute_focused_block(ctx);
}
KeyCode::Char('j') | KeyCode::Down if ctrl => {
ctx.dispatch(AppCommand::HelpNextTopic(5));
}
@@ -49,6 +64,8 @@ pub(super) fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
selected: false,
}));
}
KeyCode::Char('s') => super::open_save(ctx),
KeyCode::Char('l') => super::open_load(ctx),
KeyCode::Char('?') => {
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
}
@@ -62,6 +79,57 @@ pub(super) fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
InputResult::Continue
}
fn navigate_code_block(ctx: &mut InputContext, forward: bool) {
let cache = ctx.app.ui.help_parsed.borrow();
let Some(parsed) = cache[ctx.app.ui.help_topic].as_ref() else {
return;
};
let count = parsed.code_blocks.len();
if count == 0 {
return;
}
let next = match ctx.app.ui.help_focused_block {
Some(cur) if forward => (cur + 1) % count,
Some(0) if !forward => count - 1,
Some(cur) if !forward => cur - 1,
_ if forward => 0,
_ => count - 1,
};
let scroll_to = parsed.code_blocks[next].start_line.saturating_sub(2);
drop(cache);
ctx.app.ui.help_focused_block = Some(next);
*ctx.app.ui.help_scroll_mut() = scroll_to;
}
fn execute_focused_block(ctx: &mut InputContext) {
let source = {
let cache = ctx.app.ui.help_parsed.borrow();
let Some(parsed) = cache[ctx.app.ui.help_topic].as_ref() else {
return;
};
let idx = ctx.app.ui.help_focused_block.unwrap();
let Some(block) = parsed.code_blocks.get(idx) else {
return;
};
block.source.clone()
};
let cleaned: String = source
.lines()
.map(|l| l.split(" => ").next().unwrap_or(l))
.collect::<Vec<_>>()
.join("\n");
match ctx
.app
.execute_script_oneshot(&cleaned, ctx.link, ctx.audio_tx)
{
Ok(()) => ctx.app.ui.flash("Executed", 100, FlashKind::Info),
Err(e) => ctx
.app
.ui
.flash(&format!("Error: {e}"), 200, FlashKind::Error),
}
}
pub(super) fn handle_dict_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
@@ -100,6 +168,8 @@ pub(super) fn handle_dict_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
selected: false,
}));
}
KeyCode::Char('s') => super::open_save(ctx),
KeyCode::Char('l') => super::open_load(ctx),
KeyCode::Char('?') => {
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
}

View File

@@ -84,18 +84,7 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
ctx.dispatch(AppCommand::OpenModal(Modal::Editor));
}
KeyCode::Char('t') => ctx.dispatch(AppCommand::ToggleSteps),
KeyCode::Char('s') => {
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))));
}
KeyCode::Char('s') => super::open_save(ctx),
KeyCode::Char('z') if ctrl && !shift => {
ctx.dispatch(AppCommand::Undo);
}
@@ -115,25 +104,7 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
ctx.dispatch(AppCommand::DuplicateSteps);
}
KeyCode::Char('h') if ctrl => ctx.dispatch(AppCommand::HardenSteps),
KeyCode::Char('l') => {
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))));
}
KeyCode::Char('l') => super::open_load(ctx),
KeyCode::Char('+') | KeyCode::Char('=') => ctx.dispatch(AppCommand::TempoUp),
KeyCode::Char('-') => ctx.dispatch(AppCommand::TempoDown),
KeyCode::Char('T') => {

View File

@@ -147,6 +147,39 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
}
}
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() {

View File

@@ -525,6 +525,14 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
_ => {}
}
}
Modal::Onboarding => match key.code {
KeyCode::Enter => {
ctx.dispatch(AppCommand::DismissOnboarding);
ctx.dispatch(AppCommand::CloseModal);
ctx.app.save_settings(ctx.link);
}
_ => ctx.dispatch(AppCommand::CloseModal),
},
Modal::None => unreachable!(),
}
InputResult::Continue

View File

@@ -87,6 +87,9 @@ pub(crate) fn cycle_option_value(ctx: &mut InputContext, right: bool) {
}
}
}
OptionsFocus::ResetOnboarding => {
ctx.dispatch(AppCommand::ResetOnboarding);
}
OptionsFocus::MidiInput0
| OptionsFocus::MidiInput1
| OptionsFocus::MidiInput2
@@ -162,6 +165,8 @@ pub(super) fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> Inpu
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 }));
}

View File

@@ -249,6 +249,8 @@ pub(super) fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> Inp
ctx.dispatch(AppCommand::ClearSolos);
ctx.app.send_mute_state(ctx.seq_cmd_tx);
}
KeyCode::Char('s') => super::open_save(ctx),
KeyCode::Char('l') => super::open_load(ctx),
KeyCode::Char('?') => {
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
}