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
|
mkdir -p pkg-root/Applications pkg-root/usr/local/bin
|
||||||
cp -R Cagire.app pkg-root/Applications/
|
cp -R Cagire.app pkg-root/Applications/
|
||||||
cp cagire pkg-root/usr/local/bin/
|
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 \
|
pkgbuild --root pkg-root --identifier com.sova.cagire \
|
||||||
--version "$VERSION" --install-location / \
|
--version "$VERSION" --install-location / \
|
||||||
|
--component-plist component.plist \
|
||||||
"Cagire-${VERSION}-universal.pkg"
|
"Cagire-${VERSION}-universal.pkg"
|
||||||
|
|
||||||
- name: Upload universal CLI
|
- 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
|
## [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
|
### 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.
|
- 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).
|
- 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...).
|
- `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)),
|
variable: (pink, Color::Rgb(245, 220, 240)),
|
||||||
vary: (yellow, Color::Rgb(245, 235, 210)),
|
vary: (yellow, Color::Rgb(245, 235, 210)),
|
||||||
generator: (teal, Color::Rgb(210, 240, 235)),
|
generator: (teal, Color::Rgb(210, 240, 235)),
|
||||||
|
user_defined: (maroon, Color::Rgb(245, 225, 230)),
|
||||||
default: (subtext0, mantle),
|
default: (subtext0, mantle),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (pink, Color::Rgb(55, 40, 55)),
|
variable: (pink, Color::Rgb(55, 40, 55)),
|
||||||
vary: (yellow, Color::Rgb(55, 50, 35)),
|
vary: (yellow, Color::Rgb(55, 50, 35)),
|
||||||
generator: (teal, Color::Rgb(35, 55, 50)),
|
generator: (teal, Color::Rgb(35, 55, 50)),
|
||||||
|
user_defined: (maroon, Color::Rgb(55, 35, 40)),
|
||||||
default: (subtext0, mantle),
|
default: (subtext0, mantle),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (pink, Color::Rgb(80, 50, 65)),
|
variable: (pink, Color::Rgb(80, 50, 65)),
|
||||||
vary: (yellow, Color::Rgb(70, 70, 45)),
|
vary: (yellow, Color::Rgb(70, 70, 45)),
|
||||||
generator: (cyan, Color::Rgb(45, 70, 65)),
|
generator: (cyan, Color::Rgb(45, 70, 65)),
|
||||||
|
user_defined: (orange, Color::Rgb(65, 55, 40)),
|
||||||
default: (comment, darker_bg),
|
default: (comment, darker_bg),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (purple, Color::Rgb(26, 22, 28)),
|
variable: (purple, Color::Rgb(26, 22, 28)),
|
||||||
vary: (yellow, Color::Rgb(30, 30, 12)),
|
vary: (yellow, Color::Rgb(30, 30, 12)),
|
||||||
generator: (cyan, Color::Rgb(16, 30, 26)),
|
generator: (cyan, Color::Rgb(16, 30, 26)),
|
||||||
|
user_defined: (orange, Color::Rgb(30, 22, 10)),
|
||||||
default: (fg_dim, bg),
|
default: (fg_dim, bg),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (purple, Color::Rgb(32, 24, 28)),
|
variable: (purple, Color::Rgb(32, 24, 28)),
|
||||||
vary: (yellow, Color::Rgb(40, 30, 12)),
|
vary: (yellow, Color::Rgb(40, 30, 12)),
|
||||||
generator: (cyan, Color::Rgb(22, 32, 30)),
|
generator: (cyan, Color::Rgb(22, 32, 30)),
|
||||||
|
user_defined: (purple, Color::Rgb(28, 20, 26)),
|
||||||
default: (fg_dim, bg),
|
default: (fg_dim, bg),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (lavender, Color::Rgb(85, 75, 110)),
|
variable: (lavender, Color::Rgb(85, 75, 110)),
|
||||||
vary: (yellow, Color::Rgb(100, 95, 60)),
|
vary: (yellow, Color::Rgb(100, 95, 60)),
|
||||||
generator: (mint, Color::Rgb(70, 95, 95)),
|
generator: (mint, Color::Rgb(70, 95, 95)),
|
||||||
|
user_defined: (coral, Color::Rgb(100, 75, 75)),
|
||||||
default: (fg_dim, dark),
|
default: (fg_dim, dark),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (violet, Color::Rgb(24, 8, 24)),
|
variable: (violet, Color::Rgb(24, 8, 24)),
|
||||||
vary: (yellow, Color::Rgb(30, 30, 14)),
|
vary: (yellow, Color::Rgb(30, 30, 14)),
|
||||||
generator: (cyan, Color::Rgb(20, 30, 28)),
|
generator: (cyan, Color::Rgb(20, 30, 28)),
|
||||||
|
user_defined: (orange, Color::Rgb(30, 20, 10)),
|
||||||
default: (fg_dim, bg),
|
default: (fg_dim, bg),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (purple, Color::Rgb(65, 50, 55)),
|
variable: (purple, Color::Rgb(65, 50, 55)),
|
||||||
vary: (yellow, Color::Rgb(70, 65, 40)),
|
vary: (yellow, Color::Rgb(70, 65, 40)),
|
||||||
generator: (aqua, Color::Rgb(45, 60, 50)),
|
generator: (aqua, Color::Rgb(45, 60, 50)),
|
||||||
|
user_defined: (orange, Color::Rgb(60, 45, 30)),
|
||||||
default: (fg3, darker_bg),
|
default: (fg3, darker_bg),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (white, muted_red),
|
variable: (white, muted_red),
|
||||||
vary: (gold, muted_red),
|
vary: (gold, muted_red),
|
||||||
generator: (yellow, muted_red),
|
generator: (yellow, muted_red),
|
||||||
|
user_defined: (gold, Color::Rgb(100, 70, 0)),
|
||||||
default: (light_yellow, darker_red),
|
default: (light_yellow, darker_red),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (autumn_green, Color::Rgb(45, 55, 45)),
|
variable: (autumn_green, Color::Rgb(45, 55, 45)),
|
||||||
vary: (carp_yellow, Color::Rgb(65, 60, 50)),
|
vary: (carp_yellow, Color::Rgb(65, 60, 50)),
|
||||||
generator: (spring_blue, Color::Rgb(45, 55, 65)),
|
generator: (spring_blue, Color::Rgb(45, 55, 65)),
|
||||||
|
user_defined: (sakura_pink, Color::Rgb(55, 40, 50)),
|
||||||
default: (fg_dim, darker_bg),
|
default: (fg_dim, darker_bg),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (keyword, Color::Rgb(240, 225, 240)),
|
variable: (keyword, Color::Rgb(240, 225, 240)),
|
||||||
vary: (Color::Rgb(180, 140, 20), Color::Rgb(250, 240, 210)),
|
vary: (Color::Rgb(180, 140, 20), Color::Rgb(250, 240, 210)),
|
||||||
generator: (function, Color::Rgb(215, 240, 235)),
|
generator: (function, Color::Rgb(215, 240, 235)),
|
||||||
|
user_defined: (preproc, Color::Rgb(240, 230, 215)),
|
||||||
default: (text_dim, off_white),
|
default: (text_dim, off_white),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ pub struct SyntaxColors {
|
|||||||
pub variable: (Color, Color),
|
pub variable: (Color, Color),
|
||||||
pub vary: (Color, Color),
|
pub vary: (Color, Color),
|
||||||
pub generator: (Color, Color),
|
pub generator: (Color, Color),
|
||||||
|
pub user_defined: (Color, Color),
|
||||||
pub default: (Color, Color),
|
pub default: (Color, Color),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (medium, Color::Rgb(30, 30, 30)),
|
variable: (medium, Color::Rgb(30, 30, 30)),
|
||||||
vary: (dim, Color::Rgb(25, 25, 25)),
|
vary: (dim, Color::Rgb(25, 25, 25)),
|
||||||
generator: (bright, Color::Rgb(45, 45, 45)),
|
generator: (bright, Color::Rgb(45, 45, 45)),
|
||||||
|
user_defined: (medium, Color::Rgb(35, 35, 35)),
|
||||||
default: (fg_dim, bg),
|
default: (fg_dim, bg),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (medium, Color::Rgb(230, 230, 230)),
|
variable: (medium, Color::Rgb(230, 230, 230)),
|
||||||
vary: (dim, Color::Rgb(235, 235, 235)),
|
vary: (dim, Color::Rgb(235, 235, 235)),
|
||||||
generator: (dark, Color::Rgb(215, 215, 215)),
|
generator: (dark, Color::Rgb(215, 215, 215)),
|
||||||
|
user_defined: (medium, Color::Rgb(225, 225, 225)),
|
||||||
default: (fg_dim, bg),
|
default: (fg_dim, bg),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (green, Color::Rgb(55, 75, 45)),
|
variable: (green, Color::Rgb(55, 75, 45)),
|
||||||
vary: (yellow, Color::Rgb(70, 65, 45)),
|
vary: (yellow, Color::Rgb(70, 65, 45)),
|
||||||
generator: (blue, Color::Rgb(50, 70, 70)),
|
generator: (blue, Color::Rgb(50, 70, 70)),
|
||||||
|
user_defined: (orange, Color::Rgb(60, 50, 30)),
|
||||||
default: (fg_dim, darker_bg),
|
default: (fg_dim, darker_bg),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (aurora_purple, Color::Rgb(60, 50, 60)),
|
variable: (aurora_purple, Color::Rgb(60, 50, 60)),
|
||||||
vary: (aurora_yellow, Color::Rgb(65, 60, 45)),
|
vary: (aurora_yellow, Color::Rgb(65, 60, 45)),
|
||||||
generator: (frost0, Color::Rgb(45, 60, 55)),
|
generator: (frost0, Color::Rgb(45, 60, 55)),
|
||||||
|
user_defined: (aurora_orange, Color::Rgb(60, 50, 45)),
|
||||||
default: (snow_storm0, polar_night1),
|
default: (snow_storm0, polar_night1),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (purple, Color::Rgb(40, 25, 50)),
|
variable: (purple, Color::Rgb(40, 25, 50)),
|
||||||
vary: (yellow, Color::Rgb(50, 45, 20)),
|
vary: (yellow, Color::Rgb(50, 45, 20)),
|
||||||
generator: (cyan, Color::Rgb(20, 45, 40)),
|
generator: (cyan, Color::Rgb(20, 45, 40)),
|
||||||
|
user_defined: (orange, Color::Rgb(40, 25, 10)),
|
||||||
default: (fg_dim, bg),
|
default: (fg_dim, bg),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (pine, Color::Rgb(35, 50, 55)),
|
variable: (pine, Color::Rgb(35, 50, 55)),
|
||||||
vary: (subtle, Color::Rgb(60, 55, 55)),
|
vary: (subtle, Color::Rgb(60, 55, 55)),
|
||||||
generator: (foam, Color::Rgb(40, 55, 60)),
|
generator: (foam, Color::Rgb(40, 55, 60)),
|
||||||
|
user_defined: (love, Color::Rgb(55, 35, 45)),
|
||||||
default: (fg_dim, darker_bg),
|
default: (fg_dim, darker_bg),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
variable: (green, Color::Rgb(50, 60, 45)),
|
variable: (green, Color::Rgb(50, 60, 45)),
|
||||||
vary: (yellow, Color::Rgb(70, 60, 45)),
|
vary: (yellow, Color::Rgb(70, 60, 45)),
|
||||||
generator: (cyan, Color::Rgb(45, 60, 75)),
|
generator: (cyan, Color::Rgb(45, 60, 75)),
|
||||||
|
user_defined: (orange, Color::Rgb(60, 50, 35)),
|
||||||
default: (fg_dim, darker_bg),
|
default: (fg_dim, darker_bg),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ pub fn rotate_theme(theme: ThemeColors, degrees: f32) -> ThemeColors {
|
|||||||
variable: rotate_color_pair(theme.syntax.variable, degrees),
|
variable: rotate_color_pair(theme.syntax.variable, degrees),
|
||||||
vary: rotate_color_pair(theme.syntax.vary, degrees),
|
vary: rotate_color_pair(theme.syntax.vary, degrees),
|
||||||
generator: rotate_color_pair(theme.syntax.generator, 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),
|
default: rotate_color_pair(theme.syntax.default, degrees),
|
||||||
},
|
},
|
||||||
table: TableColors {
|
table: TableColors {
|
||||||
|
|||||||
160
src/app.rs
160
src/app.rs
@@ -58,8 +58,8 @@ pub struct App {
|
|||||||
pub rng: Rng,
|
pub rng: Rng,
|
||||||
pub live_keys: Arc<LiveKeyState>,
|
pub live_keys: Arc<LiveKeyState>,
|
||||||
pub clipboard: Option<arboard::Clipboard>,
|
pub clipboard: Option<arboard::Clipboard>,
|
||||||
pub copied_pattern: Option<Pattern>,
|
pub copied_patterns: Option<Vec<Pattern>>,
|
||||||
pub copied_bank: Option<Bank>,
|
pub copied_banks: Option<Vec<Bank>>,
|
||||||
|
|
||||||
pub audio: AudioSettings,
|
pub audio: AudioSettings,
|
||||||
pub options: OptionsState,
|
pub options: OptionsState,
|
||||||
@@ -100,8 +100,8 @@ impl App {
|
|||||||
live_keys,
|
live_keys,
|
||||||
script_engine,
|
script_engine,
|
||||||
clipboard: arboard::Clipboard::new().ok(),
|
clipboard: arboard::Clipboard::new().ok(),
|
||||||
copied_pattern: None,
|
copied_patterns: None,
|
||||||
copied_bank: None,
|
copied_banks: None,
|
||||||
|
|
||||||
audio: AudioSettings::default(),
|
audio: AudioSettings::default(),
|
||||||
options: OptionsState::default(),
|
options: OptionsState::default(),
|
||||||
@@ -682,12 +682,17 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy_pattern(&mut self, bank: usize, pattern: usize) {
|
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);
|
self.ui.flash("Pattern copied", 150, FlashKind::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn paste_pattern(&mut self, bank: usize, pattern: usize) {
|
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);
|
clipboard::paste_pattern(&mut self.project_state.project, bank, pattern, &src);
|
||||||
self.project_state.mark_dirty(bank, pattern);
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == 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) {
|
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);
|
self.ui.flash("Bank copied", 150, FlashKind::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn paste_bank(&mut self, bank: usize) {
|
pub fn paste_bank(&mut self, bank: usize) {
|
||||||
if let Some(src) = self.copied_bank.clone() {
|
if let Some(src) = self.copied_banks.as_ref().and_then(|v| v.first()) {
|
||||||
let pat_count = clipboard::paste_bank(&mut self.project_state.project, bank, &src);
|
let src = src.clone();
|
||||||
|
let pat_count =
|
||||||
|
clipboard::paste_bank(&mut self.project_state.project, bank, &src);
|
||||||
for pattern in 0..pat_count {
|
for pattern in 0..pat_count {
|
||||||
self.project_state.mark_dirty(bank, pattern);
|
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) {
|
pub fn harden_steps(&mut self) {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
let indices = self.selected_steps();
|
let indices = self.selected_steps();
|
||||||
@@ -944,12 +1063,30 @@ impl App {
|
|||||||
AppCommand::PastePattern { bank, pattern } => {
|
AppCommand::PastePattern { bank, pattern } => {
|
||||||
self.paste_pattern(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 } => {
|
AppCommand::CopyBank { bank } => {
|
||||||
self.copy_bank(bank);
|
self.copy_bank(bank);
|
||||||
}
|
}
|
||||||
AppCommand::PasteBank { bank } => {
|
AppCommand::PasteBank { bank } => {
|
||||||
self.paste_bank(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
|
// Clipboard
|
||||||
AppCommand::HardenSteps => self.harden_steps(),
|
AppCommand::HardenSteps => self.harden_steps(),
|
||||||
@@ -1090,11 +1227,6 @@ impl App {
|
|||||||
AppCommand::PatternsBack => {
|
AppCommand::PatternsBack => {
|
||||||
self.page.down();
|
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)
|
// Mute/Solo (staged)
|
||||||
AppCommand::StageMute { bank, pattern } => {
|
AppCommand::StageMute { bank, pattern } => {
|
||||||
|
|||||||
@@ -60,12 +60,33 @@ pub enum AppCommand {
|
|||||||
bank: usize,
|
bank: usize,
|
||||||
pattern: usize,
|
pattern: usize,
|
||||||
},
|
},
|
||||||
|
CopyPatterns {
|
||||||
|
bank: usize,
|
||||||
|
patterns: Vec<usize>,
|
||||||
|
},
|
||||||
|
PastePatterns {
|
||||||
|
bank: usize,
|
||||||
|
start: usize,
|
||||||
|
},
|
||||||
CopyBank {
|
CopyBank {
|
||||||
bank: usize,
|
bank: usize,
|
||||||
},
|
},
|
||||||
PasteBank {
|
PasteBank {
|
||||||
bank: usize,
|
bank: usize,
|
||||||
},
|
},
|
||||||
|
CopyBanks {
|
||||||
|
banks: Vec<usize>,
|
||||||
|
},
|
||||||
|
PasteBanks {
|
||||||
|
start: usize,
|
||||||
|
},
|
||||||
|
ResetPatterns {
|
||||||
|
bank: usize,
|
||||||
|
patterns: Vec<usize>,
|
||||||
|
},
|
||||||
|
ResetBanks {
|
||||||
|
banks: Vec<usize>,
|
||||||
|
},
|
||||||
|
|
||||||
// Clipboard
|
// Clipboard
|
||||||
HardenSteps,
|
HardenSteps,
|
||||||
@@ -153,7 +174,6 @@ pub enum AppCommand {
|
|||||||
PatternsCursorDown,
|
PatternsCursorDown,
|
||||||
PatternsEnter,
|
PatternsEnter,
|
||||||
PatternsBack,
|
PatternsBack,
|
||||||
PatternsTogglePlay,
|
|
||||||
|
|
||||||
// Mute/Solo (staged)
|
// Mute/Solo (staged)
|
||||||
StageMute { bank: usize, pattern: usize },
|
StageMute { bank: usize, pattern: usize },
|
||||||
|
|||||||
258
src/input.rs
258
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!(),
|
Modal::None => unreachable!(),
|
||||||
}
|
}
|
||||||
InputResult::Continue
|
InputResult::Continue
|
||||||
@@ -1142,14 +1206,55 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
use crate::state::PatternsColumn;
|
use crate::state::PatternsColumn;
|
||||||
|
|
||||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||||
|
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||||
|
|
||||||
match key.code {
|
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::Left => ctx.dispatch(AppCommand::PatternsCursorLeft),
|
||||||
KeyCode::Right => ctx.dispatch(AppCommand::PatternsCursorRight),
|
KeyCode::Right => ctx.dispatch(AppCommand::PatternsCursorRight),
|
||||||
KeyCode::Up => ctx.dispatch(AppCommand::PatternsCursorUp),
|
|
||||||
KeyCode::Down => ctx.dispatch(AppCommand::PatternsCursorDown),
|
|
||||||
KeyCode::Esc => {
|
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_mute_changes.is_empty()
|
||||||
|| !ctx.app.playback.staged_prop_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);
|
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') => {
|
KeyCode::Char('p') => {
|
||||||
if ctx.app.patterns_nav.column == PatternsColumn::Patterns {
|
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(' ') => {
|
KeyCode::Char(' ') => {
|
||||||
@@ -1184,11 +1296,21 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||||
match ctx.app.patterns_nav.column {
|
match ctx.app.patterns_nav.column {
|
||||||
PatternsColumn::Banks => {
|
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 => {
|
PatternsColumn::Patterns => {
|
||||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
let patterns = ctx.app.patterns_nav.selected_patterns();
|
||||||
ctx.dispatch(AppCommand::CopyPattern { bank, pattern });
|
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;
|
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||||
match ctx.app.patterns_nav.column {
|
match ctx.app.patterns_nav.column {
|
||||||
PatternsColumn::Banks => {
|
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 => {
|
PatternsColumn::Patterns => {
|
||||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
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;
|
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||||
match ctx.app.patterns_nav.column {
|
match ctx.app.patterns_nav.column {
|
||||||
PatternsColumn::Banks => {
|
PatternsColumn::Banks => {
|
||||||
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmResetBank {
|
let banks = ctx.app.patterns_nav.selected_banks();
|
||||||
bank,
|
if banks.len() > 1 {
|
||||||
selected: false,
|
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmResetBanks {
|
||||||
}));
|
banks,
|
||||||
|
selected: false,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmResetBank {
|
||||||
|
bank,
|
||||||
|
selected: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PatternsColumn::Patterns => {
|
PatternsColumn::Patterns => {
|
||||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
let patterns = ctx.app.patterns_nav.selected_patterns();
|
||||||
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmResetPattern {
|
if patterns.len() > 1 {
|
||||||
bank,
|
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmResetPatterns {
|
||||||
pattern,
|
bank,
|
||||||
selected: false,
|
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') => {
|
KeyCode::Char('r') => {
|
||||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
if !ctx.app.patterns_nav.has_selection() {
|
||||||
match ctx.app.patterns_nav.column {
|
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||||
PatternsColumn::Banks => {
|
match ctx.app.patterns_nav.column {
|
||||||
let current_name = ctx.app.project_state.project.banks[bank]
|
PatternsColumn::Banks => {
|
||||||
.name
|
let current_name = ctx.app.project_state.project.banks[bank]
|
||||||
.clone()
|
.name
|
||||||
.unwrap_or_default();
|
.clone()
|
||||||
ctx.dispatch(AppCommand::OpenModal(Modal::RenameBank {
|
.unwrap_or_default();
|
||||||
bank,
|
ctx.dispatch(AppCommand::OpenModal(Modal::RenameBank {
|
||||||
name: current_name,
|
bank,
|
||||||
}));
|
name: current_name,
|
||||||
}
|
}));
|
||||||
PatternsColumn::Patterns => {
|
}
|
||||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
PatternsColumn::Patterns => {
|
||||||
let current_name = ctx.app.project_state.project.banks[bank].patterns[pattern]
|
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||||
.name
|
let current_name = ctx.app.project_state.project.banks[bank].patterns
|
||||||
.clone()
|
[pattern]
|
||||||
.unwrap_or_default();
|
.name
|
||||||
ctx.dispatch(AppCommand::OpenModal(Modal::RenamePattern {
|
.clone()
|
||||||
bank,
|
.unwrap_or_default();
|
||||||
pattern,
|
ctx.dispatch(AppCommand::OpenModal(Modal::RenamePattern {
|
||||||
name: current_name,
|
bank,
|
||||||
}));
|
pattern,
|
||||||
|
name: current_name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('e') if !ctrl => {
|
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 bank = ctx.app.patterns_nav.bank_cursor;
|
||||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||||
ctx.dispatch(AppCommand::OpenPatternPropsModal { bank, pattern });
|
ctx.dispatch(AppCommand::OpenPatternPropsModal { bank, pattern });
|
||||||
@@ -1259,13 +1419,15 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
}
|
}
|
||||||
KeyCode::Char('m') => {
|
KeyCode::Char('m') => {
|
||||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
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 });
|
ctx.dispatch(AppCommand::StageMute { bank, pattern });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('x') => {
|
KeyCode::Char('x') => {
|
||||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
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 });
|
ctx.dispatch(AppCommand::StageSolo { bank, pattern });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('M') => {
|
KeyCode::Char('M') => {
|
||||||
ctx.dispatch(AppCommand::ClearMutes);
|
ctx.dispatch(AppCommand::ClearMutes);
|
||||||
|
|||||||
@@ -24,6 +24,33 @@ pub fn paste_pattern(
|
|||||||
project.banks[bank].patterns[pattern] = pat;
|
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 {
|
pub fn copy_bank(project: &Project, bank: usize) -> Bank {
|
||||||
project.banks[bank].clone()
|
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()
|
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(
|
pub fn copy_steps(
|
||||||
project: &Project,
|
project: &Project,
|
||||||
bank: usize,
|
bank: usize,
|
||||||
|
|||||||
@@ -29,6 +29,15 @@ pub enum Modal {
|
|||||||
bank: usize,
|
bank: usize,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
},
|
},
|
||||||
|
ConfirmResetPatterns {
|
||||||
|
bank: usize,
|
||||||
|
patterns: Vec<usize>,
|
||||||
|
selected: bool,
|
||||||
|
},
|
||||||
|
ConfirmResetBanks {
|
||||||
|
banks: Vec<usize>,
|
||||||
|
selected: bool,
|
||||||
|
},
|
||||||
FileBrowser(Box<FileBrowserState>),
|
FileBrowser(Box<FileBrowserState>),
|
||||||
RenameBank {
|
RenameBank {
|
||||||
bank: usize,
|
bank: usize,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
use crate::model::{MAX_BANKS, MAX_PATTERNS};
|
use crate::model::{MAX_BANKS, MAX_PATTERNS};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||||
@@ -12,14 +14,18 @@ pub struct PatternsNav {
|
|||||||
pub column: PatternsColumn,
|
pub column: PatternsColumn,
|
||||||
pub bank_cursor: usize,
|
pub bank_cursor: usize,
|
||||||
pub pattern_cursor: usize,
|
pub pattern_cursor: usize,
|
||||||
|
pub bank_anchor: Option<usize>,
|
||||||
|
pub pattern_anchor: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PatternsNav {
|
impl PatternsNav {
|
||||||
pub fn move_left(&mut self) {
|
pub fn move_left(&mut self) {
|
||||||
|
self.clear_selection();
|
||||||
self.column = PatternsColumn::Banks;
|
self.column = PatternsColumn::Banks;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_right(&mut self) {
|
pub fn move_right(&mut self) {
|
||||||
|
self.clear_selection();
|
||||||
self.column = PatternsColumn::Patterns;
|
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 {
|
pub fn selected_bank(&self) -> usize {
|
||||||
self.bank_cursor
|
self.bank_cursor
|
||||||
}
|
}
|
||||||
@@ -52,4 +80,44 @@ impl PatternsNav {
|
|||||||
pub fn selected_pattern(&self) -> usize {
|
pub fn selected_pattern(&self) -> usize {
|
||||||
self.pattern_cursor
|
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 ratatui::style::{Modifier, Style};
|
||||||
|
|
||||||
use crate::model::{lookup_word, SourceSpan, WordCompile};
|
use crate::model::{lookup_word, SourceSpan, WordCompile};
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
|
static EMPTY_SET: LazyLock<HashSet<String>> = LazyLock::new(HashSet::new);
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum TokenKind {
|
pub enum TokenKind {
|
||||||
Number,
|
Number,
|
||||||
@@ -20,6 +25,7 @@ pub enum TokenKind {
|
|||||||
Emit,
|
Emit,
|
||||||
Vary,
|
Vary,
|
||||||
Generator,
|
Generator,
|
||||||
|
UserDefined,
|
||||||
Default,
|
Default,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +48,7 @@ impl TokenKind {
|
|||||||
TokenKind::Variable => theme.syntax.variable,
|
TokenKind::Variable => theme.syntax.variable,
|
||||||
TokenKind::Vary => theme.syntax.vary,
|
TokenKind::Vary => theme.syntax.vary,
|
||||||
TokenKind::Generator => theme.syntax.generator,
|
TokenKind::Generator => theme.syntax.generator,
|
||||||
|
TokenKind::UserDefined => theme.syntax.user_defined,
|
||||||
TokenKind::Default => theme.syntax.default,
|
TokenKind::Default => theme.syntax.default,
|
||||||
};
|
};
|
||||||
let style = Style::default().fg(fg).bg(bg);
|
let style = Style::default().fg(fg).bg(bg);
|
||||||
@@ -114,7 +121,7 @@ const INTERVALS: &[&str] = &[
|
|||||||
"M14", "P15",
|
"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 tokens = Vec::new();
|
||||||
let mut chars = line.char_indices().peekable();
|
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 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.push(Token { start, end, kind, varargs });
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens
|
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() {
|
if word.parse::<f64>().is_ok() || word.parse::<i64>().is_ok() {
|
||||||
return (TokenKind::Number, false);
|
return (TokenKind::Number, false);
|
||||||
}
|
}
|
||||||
@@ -188,19 +195,24 @@ fn classify_word(word: &str) -> (TokenKind, bool) {
|
|||||||
return (TokenKind::Variable, false);
|
return (TokenKind::Variable, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user_words.contains(word) {
|
||||||
|
return (TokenKind::UserDefined, false);
|
||||||
|
}
|
||||||
|
|
||||||
(TokenKind::Default, false)
|
(TokenKind::Default, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn highlight_line(line: &str) -> Vec<(Style, String)> {
|
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(
|
pub fn highlight_line_with_runtime(
|
||||||
line: &str,
|
line: &str,
|
||||||
executed_spans: &[SourceSpan],
|
executed_spans: &[SourceSpan],
|
||||||
selected_spans: &[SourceSpan],
|
selected_spans: &[SourceSpan],
|
||||||
|
user_words: &HashSet<String>,
|
||||||
) -> Vec<(Style, String)> {
|
) -> Vec<(Style, String)> {
|
||||||
let tokens = tokenize_line(line);
|
let tokens = tokenize_line(line, user_words);
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut last_end = 0;
|
let mut last_end = 0;
|
||||||
let gap_style = TokenKind::gap_style();
|
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_edit = idx == app.editor_ctx.bank;
|
||||||
let is_playing = banks_with_playback.contains(&idx);
|
let is_playing = banks_with_playback.contains(&idx);
|
||||||
let is_staged = banks_with_staged.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)
|
// 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));
|
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 {
|
let (bg, fg, prefix) = if is_cursor {
|
||||||
(theme.selection.cursor, theme.selection.cursor_fg, "")
|
(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 {
|
} else if is_playing {
|
||||||
if has_staged_mute_solo {
|
if has_staged_mute_solo {
|
||||||
(theme.list.staged_play_bg, theme.list.staged_play_fg, ">*")
|
(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_playing = playing_patterns.contains(&idx);
|
||||||
let is_staged_play = staged_to_play.contains(&idx);
|
let is_staged_play = staged_to_play.contains(&idx);
|
||||||
let is_staged_stop = staged_to_stop.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
|
// Current applied mute/solo state
|
||||||
let is_muted = app.mute.is_muted(bank, idx);
|
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 {
|
let (bg, fg, prefix) = if is_cursor {
|
||||||
(theme.selection.cursor, theme.selection.cursor_fg, "")
|
(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 {
|
} else if is_playing {
|
||||||
// Playing patterns
|
// Playing patterns
|
||||||
if is_staged_stop {
|
if is_staged_stop {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||||
@@ -14,7 +15,7 @@ use crate::state::{
|
|||||||
EditorTarget, EuclideanField, FlashKind, Modal, PanelFocus, PatternField, SidePanel,
|
EditorTarget, EuclideanField, FlashKind, Modal, PanelFocus, PatternField, SidePanel,
|
||||||
};
|
};
|
||||||
use crate::theme;
|
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::{
|
use crate::widgets::{
|
||||||
ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal,
|
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> {
|
fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term: Rect) -> Option<Rect> {
|
||||||
let theme = theme::get();
|
let theme = theme::get();
|
||||||
|
let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
|
||||||
let inner = match &app.ui.modal {
|
let inner = match &app.ui.modal {
|
||||||
Modal::None => return None,
|
Modal::None => return None,
|
||||||
Modal::ConfirmQuit { selected } => {
|
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)
|
ConfirmModal::new("Confirm", &format!("Reset bank {}?", bank + 1), *selected)
|
||||||
.render_centered(frame, term)
|
.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) => {
|
Modal::FileBrowser(state) => {
|
||||||
use crate::state::file_browser::FileBrowserMode;
|
use crate::state::file_browser::FileBrowserMode;
|
||||||
use crate::widgets::FileBrowserModal;
|
use crate::widgets::FileBrowserModal;
|
||||||
@@ -621,9 +633,9 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
line_start,
|
line_start,
|
||||||
line_str.len(),
|
line_str.len(),
|
||||||
);
|
);
|
||||||
highlight_line_with_runtime(line_str, &exec, &sel)
|
highlight_line_with_runtime(line_str, &exec, &sel, &user_words)
|
||||||
} else {
|
} else {
|
||||||
highlight_line(line_str)
|
highlight_line_with_runtime(line_str, &[], &[], &user_words)
|
||||||
};
|
};
|
||||||
line_start += line_str.len() + 1;
|
line_start += line_str.len() + 1;
|
||||||
let spans: Vec<Span> = tokens
|
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()),
|
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()
|
let show_search = app.editor_ctx.editor.search_active()
|
||||||
|
|||||||
Reference in New Issue
Block a user