Feat: comfort features
This commit is contained in:
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -165,8 +165,11 @@ jobs:
|
||||
mkdir -p pkg-root/Applications pkg-root/usr/local/bin
|
||||
cp -R Cagire.app pkg-root/Applications/
|
||||
cp cagire pkg-root/usr/local/bin/
|
||||
pkgbuild --analyze --root pkg-root component.plist
|
||||
plutil -replace BundleIsRelocatable -bool NO component.plist
|
||||
pkgbuild --root pkg-root --identifier com.sova.cagire \
|
||||
--version "$VERSION" --install-location / \
|
||||
--component-plist component.plist \
|
||||
"Cagire-${VERSION}-universal.pkg"
|
||||
|
||||
- name: Upload universal CLI
|
||||
|
||||
@@ -4,7 +4,12 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.0.8] - 2026-02-07
|
||||
|
||||
### Fixed
|
||||
- macOS `.pkg` installer bundle relocation: disabled `BundleIsRelocatable` so `Cagire.app` always installs to `/Applications/` instead of being redirected to an existing bundle location.
|
||||
|
||||
### Added
|
||||
- Syntax highlighting for user-defined Forth words: words created with `: name ... ;` now render with a distinct color in both the editor and step preview, instead of dimmed gray.
|
||||
- Multi-selection in Patterns view: Shift+Up/Down selects adjacent ranges of banks or patterns using anchor-based selection. Works with copy/paste (Ctrl+C/V), reset (Delete), toggle play (`p`), mute (`m`), and solo (`x`). Selection is column-scoped and clears on plain arrows, column switch, or Esc. Single-only actions (rename, pattern props, enter) are disabled during multi-selection.
|
||||
- Audio-rate modulation DSL: LFO words (`lfo`, `tlfo`, `wlfo`, `qlfo` for sine/triangle/sawtooth/square), transition envelopes (`slide`, `expslide`, `sslide` for linear/exponential/smooth), random modulation (`jit`, `sjit`, `drunk` for random hold/smooth random/drunk walk), and multi-segment envelope (`env`). These produce modulation strings consumed by parameter words for continuous audio-rate control.
|
||||
- Feedback delay FX words: `feedback`/`fb` (level), `fbtime`/`fbt` (delay time), `fbdamp`/`fbd` (damping), `fblfo` (LFO rate), `fblfodepth` (LFO depth), `fblfoshape` (LFO shape).
|
||||
- `bounce` word: ping-pong cycle through n items by step runs (e.g., `60 64 67 72 4 bounce` → 60 64 67 72 67 64 60 64...).
|
||||
|
||||
@@ -151,6 +151,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (pink, Color::Rgb(245, 220, 240)),
|
||||
vary: (yellow, Color::Rgb(245, 235, 210)),
|
||||
generator: (teal, Color::Rgb(210, 240, 235)),
|
||||
user_defined: (maroon, Color::Rgb(245, 225, 230)),
|
||||
default: (subtext0, mantle),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -151,6 +151,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (pink, Color::Rgb(55, 40, 55)),
|
||||
vary: (yellow, Color::Rgb(55, 50, 35)),
|
||||
generator: (teal, Color::Rgb(35, 55, 50)),
|
||||
user_defined: (maroon, Color::Rgb(55, 35, 40)),
|
||||
default: (subtext0, mantle),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -145,6 +145,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (pink, Color::Rgb(80, 50, 65)),
|
||||
vary: (yellow, Color::Rgb(70, 70, 45)),
|
||||
generator: (cyan, Color::Rgb(45, 70, 65)),
|
||||
user_defined: (orange, Color::Rgb(65, 55, 40)),
|
||||
default: (comment, darker_bg),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -148,6 +148,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (purple, Color::Rgb(26, 22, 28)),
|
||||
vary: (yellow, Color::Rgb(30, 30, 12)),
|
||||
generator: (cyan, Color::Rgb(16, 30, 26)),
|
||||
user_defined: (orange, Color::Rgb(30, 22, 10)),
|
||||
default: (fg_dim, bg),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -146,6 +146,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (purple, Color::Rgb(32, 24, 28)),
|
||||
vary: (yellow, Color::Rgb(40, 30, 12)),
|
||||
generator: (cyan, Color::Rgb(22, 32, 30)),
|
||||
user_defined: (purple, Color::Rgb(28, 20, 26)),
|
||||
default: (fg_dim, bg),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -146,6 +146,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (lavender, Color::Rgb(85, 75, 110)),
|
||||
vary: (yellow, Color::Rgb(100, 95, 60)),
|
||||
generator: (mint, Color::Rgb(70, 95, 95)),
|
||||
user_defined: (coral, Color::Rgb(100, 75, 75)),
|
||||
default: (fg_dim, dark),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -150,6 +150,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (violet, Color::Rgb(24, 8, 24)),
|
||||
vary: (yellow, Color::Rgb(30, 30, 14)),
|
||||
generator: (cyan, Color::Rgb(20, 30, 28)),
|
||||
user_defined: (orange, Color::Rgb(30, 20, 10)),
|
||||
default: (fg_dim, bg),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -147,6 +147,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (purple, Color::Rgb(65, 50, 55)),
|
||||
vary: (yellow, Color::Rgb(70, 65, 40)),
|
||||
generator: (aqua, Color::Rgb(45, 60, 50)),
|
||||
user_defined: (orange, Color::Rgb(60, 45, 30)),
|
||||
default: (fg3, darker_bg),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -142,6 +142,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (white, muted_red),
|
||||
vary: (gold, muted_red),
|
||||
generator: (yellow, muted_red),
|
||||
user_defined: (gold, Color::Rgb(100, 70, 0)),
|
||||
default: (light_yellow, darker_red),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -147,6 +147,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (autumn_green, Color::Rgb(45, 55, 45)),
|
||||
vary: (carp_yellow, Color::Rgb(65, 60, 50)),
|
||||
generator: (spring_blue, Color::Rgb(45, 55, 65)),
|
||||
user_defined: (sakura_pink, Color::Rgb(55, 40, 50)),
|
||||
default: (fg_dim, darker_bg),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -147,6 +147,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (keyword, Color::Rgb(240, 225, 240)),
|
||||
vary: (Color::Rgb(180, 140, 20), Color::Rgb(250, 240, 210)),
|
||||
generator: (function, Color::Rgb(215, 240, 235)),
|
||||
user_defined: (preproc, Color::Rgb(240, 230, 215)),
|
||||
default: (text_dim, off_white),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -227,6 +227,7 @@ pub struct SyntaxColors {
|
||||
pub variable: (Color, Color),
|
||||
pub vary: (Color, Color),
|
||||
pub generator: (Color, Color),
|
||||
pub user_defined: (Color, Color),
|
||||
pub default: (Color, Color),
|
||||
}
|
||||
|
||||
|
||||
@@ -144,6 +144,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (medium, Color::Rgb(30, 30, 30)),
|
||||
vary: (dim, Color::Rgb(25, 25, 25)),
|
||||
generator: (bright, Color::Rgb(45, 45, 45)),
|
||||
user_defined: (medium, Color::Rgb(35, 35, 35)),
|
||||
default: (fg_dim, bg),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -144,6 +144,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (medium, Color::Rgb(230, 230, 230)),
|
||||
vary: (dim, Color::Rgb(235, 235, 235)),
|
||||
generator: (dark, Color::Rgb(215, 215, 215)),
|
||||
user_defined: (medium, Color::Rgb(225, 225, 225)),
|
||||
default: (fg_dim, bg),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -145,6 +145,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (green, Color::Rgb(55, 75, 45)),
|
||||
vary: (yellow, Color::Rgb(70, 65, 45)),
|
||||
generator: (blue, Color::Rgb(50, 70, 70)),
|
||||
user_defined: (orange, Color::Rgb(60, 50, 30)),
|
||||
default: (fg_dim, darker_bg),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -145,6 +145,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (aurora_purple, Color::Rgb(60, 50, 60)),
|
||||
vary: (aurora_yellow, Color::Rgb(65, 60, 45)),
|
||||
generator: (frost0, Color::Rgb(45, 60, 55)),
|
||||
user_defined: (aurora_orange, Color::Rgb(60, 50, 45)),
|
||||
default: (snow_storm0, polar_night1),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -146,6 +146,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (purple, Color::Rgb(40, 25, 50)),
|
||||
vary: (yellow, Color::Rgb(50, 45, 20)),
|
||||
generator: (cyan, Color::Rgb(20, 45, 40)),
|
||||
user_defined: (orange, Color::Rgb(40, 25, 10)),
|
||||
default: (fg_dim, bg),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -146,6 +146,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (pine, Color::Rgb(35, 50, 55)),
|
||||
vary: (subtle, Color::Rgb(60, 55, 55)),
|
||||
generator: (foam, Color::Rgb(40, 55, 60)),
|
||||
user_defined: (love, Color::Rgb(55, 35, 45)),
|
||||
default: (fg_dim, darker_bg),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -146,6 +146,7 @@ pub fn theme() -> ThemeColors {
|
||||
variable: (green, Color::Rgb(50, 60, 45)),
|
||||
vary: (yellow, Color::Rgb(70, 60, 45)),
|
||||
generator: (cyan, Color::Rgb(45, 60, 75)),
|
||||
user_defined: (orange, Color::Rgb(60, 50, 35)),
|
||||
default: (fg_dim, darker_bg),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
@@ -211,6 +211,7 @@ pub fn rotate_theme(theme: ThemeColors, degrees: f32) -> ThemeColors {
|
||||
variable: rotate_color_pair(theme.syntax.variable, degrees),
|
||||
vary: rotate_color_pair(theme.syntax.vary, degrees),
|
||||
generator: rotate_color_pair(theme.syntax.generator, degrees),
|
||||
user_defined: rotate_color_pair(theme.syntax.user_defined, degrees),
|
||||
default: rotate_color_pair(theme.syntax.default, degrees),
|
||||
},
|
||||
table: TableColors {
|
||||
|
||||
160
src/app.rs
160
src/app.rs
@@ -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 } => {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
180
src/input.rs
180
src/input.rs
@@ -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,36 +1296,78 @@ 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 => {
|
||||
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 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('v') if ctrl => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
match ctx.app.patterns_nav.column {
|
||||
PatternsColumn::Banks => {
|
||||
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;
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Delete | KeyCode::Backspace => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
match ctx.app.patterns_nav.column {
|
||||
PatternsColumn::Banks => {
|
||||
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 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,
|
||||
@@ -1223,7 +1377,9 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('r') => {
|
||||
if !ctx.app.patterns_nav.has_selection() {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
match ctx.app.patterns_nav.column {
|
||||
PatternsColumn::Banks => {
|
||||
@@ -1238,7 +1394,8 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
}
|
||||
PatternsColumn::Patterns => {
|
||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||
let current_name = ctx.app.project_state.project.banks[bank].patterns[pattern]
|
||||
let current_name = ctx.app.project_state.project.banks[bank].patterns
|
||||
[pattern]
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
@@ -1250,8 +1407,11 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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,14 +1419,16 @@ 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;
|
||||
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;
|
||||
for pattern in ctx.app.patterns_nav.selected_patterns() {
|
||||
ctx.dispatch(AppCommand::StageSolo { bank, pattern });
|
||||
}
|
||||
}
|
||||
KeyCode::Char('M') => {
|
||||
ctx.dispatch(AppCommand::ClearMutes);
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user