Feat: comfort features

This commit is contained in:
2026-02-08 00:46:56 +01:00
parent 20c32ce0d8
commit 8ffe2c22c7
31 changed files with 578 additions and 72 deletions

View File

@@ -58,8 +58,8 @@ pub struct App {
pub rng: Rng,
pub live_keys: Arc<LiveKeyState>,
pub clipboard: Option<arboard::Clipboard>,
pub copied_pattern: Option<Pattern>,
pub copied_bank: Option<Bank>,
pub copied_patterns: Option<Vec<Pattern>>,
pub copied_banks: Option<Vec<Bank>>,
pub audio: AudioSettings,
pub options: OptionsState,
@@ -100,8 +100,8 @@ impl App {
live_keys,
script_engine,
clipboard: arboard::Clipboard::new().ok(),
copied_pattern: None,
copied_bank: None,
copied_patterns: None,
copied_banks: None,
audio: AudioSettings::default(),
options: OptionsState::default(),
@@ -682,12 +682,17 @@ impl App {
}
pub fn copy_pattern(&mut self, bank: usize, pattern: usize) {
self.copied_pattern = Some(clipboard::copy_pattern(&self.project_state.project, bank, pattern));
self.copied_patterns = Some(vec![clipboard::copy_pattern(
&self.project_state.project,
bank,
pattern,
)]);
self.ui.flash("Pattern copied", 150, FlashKind::Success);
}
pub fn paste_pattern(&mut self, bank: usize, pattern: usize) {
if let Some(src) = self.copied_pattern.clone() {
if let Some(src) = self.copied_patterns.as_ref().and_then(|v| v.first()) {
let src = src.clone();
clipboard::paste_pattern(&mut self.project_state.project, bank, pattern, &src);
self.project_state.mark_dirty(bank, pattern);
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
@@ -697,14 +702,55 @@ impl App {
}
}
pub fn copy_patterns(&mut self, bank: usize, patterns: &[usize]) {
self.copied_patterns = Some(clipboard::copy_patterns(
&self.project_state.project,
bank,
patterns,
));
let n = patterns.len();
self.ui.flash(
&format!("{n} pattern{} copied", if n == 1 { "" } else { "s" }),
150,
FlashKind::Success,
);
}
pub fn paste_patterns(&mut self, bank: usize, start: usize) {
if let Some(sources) = self.copied_patterns.clone() {
let count = clipboard::paste_patterns(
&mut self.project_state.project,
bank,
start,
&sources,
);
for i in 0..count {
self.project_state.mark_dirty(bank, start + i);
}
if self.editor_ctx.bank == bank {
self.load_step_to_editor();
}
self.ui.flash(
&format!("{count} pattern{} pasted", if count == 1 { "" } else { "s" }),
150,
FlashKind::Success,
);
}
}
pub fn copy_bank(&mut self, bank: usize) {
self.copied_bank = Some(clipboard::copy_bank(&self.project_state.project, bank));
self.copied_banks = Some(vec![clipboard::copy_bank(
&self.project_state.project,
bank,
)]);
self.ui.flash("Bank copied", 150, FlashKind::Success);
}
pub fn paste_bank(&mut self, bank: usize) {
if let Some(src) = self.copied_bank.clone() {
let pat_count = clipboard::paste_bank(&mut self.project_state.project, bank, &src);
if let Some(src) = self.copied_banks.as_ref().and_then(|v| v.first()) {
let src = src.clone();
let pat_count =
clipboard::paste_bank(&mut self.project_state.project, bank, &src);
for pattern in 0..pat_count {
self.project_state.mark_dirty(bank, pattern);
}
@@ -715,6 +761,79 @@ impl App {
}
}
pub fn copy_banks(&mut self, banks: &[usize]) {
self.copied_banks = Some(clipboard::copy_banks(
&self.project_state.project,
banks,
));
let n = banks.len();
self.ui.flash(
&format!("{n} bank{} copied", if n == 1 { "" } else { "s" }),
150,
FlashKind::Success,
);
}
pub fn paste_banks(&mut self, start: usize) {
if let Some(sources) = self.copied_banks.clone() {
let count = clipboard::paste_banks(
&mut self.project_state.project,
start,
&sources,
);
for i in 0..count {
let bank = start + i;
for pattern in 0..model::MAX_PATTERNS {
self.project_state.mark_dirty(bank, pattern);
}
}
if (start..start + count).contains(&self.editor_ctx.bank) {
self.load_step_to_editor();
}
self.ui.flash(
&format!("{count} bank{} pasted", if count == 1 { "" } else { "s" }),
150,
FlashKind::Success,
);
}
}
pub fn reset_patterns(&mut self, bank: usize, patterns: &[usize]) {
for &pattern in patterns {
let edit =
pattern_editor::reset_pattern(&mut self.project_state.project, bank, pattern);
self.project_state.mark_dirty(edit.bank, edit.pattern);
}
if self.editor_ctx.bank == bank && patterns.contains(&self.editor_ctx.pattern) {
self.load_step_to_editor();
}
let n = patterns.len();
self.ui.flash(
&format!("{n} pattern{} reset", if n == 1 { "" } else { "s" }),
150,
FlashKind::Success,
);
}
pub fn reset_banks(&mut self, banks: &[usize]) {
for &bank in banks {
let pat_count =
pattern_editor::reset_bank(&mut self.project_state.project, bank);
for pattern in 0..pat_count {
self.project_state.mark_dirty(bank, pattern);
}
}
if banks.contains(&self.editor_ctx.bank) {
self.load_step_to_editor();
}
let n = banks.len();
self.ui.flash(
&format!("{n} bank{} reset", if n == 1 { "" } else { "s" }),
150,
FlashKind::Success,
);
}
pub fn harden_steps(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
let indices = self.selected_steps();
@@ -944,12 +1063,30 @@ impl App {
AppCommand::PastePattern { bank, pattern } => {
self.paste_pattern(bank, pattern);
}
AppCommand::CopyPatterns { bank, patterns } => {
self.copy_patterns(bank, &patterns);
}
AppCommand::PastePatterns { bank, start } => {
self.paste_patterns(bank, start);
}
AppCommand::CopyBank { bank } => {
self.copy_bank(bank);
}
AppCommand::PasteBank { bank } => {
self.paste_bank(bank);
}
AppCommand::CopyBanks { banks } => {
self.copy_banks(&banks);
}
AppCommand::PasteBanks { start } => {
self.paste_banks(start);
}
AppCommand::ResetPatterns { bank, patterns } => {
self.reset_patterns(bank, &patterns);
}
AppCommand::ResetBanks { banks } => {
self.reset_banks(&banks);
}
// Clipboard
AppCommand::HardenSteps => self.harden_steps(),
@@ -1090,11 +1227,6 @@ impl App {
AppCommand::PatternsBack => {
self.page.down();
}
AppCommand::PatternsTogglePlay => {
let bank = self.patterns_nav.selected_bank();
let pattern = self.patterns_nav.selected_pattern();
self.stage_pattern_toggle(bank, pattern, snapshot);
}
// Mute/Solo (staged)
AppCommand::StageMute { bank, pattern } => {

View File

@@ -60,12 +60,33 @@ pub enum AppCommand {
bank: usize,
pattern: usize,
},
CopyPatterns {
bank: usize,
patterns: Vec<usize>,
},
PastePatterns {
bank: usize,
start: usize,
},
CopyBank {
bank: usize,
},
PasteBank {
bank: usize,
},
CopyBanks {
banks: Vec<usize>,
},
PasteBanks {
start: usize,
},
ResetPatterns {
bank: usize,
patterns: Vec<usize>,
},
ResetBanks {
banks: Vec<usize>,
},
// Clipboard
HardenSteps,
@@ -153,7 +174,6 @@ pub enum AppCommand {
PatternsCursorDown,
PatternsEnter,
PatternsBack,
PatternsTogglePlay,
// Mute/Solo (staged)
StageMute { bank: usize, pattern: usize },

View File

@@ -754,6 +754,70 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
_ => {}
}
}
Modal::ConfirmResetPatterns {
bank,
patterns,
selected: _,
} => {
let (bank, patterns) = (*bank, patterns.clone());
match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
ctx.dispatch(AppCommand::ResetPatterns { bank, patterns });
ctx.dispatch(AppCommand::CloseModal);
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
ctx.dispatch(AppCommand::CloseModal);
}
KeyCode::Left | KeyCode::Right => {
if let Modal::ConfirmResetPatterns { selected, .. } = &mut ctx.app.ui.modal {
*selected = !*selected;
}
}
KeyCode::Enter => {
let do_reset =
if let Modal::ConfirmResetPatterns { selected, .. } = &ctx.app.ui.modal {
*selected
} else {
false
};
if do_reset {
ctx.dispatch(AppCommand::ResetPatterns { bank, patterns });
}
ctx.dispatch(AppCommand::CloseModal);
}
_ => {}
}
}
Modal::ConfirmResetBanks { banks, selected: _ } => {
let banks = banks.clone();
match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
ctx.dispatch(AppCommand::ResetBanks { banks });
ctx.dispatch(AppCommand::CloseModal);
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
ctx.dispatch(AppCommand::CloseModal);
}
KeyCode::Left | KeyCode::Right => {
if let Modal::ConfirmResetBanks { selected, .. } = &mut ctx.app.ui.modal {
*selected = !*selected;
}
}
KeyCode::Enter => {
let do_reset =
if let Modal::ConfirmResetBanks { selected, .. } = &ctx.app.ui.modal {
*selected
} else {
false
};
if do_reset {
ctx.dispatch(AppCommand::ResetBanks { banks });
}
ctx.dispatch(AppCommand::CloseModal);
}
_ => {}
}
}
Modal::None => unreachable!(),
}
InputResult::Continue
@@ -1142,14 +1206,55 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
use crate::state::PatternsColumn;
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
match key.code {
KeyCode::Up if shift => {
match ctx.app.patterns_nav.column {
PatternsColumn::Banks => {
if ctx.app.patterns_nav.bank_anchor.is_none() {
ctx.app.patterns_nav.bank_anchor = Some(ctx.app.patterns_nav.bank_cursor);
}
}
PatternsColumn::Patterns => {
if ctx.app.patterns_nav.pattern_anchor.is_none() {
ctx.app.patterns_nav.pattern_anchor =
Some(ctx.app.patterns_nav.pattern_cursor);
}
}
}
ctx.app.patterns_nav.move_up_clamped();
}
KeyCode::Down if shift => {
match ctx.app.patterns_nav.column {
PatternsColumn::Banks => {
if ctx.app.patterns_nav.bank_anchor.is_none() {
ctx.app.patterns_nav.bank_anchor = Some(ctx.app.patterns_nav.bank_cursor);
}
}
PatternsColumn::Patterns => {
if ctx.app.patterns_nav.pattern_anchor.is_none() {
ctx.app.patterns_nav.pattern_anchor =
Some(ctx.app.patterns_nav.pattern_cursor);
}
}
}
ctx.app.patterns_nav.move_down_clamped();
}
KeyCode::Up => {
ctx.app.patterns_nav.clear_selection();
ctx.dispatch(AppCommand::PatternsCursorUp);
}
KeyCode::Down => {
ctx.app.patterns_nav.clear_selection();
ctx.dispatch(AppCommand::PatternsCursorDown);
}
KeyCode::Left => ctx.dispatch(AppCommand::PatternsCursorLeft),
KeyCode::Right => ctx.dispatch(AppCommand::PatternsCursorRight),
KeyCode::Up => ctx.dispatch(AppCommand::PatternsCursorUp),
KeyCode::Down => ctx.dispatch(AppCommand::PatternsCursorDown),
KeyCode::Esc => {
if !ctx.app.playback.staged_changes.is_empty()
if ctx.app.patterns_nav.has_selection() {
ctx.app.patterns_nav.clear_selection();
} else if !ctx.app.playback.staged_changes.is_empty()
|| !ctx.app.playback.staged_mute_changes.is_empty()
|| !ctx.app.playback.staged_prop_changes.is_empty()
{
@@ -1158,10 +1263,17 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
ctx.dispatch(AppCommand::PatternsBack);
}
}
KeyCode::Enter => ctx.dispatch(AppCommand::PatternsEnter),
KeyCode::Enter => {
if !ctx.app.patterns_nav.has_selection() {
ctx.dispatch(AppCommand::PatternsEnter);
}
}
KeyCode::Char('p') => {
if ctx.app.patterns_nav.column == PatternsColumn::Patterns {
ctx.dispatch(AppCommand::PatternsTogglePlay);
let bank = ctx.app.patterns_nav.bank_cursor;
for pattern in ctx.app.patterns_nav.selected_patterns() {
ctx.app.stage_pattern_toggle(bank, pattern, ctx.snapshot);
}
}
}
KeyCode::Char(' ') => {
@@ -1184,11 +1296,21 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let bank = ctx.app.patterns_nav.bank_cursor;
match ctx.app.patterns_nav.column {
PatternsColumn::Banks => {
ctx.dispatch(AppCommand::CopyBank { bank });
let banks = ctx.app.patterns_nav.selected_banks();
if banks.len() > 1 {
ctx.dispatch(AppCommand::CopyBanks { banks });
} else {
ctx.dispatch(AppCommand::CopyBank { bank });
}
}
PatternsColumn::Patterns => {
let pattern = ctx.app.patterns_nav.pattern_cursor;
ctx.dispatch(AppCommand::CopyPattern { bank, pattern });
let patterns = ctx.app.patterns_nav.selected_patterns();
if patterns.len() > 1 {
ctx.dispatch(AppCommand::CopyPatterns { bank, patterns });
} else {
let pattern = ctx.app.patterns_nav.pattern_cursor;
ctx.dispatch(AppCommand::CopyPattern { bank, pattern });
}
}
}
}
@@ -1196,11 +1318,27 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let bank = ctx.app.patterns_nav.bank_cursor;
match ctx.app.patterns_nav.column {
PatternsColumn::Banks => {
ctx.dispatch(AppCommand::PasteBank { bank });
if ctx.app.copied_banks.as_ref().is_some_and(|v| v.len() > 1) {
ctx.dispatch(AppCommand::PasteBanks { start: bank });
} else {
ctx.dispatch(AppCommand::PasteBank { bank });
}
}
PatternsColumn::Patterns => {
let pattern = ctx.app.patterns_nav.pattern_cursor;
ctx.dispatch(AppCommand::PastePattern { bank, pattern });
if ctx
.app
.copied_patterns
.as_ref()
.is_some_and(|v| v.len() > 1)
{
ctx.dispatch(AppCommand::PastePatterns {
bank,
start: pattern,
});
} else {
ctx.dispatch(AppCommand::PastePattern { bank, pattern });
}
}
}
}
@@ -1208,50 +1346,72 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let bank = ctx.app.patterns_nav.bank_cursor;
match ctx.app.patterns_nav.column {
PatternsColumn::Banks => {
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmResetBank {
bank,
selected: false,
}));
let banks = ctx.app.patterns_nav.selected_banks();
if banks.len() > 1 {
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmResetBanks {
banks,
selected: false,
}));
} else {
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmResetBank {
bank,
selected: false,
}));
}
}
PatternsColumn::Patterns => {
let pattern = ctx.app.patterns_nav.pattern_cursor;
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmResetPattern {
bank,
pattern,
selected: false,
}));
let patterns = ctx.app.patterns_nav.selected_patterns();
if patterns.len() > 1 {
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmResetPatterns {
bank,
patterns,
selected: false,
}));
} else {
let pattern = ctx.app.patterns_nav.pattern_cursor;
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmResetPattern {
bank,
pattern,
selected: false,
}));
}
}
}
}
KeyCode::Char('r') => {
let bank = ctx.app.patterns_nav.bank_cursor;
match ctx.app.patterns_nav.column {
PatternsColumn::Banks => {
let current_name = ctx.app.project_state.project.banks[bank]
.name
.clone()
.unwrap_or_default();
ctx.dispatch(AppCommand::OpenModal(Modal::RenameBank {
bank,
name: current_name,
}));
}
PatternsColumn::Patterns => {
let pattern = ctx.app.patterns_nav.pattern_cursor;
let current_name = ctx.app.project_state.project.banks[bank].patterns[pattern]
.name
.clone()
.unwrap_or_default();
ctx.dispatch(AppCommand::OpenModal(Modal::RenamePattern {
bank,
pattern,
name: current_name,
}));
if !ctx.app.patterns_nav.has_selection() {
let bank = ctx.app.patterns_nav.bank_cursor;
match ctx.app.patterns_nav.column {
PatternsColumn::Banks => {
let current_name = ctx.app.project_state.project.banks[bank]
.name
.clone()
.unwrap_or_default();
ctx.dispatch(AppCommand::OpenModal(Modal::RenameBank {
bank,
name: current_name,
}));
}
PatternsColumn::Patterns => {
let pattern = ctx.app.patterns_nav.pattern_cursor;
let current_name = ctx.app.project_state.project.banks[bank].patterns
[pattern]
.name
.clone()
.unwrap_or_default();
ctx.dispatch(AppCommand::OpenModal(Modal::RenamePattern {
bank,
pattern,
name: current_name,
}));
}
}
}
}
KeyCode::Char('e') if !ctrl => {
if ctx.app.patterns_nav.column == PatternsColumn::Patterns {
if ctx.app.patterns_nav.column == PatternsColumn::Patterns
&& !ctx.app.patterns_nav.has_selection()
{
let bank = ctx.app.patterns_nav.bank_cursor;
let pattern = ctx.app.patterns_nav.pattern_cursor;
ctx.dispatch(AppCommand::OpenPatternPropsModal { bank, pattern });
@@ -1259,13 +1419,15 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
}
KeyCode::Char('m') => {
let bank = ctx.app.patterns_nav.bank_cursor;
let pattern = ctx.app.patterns_nav.pattern_cursor;
ctx.dispatch(AppCommand::StageMute { bank, pattern });
for pattern in ctx.app.patterns_nav.selected_patterns() {
ctx.dispatch(AppCommand::StageMute { bank, pattern });
}
}
KeyCode::Char('x') => {
let bank = ctx.app.patterns_nav.bank_cursor;
let pattern = ctx.app.patterns_nav.pattern_cursor;
ctx.dispatch(AppCommand::StageSolo { bank, pattern });
for pattern in ctx.app.patterns_nav.selected_patterns() {
ctx.dispatch(AppCommand::StageSolo { bank, pattern });
}
}
KeyCode::Char('M') => {
ctx.dispatch(AppCommand::ClearMutes);

View File

@@ -24,6 +24,33 @@ pub fn paste_pattern(
project.banks[bank].patterns[pattern] = pat;
}
pub fn copy_patterns(project: &Project, bank: usize, indices: &[usize]) -> Vec<Pattern> {
indices
.iter()
.map(|&i| project.banks[bank].patterns[i].clone())
.collect()
}
pub fn paste_patterns(
project: &mut Project,
bank: usize,
start: usize,
sources: &[Pattern],
) -> usize {
let mut count = 0;
for (i, src) in sources.iter().enumerate() {
let target = start + i;
if target >= crate::model::MAX_PATTERNS {
break;
}
let mut pat = src.clone();
pat.name = annotate_copy_name(&src.name);
project.banks[bank].patterns[target] = pat;
count += 1;
}
count
}
pub fn copy_bank(project: &Project, bank: usize) -> Bank {
project.banks[bank].clone()
}
@@ -35,6 +62,28 @@ pub fn paste_bank(project: &mut Project, bank: usize, source: &Bank) -> usize {
project.banks[bank].patterns.len()
}
pub fn copy_banks(project: &Project, indices: &[usize]) -> Vec<Bank> {
indices
.iter()
.map(|&i| project.banks[i].clone())
.collect()
}
pub fn paste_banks(project: &mut Project, start: usize, sources: &[Bank]) -> usize {
let mut count = 0;
for (i, src) in sources.iter().enumerate() {
let target = start + i;
if target >= crate::model::MAX_BANKS {
break;
}
let mut b = src.clone();
b.name = annotate_copy_name(&src.name);
project.banks[target] = b;
count += 1;
}
count
}
pub fn copy_steps(
project: &Project,
bank: usize,

View File

@@ -29,6 +29,15 @@ pub enum Modal {
bank: usize,
selected: bool,
},
ConfirmResetPatterns {
bank: usize,
patterns: Vec<usize>,
selected: bool,
},
ConfirmResetBanks {
banks: Vec<usize>,
selected: bool,
},
FileBrowser(Box<FileBrowserState>),
RenameBank {
bank: usize,

View File

@@ -1,3 +1,5 @@
use std::ops::RangeInclusive;
use crate::model::{MAX_BANKS, MAX_PATTERNS};
#[derive(Clone, Copy, PartialEq, Eq, Default)]
@@ -12,14 +14,18 @@ pub struct PatternsNav {
pub column: PatternsColumn,
pub bank_cursor: usize,
pub pattern_cursor: usize,
pub bank_anchor: Option<usize>,
pub pattern_anchor: Option<usize>,
}
impl PatternsNav {
pub fn move_left(&mut self) {
self.clear_selection();
self.column = PatternsColumn::Banks;
}
pub fn move_right(&mut self) {
self.clear_selection();
self.column = PatternsColumn::Patterns;
}
@@ -45,6 +51,28 @@ impl PatternsNav {
}
}
pub fn move_up_clamped(&mut self) {
match self.column {
PatternsColumn::Banks => {
self.bank_cursor = self.bank_cursor.saturating_sub(1);
}
PatternsColumn::Patterns => {
self.pattern_cursor = self.pattern_cursor.saturating_sub(1);
}
}
}
pub fn move_down_clamped(&mut self) {
match self.column {
PatternsColumn::Banks => {
self.bank_cursor = (self.bank_cursor + 1).min(MAX_BANKS - 1);
}
PatternsColumn::Patterns => {
self.pattern_cursor = (self.pattern_cursor + 1).min(MAX_PATTERNS - 1);
}
}
}
pub fn selected_bank(&self) -> usize {
self.bank_cursor
}
@@ -52,4 +80,44 @@ impl PatternsNav {
pub fn selected_pattern(&self) -> usize {
self.pattern_cursor
}
pub fn bank_selection_range(&self) -> Option<RangeInclusive<usize>> {
let anchor = self.bank_anchor?;
let a = anchor.min(self.bank_cursor);
let b = anchor.max(self.bank_cursor);
Some(a..=b)
}
pub fn pattern_selection_range(&self) -> Option<RangeInclusive<usize>> {
let anchor = self.pattern_anchor?;
let a = anchor.min(self.pattern_cursor);
let b = anchor.max(self.pattern_cursor);
Some(a..=b)
}
pub fn selected_banks(&self) -> Vec<usize> {
match self.bank_selection_range() {
Some(range) => range.collect(),
None => vec![self.bank_cursor],
}
}
pub fn selected_patterns(&self) -> Vec<usize> {
match self.pattern_selection_range() {
Some(range) => range.collect(),
None => vec![self.pattern_cursor],
}
}
pub fn clear_selection(&mut self) {
self.bank_anchor = None;
self.pattern_anchor = None;
}
pub fn has_selection(&self) -> bool {
match self.column {
PatternsColumn::Banks => self.bank_anchor.is_some(),
PatternsColumn::Patterns => self.pattern_anchor.is_some(),
}
}
}

View File

@@ -1,8 +1,13 @@
use std::collections::HashSet;
use std::sync::LazyLock;
use ratatui::style::{Modifier, Style};
use crate::model::{lookup_word, SourceSpan, WordCompile};
use crate::theme;
static EMPTY_SET: LazyLock<HashSet<String>> = LazyLock::new(HashSet::new);
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum TokenKind {
Number,
@@ -20,6 +25,7 @@ pub enum TokenKind {
Emit,
Vary,
Generator,
UserDefined,
Default,
}
@@ -42,6 +48,7 @@ impl TokenKind {
TokenKind::Variable => theme.syntax.variable,
TokenKind::Vary => theme.syntax.vary,
TokenKind::Generator => theme.syntax.generator,
TokenKind::UserDefined => theme.syntax.user_defined,
TokenKind::Default => theme.syntax.default,
};
let style = Style::default().fg(fg).bg(bg);
@@ -114,7 +121,7 @@ const INTERVALS: &[&str] = &[
"M14", "P15",
];
pub fn tokenize_line(line: &str) -> Vec<Token> {
pub fn tokenize_line(line: &str, user_words: &HashSet<String>) -> Vec<Token> {
let mut tokens = Vec::new();
let mut chars = line.char_indices().peekable();
@@ -160,14 +167,14 @@ pub fn tokenize_line(line: &str) -> Vec<Token> {
}
let word = &line[start..end];
let (kind, varargs) = classify_word(word);
let (kind, varargs) = classify_word(word, user_words);
tokens.push(Token { start, end, kind, varargs });
}
tokens
}
fn classify_word(word: &str) -> (TokenKind, bool) {
fn classify_word(word: &str, user_words: &HashSet<String>) -> (TokenKind, bool) {
if word.parse::<f64>().is_ok() || word.parse::<i64>().is_ok() {
return (TokenKind::Number, false);
}
@@ -188,19 +195,24 @@ fn classify_word(word: &str) -> (TokenKind, bool) {
return (TokenKind::Variable, false);
}
if user_words.contains(word) {
return (TokenKind::UserDefined, false);
}
(TokenKind::Default, false)
}
pub fn highlight_line(line: &str) -> Vec<(Style, String)> {
highlight_line_with_runtime(line, &[], &[])
highlight_line_with_runtime(line, &[], &[], &EMPTY_SET)
}
pub fn highlight_line_with_runtime(
line: &str,
executed_spans: &[SourceSpan],
selected_spans: &[SourceSpan],
user_words: &HashSet<String>,
) -> Vec<(Style, String)> {
let tokens = tokenize_line(line);
let tokens = tokenize_line(line, user_words);
let mut result = Vec::new();
let mut last_end = 0;
let gap_style = TokenKind::gap_style();

View File

@@ -90,6 +90,11 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
let is_edit = idx == app.editor_ctx.bank;
let is_playing = banks_with_playback.contains(&idx);
let is_staged = banks_with_staged.contains(&idx);
let is_in_range = is_focused
&& app
.patterns_nav
.bank_selection_range()
.is_some_and(|r| r.contains(&idx));
// Check if any pattern in this bank is muted/soloed (applied)
let has_muted = (0..MAX_PATTERNS).any(|p| app.mute.is_muted(idx, p));
@@ -102,6 +107,8 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
let (bg, fg, prefix) = if is_cursor {
(theme.selection.cursor, theme.selection.cursor_fg, "")
} else if is_in_range {
(theme.selection.in_range_bg, theme.selection.in_range_fg, "")
} else if is_playing {
if has_staged_mute_solo {
(theme.list.staged_play_bg, theme.list.staged_play_fg, ">*")
@@ -277,6 +284,11 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
let is_playing = playing_patterns.contains(&idx);
let is_staged_play = staged_to_play.contains(&idx);
let is_staged_stop = staged_to_stop.contains(&idx);
let is_in_range = is_focused
&& app
.patterns_nav
.pattern_selection_range()
.is_some_and(|r| r.contains(&idx));
// Current applied mute/solo state
let is_muted = app.mute.is_muted(bank, idx);
@@ -294,6 +306,8 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
let (bg, fg, prefix) = if is_cursor {
(theme.selection.cursor, theme.selection.cursor_fg, "")
} else if is_in_range {
(theme.selection.in_range_bg, theme.selection.in_range_fg, "")
} else if is_playing {
// Playing patterns
if is_staged_stop {

View File

@@ -1,3 +1,4 @@
use std::collections::HashSet;
use std::time::{Duration, Instant};
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
@@ -14,7 +15,7 @@ use crate::state::{
EditorTarget, EuclideanField, FlashKind, Modal, PanelFocus, PatternField, SidePanel,
};
use crate::theme;
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
use crate::views::highlight::{self, highlight_line_with_runtime};
use crate::widgets::{
ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal,
};
@@ -460,6 +461,7 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term: Rect) -> Option<Rect> {
let theme = theme::get();
let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
let inner = match &app.ui.modal {
Modal::None => return None,
Modal::ConfirmQuit { selected } => {
@@ -488,6 +490,16 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
ConfirmModal::new("Confirm", &format!("Reset bank {}?", bank + 1), *selected)
.render_centered(frame, term)
}
Modal::ConfirmResetPatterns {
patterns, selected, ..
} => {
let label = format!("Reset {} patterns?", patterns.len());
ConfirmModal::new("Confirm", &label, *selected).render_centered(frame, term)
}
Modal::ConfirmResetBanks { banks, selected } => {
let label = format!("Reset {} banks?", banks.len());
ConfirmModal::new("Confirm", &label, *selected).render_centered(frame, term)
}
Modal::FileBrowser(state) => {
use crate::state::file_browser::FileBrowserMode;
use crate::widgets::FileBrowserModal;
@@ -621,9 +633,9 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
line_start,
line_str.len(),
);
highlight_line_with_runtime(line_str, &exec, &sel)
highlight_line_with_runtime(line_str, &exec, &sel, &user_words)
} else {
highlight_line(line_str)
highlight_line_with_runtime(line_str, &[], &[], &user_words)
};
line_start += line_str.len() + 1;
let spans: Vec<Span> = tokens
@@ -700,7 +712,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
),
None => (Vec::new(), Vec::new()),
};
highlight::highlight_line_with_runtime(line, &exec, &sel)
highlight::highlight_line_with_runtime(line, &exec, &sel, &user_words)
};
let show_search = app.editor_ctx.editor.search_active()