Pattern mute and so on

This commit is contained in:
2026-02-02 16:27:11 +01:00
parent 7c14ce7634
commit 39ca7de169
29 changed files with 518 additions and 58 deletions

View File

@@ -17,8 +17,8 @@ use crate::services::pattern_editor;
use crate::settings::Settings;
use crate::state::{
AudioSettings, CyclicEnum, DictFocus, EditorContext, FlashKind, LiveKeyState, Metrics, Modal,
OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav, PlaybackState,
ProjectState, StagedChange, UiState,
MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
PlaybackState, ProjectState, StagedChange, UiState,
};
use crate::views::{dict_view, help_view};
@@ -28,6 +28,7 @@ pub struct App {
pub project_state: ProjectState,
pub ui: UiState,
pub playback: PlaybackState,
pub mute: MuteState,
pub page: Page,
pub editor_ctx: EditorContext,
@@ -69,6 +70,7 @@ impl App {
project_state: ProjectState::default(),
ui: UiState::default(),
playback: PlaybackState::default(),
mute: MuteState::default(),
page: Page::default(),
editor_ctx: EditorContext::default(),
@@ -494,26 +496,64 @@ impl App {
}
}
pub fn commit_staged_changes(&mut self) {
if self.playback.staged_changes.is_empty() {
/// Commits staged pattern and mute/solo changes.
/// Returns true if mute state changed (caller should send to sequencer).
pub fn commit_staged_changes(&mut self) -> bool {
let pattern_count = self.playback.staged_changes.len();
let mute_count = self.playback.staged_mute_changes.len();
if pattern_count == 0 && mute_count == 0 {
self.ui.set_status("No changes to commit".to_string());
return;
return false;
}
let count = self.playback.staged_changes.len();
self.playback
.queued_changes
.append(&mut self.playback.staged_changes);
self.ui.set_status(format!("Committed {count} changes"));
// Commit pattern changes (queued for quantization)
if pattern_count > 0 {
self.playback
.queued_changes
.append(&mut self.playback.staged_changes);
}
// Apply mute/solo changes immediately
let mute_changed = mute_count > 0;
for change in self.playback.staged_mute_changes.drain() {
match change {
crate::state::StagedMuteChange::ToggleMute { bank, pattern } => {
self.mute.toggle_mute(bank, pattern);
}
crate::state::StagedMuteChange::ToggleSolo { bank, pattern } => {
self.mute.toggle_solo(bank, pattern);
}
}
}
let status = match (pattern_count, mute_count) {
(0, m) => format!("Applied {m} mute/solo changes"),
(p, 0) => format!("Committed {p} pattern changes"),
(p, m) => format!("Committed {p} pattern + {m} mute/solo changes"),
};
self.ui.set_status(status);
mute_changed
}
pub fn clear_staged_changes(&mut self) {
if self.playback.staged_changes.is_empty() {
let pattern_count = self.playback.staged_changes.len();
let mute_count = self.playback.staged_mute_changes.len();
if pattern_count == 0 && mute_count == 0 {
return;
}
let count = self.playback.staged_changes.len();
self.playback.staged_changes.clear();
self.ui
.set_status(format!("Cleared {count} staged changes"));
self.playback.staged_mute_changes.clear();
let status = match (pattern_count, mute_count) {
(0, m) => format!("Cleared {m} staged mute/solo"),
(p, 0) => format!("Cleared {p} staged patterns"),
(p, m) => format!("Cleared {p} patterns + {m} mute/solo"),
};
self.ui.set_status(status);
}
pub fn select_edit_pattern(&mut self, pattern: usize) {
@@ -1094,9 +1134,6 @@ impl App {
AppCommand::DuplicateSteps => self.duplicate_steps(link),
// Pattern playback (staging)
AppCommand::CommitStagedChanges => {
self.commit_staged_changes();
}
AppCommand::ClearStagedChanges => {
self.clear_staged_changes();
}
@@ -1300,6 +1337,22 @@ impl App {
self.stage_pattern_toggle(bank, pattern, snapshot);
}
// Mute/Solo (staged)
AppCommand::StageMute { bank, pattern } => {
self.playback.stage_mute(bank, pattern);
}
AppCommand::StageSolo { bank, pattern } => {
self.playback.stage_solo(bank, pattern);
}
AppCommand::ClearMutes => {
self.playback.clear_staged_mutes();
self.mute.clear_mute();
}
AppCommand::ClearSolos => {
self.playback.clear_staged_solos();
self.mute.clear_solo();
}
// UI state
AppCommand::ClearMinimap => {
self.ui.minimap_until = None;
@@ -1522,6 +1575,13 @@ impl App {
}
}
pub fn send_mute_state(&self, cmd_tx: &Sender<SeqCommand>) {
let _ = cmd_tx.send(SeqCommand::SetMuteState {
muted: self.mute.muted.clone(),
soloed: self.mute.soloed.clone(),
});
}
pub fn flush_dirty_patterns(&mut self, cmd_tx: &Sender<SeqCommand>) {
for (bank, pattern) in self.project_state.take_dirty() {
let pat = self.project_state.project.pattern_at(bank, pattern);

View File

@@ -75,7 +75,6 @@ pub enum AppCommand {
DuplicateSteps,
// Pattern playback (staging)
CommitStagedChanges,
ClearStagedChanges,
// Project
@@ -156,6 +155,12 @@ pub enum AppCommand {
PatternsBack,
PatternsTogglePlay,
// Mute/Solo (staged)
StageMute { bank: usize, pattern: usize },
StageSolo { bank: usize, pattern: usize },
ClearMutes, // Clears both staged and applied mutes
ClearSolos, // Clears both staged and applied solos
// UI state
ClearMinimap,
HideTitle,

View File

@@ -115,6 +115,10 @@ pub enum SeqCommand {
pattern: usize,
quantization: LaunchQuantization,
},
SetMuteState {
muted: std::collections::HashSet<(usize, usize)>,
soloed: std::collections::HashSet<(usize, usize)>,
},
StopAll,
Shutdown,
}
@@ -551,6 +555,8 @@ pub(crate) struct SequencerState {
buf_audio_commands: Vec<TimestampedCommand>,
cc_access: Option<Arc<dyn CcAccess>>,
active_notes: HashMap<(u8, u8, u8), ActiveNote>,
muted: std::collections::HashSet<(usize, usize)>,
soloed: std::collections::HashSet<(usize, usize)>,
}
impl SequencerState {
@@ -575,9 +581,22 @@ impl SequencerState {
buf_audio_commands: Vec::new(),
cc_access,
active_notes: HashMap::new(),
muted: std::collections::HashSet::new(),
soloed: std::collections::HashSet::new(),
}
}
fn is_effectively_muted(&self, bank: usize, pattern: usize) -> bool {
let key = (bank, pattern);
if self.muted.contains(&key) {
return true;
}
if !self.soloed.is_empty() && !self.soloed.contains(&key) {
return true;
}
false
}
fn process_commands(&mut self, commands: Vec<SeqCommand>) {
for cmd in commands {
match cmd {
@@ -619,6 +638,28 @@ impl SequencerState {
});
}
}
SeqCommand::SetMuteState { muted, soloed } => {
let newly_muted: Vec<(usize, usize)> = self
.audio_state
.active_patterns
.keys()
.filter(|id| {
let key = (id.bank, id.pattern);
let was_muted = self.is_effectively_muted(id.bank, id.pattern);
let now_muted = muted.contains(&key)
|| (!soloed.is_empty() && !soloed.contains(&key));
!was_muted && now_muted
})
.map(|id| (id.bank, id.pattern))
.collect();
self.muted = muted;
self.soloed = soloed;
if !newly_muted.is_empty() {
self.audio_state.flush_midi_notes = true;
}
}
SeqCommand::StopAll => {
self.audio_state.active_patterns.clear();
self.audio_state.pending_starts.clear();
@@ -783,6 +824,9 @@ impl SequencerState {
}
}
let muted_snapshot = self.muted.clone();
let soloed_snapshot = self.soloed.clone();
for (_id, active) in self.audio_state.active_patterns.iter_mut() {
let Some(pattern) = self.pattern_cache.get(active.bank, active.pattern) else {
continue;
@@ -807,6 +851,10 @@ impl SequencerState {
.unwrap_or(false);
if step.active && has_script {
let key = (active.bank, active.pattern);
let is_muted = muted_snapshot.contains(&key)
|| (!soloed_snapshot.is_empty() && !soloed_snapshot.contains(&key));
let source_idx = pattern.resolve_source(step_idx);
let runs = self.runs_counter.get_and_increment(
active.bank,
@@ -845,18 +893,20 @@ impl SequencerState {
std::mem::take(&mut trace),
);
let event_time = if lookahead_secs > 0.0 {
Some(engine_time + lookahead_secs)
} else {
None
};
if !is_muted {
let event_time = if lookahead_secs > 0.0 {
Some(engine_time + lookahead_secs)
} else {
None
};
for cmd in cmds {
self.event_count += 1;
self.buf_audio_commands.push(TimestampedCommand {
cmd,
time: event_time,
});
for cmd in cmds {
self.event_count += 1;
self.buf_audio_commands.push(TimestampedCommand {
cmd,
time: event_time,
});
}
}
}
}

View File

@@ -1054,6 +1054,22 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
rotation: "0".to_string(),
}));
}
KeyCode::Char('m') => {
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
ctx.dispatch(AppCommand::StageMute { bank, pattern });
}
KeyCode::Char('x') => {
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
ctx.dispatch(AppCommand::StageSolo { bank, pattern });
}
KeyCode::Char('M') => {
ctx.dispatch(AppCommand::ClearMutes);
ctx.app.send_mute_state(ctx.seq_cmd_tx);
}
KeyCode::Char('X') => {
ctx.dispatch(AppCommand::ClearSolos);
ctx.app.send_mute_state(ctx.seq_cmd_tx);
}
_ => {}
}
InputResult::Continue
@@ -1070,7 +1086,9 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
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.playback.staged_changes.is_empty()
|| !ctx.app.playback.staged_mute_changes.is_empty()
{
ctx.dispatch(AppCommand::ClearStagedChanges);
} else {
ctx.dispatch(AppCommand::PatternsBack);
@@ -1082,7 +1100,12 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
ctx.dispatch(AppCommand::PatternsTogglePlay);
}
}
KeyCode::Char('c') if !ctrl => ctx.dispatch(AppCommand::CommitStagedChanges),
KeyCode::Char('c') if !ctrl => {
let mute_changed = ctx.app.commit_staged_changes();
if mute_changed {
ctx.app.send_mute_state(ctx.seq_cmd_tx);
}
}
KeyCode::Char('q') => {
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
selected: false,
@@ -1165,6 +1188,24 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
ctx.dispatch(AppCommand::OpenPatternPropsModal { bank, pattern });
}
}
KeyCode::Char('m') => {
let bank = ctx.app.patterns_nav.bank_cursor;
let pattern = ctx.app.patterns_nav.pattern_cursor;
ctx.dispatch(AppCommand::StageMute { bank, pattern });
}
KeyCode::Char('x') => {
let bank = ctx.app.patterns_nav.bank_cursor;
let pattern = ctx.app.patterns_nav.pattern_cursor;
ctx.dispatch(AppCommand::StageSolo { bank, pattern });
}
KeyCode::Char('M') => {
ctx.dispatch(AppCommand::ClearMutes);
ctx.app.send_mute_state(ctx.seq_cmd_tx);
}
KeyCode::Char('X') => {
ctx.dispatch(AppCommand::ClearSolos);
ctx.app.send_mute_state(ctx.seq_cmd_tx);
}
KeyCode::Char('?') => {
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
}

View File

@@ -19,6 +19,7 @@ pub mod editor;
pub mod file_browser;
pub mod live_keys;
pub mod modal;
pub mod mute;
pub mod options;
pub mod panel;
pub mod patterns_nav;
@@ -38,7 +39,8 @@ pub use modal::Modal;
pub use options::{OptionsFocus, OptionsState};
pub use panel::{PanelFocus, PanelState, SidePanel};
pub use patterns_nav::{PatternsColumn, PatternsNav};
pub use playback::{PlaybackState, StagedChange};
pub use mute::MuteState;
pub use playback::{PlaybackState, StagedChange, StagedMuteChange};
pub use project::ProjectState;
pub use sample_browser::SampleBrowserState;
pub use ui::{DictFocus, FlashKind, HelpFocus, UiState};

53
src/state/mute.rs Normal file
View File

@@ -0,0 +1,53 @@
use std::collections::HashSet;
#[derive(Default)]
pub struct MuteState {
pub muted: HashSet<(usize, usize)>,
pub soloed: HashSet<(usize, usize)>,
}
impl MuteState {
pub fn toggle_mute(&mut self, bank: usize, pattern: usize) {
let key = (bank, pattern);
if self.muted.contains(&key) {
self.muted.remove(&key);
} else {
self.muted.insert(key);
}
}
pub fn toggle_solo(&mut self, bank: usize, pattern: usize) {
let key = (bank, pattern);
if self.soloed.contains(&key) {
self.soloed.remove(&key);
} else {
self.soloed.insert(key);
}
}
pub fn clear_mute(&mut self) {
self.muted.clear();
}
pub fn clear_solo(&mut self) {
self.soloed.clear();
}
pub fn is_muted(&self, bank: usize, pattern: usize) -> bool {
self.muted.contains(&(bank, pattern))
}
pub fn is_soloed(&self, bank: usize, pattern: usize) -> bool {
self.soloed.contains(&(bank, pattern))
}
pub fn is_effectively_muted(&self, bank: usize, pattern: usize) -> bool {
if self.muted.contains(&(bank, pattern)) {
return true;
}
if !self.soloed.is_empty() && !self.soloed.contains(&(bank, pattern)) {
return true;
}
false
}
}

View File

@@ -1,5 +1,6 @@
use crate::engine::PatternChange;
use crate::model::{LaunchQuantization, SyncMode};
use std::collections::HashSet;
#[derive(Clone)]
pub struct StagedChange {
@@ -8,10 +9,17 @@ pub struct StagedChange {
pub sync_mode: SyncMode,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum StagedMuteChange {
ToggleMute { bank: usize, pattern: usize },
ToggleSolo { bank: usize, pattern: usize },
}
pub struct PlaybackState {
pub playing: bool,
pub staged_changes: Vec<StagedChange>,
pub queued_changes: Vec<StagedChange>,
pub staged_mute_changes: HashSet<StagedMuteChange>,
}
impl Default for PlaybackState {
@@ -20,6 +28,7 @@ impl Default for PlaybackState {
playing: true,
staged_changes: Vec::new(),
queued_changes: Vec::new(),
staged_mute_changes: HashSet::new(),
}
}
}
@@ -33,4 +42,38 @@ impl PlaybackState {
self.staged_changes.clear();
self.queued_changes.clear();
}
pub fn stage_mute(&mut self, bank: usize, pattern: usize) {
let change = StagedMuteChange::ToggleMute { bank, pattern };
if self.staged_mute_changes.contains(&change) {
self.staged_mute_changes.remove(&change);
} else {
self.staged_mute_changes.insert(change);
}
}
pub fn stage_solo(&mut self, bank: usize, pattern: usize) {
let change = StagedMuteChange::ToggleSolo { bank, pattern };
if self.staged_mute_changes.contains(&change) {
self.staged_mute_changes.remove(&change);
} else {
self.staged_mute_changes.insert(change);
}
}
pub fn clear_staged_mutes(&mut self) {
self.staged_mute_changes.retain(|c| !matches!(c, StagedMuteChange::ToggleMute { .. }));
}
pub fn clear_staged_solos(&mut self) {
self.staged_mute_changes.retain(|c| !matches!(c, StagedMuteChange::ToggleSolo { .. }));
}
pub fn has_staged_mute(&self, bank: usize, pattern: usize) -> bool {
self.staged_mute_changes.contains(&StagedMuteChange::ToggleMute { bank, pattern })
}
pub fn has_staged_solo(&self, bank: usize, pattern: usize) -> bool {
self.staged_mute_changes.contains(&StagedMuteChange::ToggleSolo { bank, pattern })
}
}

View File

@@ -323,6 +323,8 @@ fn render_vu_meter(frame: &mut Frame, app: &App, area: Rect) {
}
fn render_active_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
use crate::widgets::MuteStatus;
let theme = theme::get();
let block = Block::default()
.borders(Borders::ALL)
@@ -336,11 +338,27 @@ fn render_active_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnap
.map(|p| (p.bank, p.pattern, p.iter))
.collect();
let mute_status: Vec<MuteStatus> = snapshot
.active_patterns
.iter()
.map(|p| {
if app.mute.is_soloed(p.bank, p.pattern) {
MuteStatus::Soloed
} else if app.mute.is_muted(p.bank, p.pattern) {
MuteStatus::Muted
} else if app.mute.is_effectively_muted(p.bank, p.pattern) {
MuteStatus::EffectivelyMuted
} else {
MuteStatus::Normal
}
})
.collect();
let step_info = snapshot
.get_step(app.editor_ctx.bank, app.editor_ctx.pattern)
.map(|step| (step, app.current_edit_pattern().length));
let mut widget = ActivePatterns::new(&patterns);
let mut widget = ActivePatterns::new(&patterns).with_mute_status(&mute_status);
if let Some((step, total)) = step_info {
widget = widget.with_step(step, total);
}

View File

@@ -91,13 +91,41 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
let is_playing = banks_with_playback.contains(&idx);
let is_staged = banks_with_staged.contains(&idx);
let (bg, fg, prefix) = match (is_cursor, is_playing, is_staged) {
(true, _, _) => (theme.selection.cursor, theme.selection.cursor_fg, ""),
(false, true, _) => (theme.list.playing_bg, theme.list.playing_fg, "> "),
(false, false, true) => (theme.list.staged_play_bg, theme.list.staged_play_fg, "+ "),
(false, false, false) if is_selected => (theme.list.hover_bg, theme.list.hover_fg, ""),
(false, false, false) if is_edit => (theme.list.edit_bg, theme.list.edit_fg, ""),
(false, false, false) => (theme.ui.bg, theme.ui.text_muted, ""),
// 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_soloed = (0..MAX_PATTERNS).any(|p| app.mute.is_soloed(idx, p));
// Check if any pattern in this bank has staged mute/solo
let has_staged_mute = (0..MAX_PATTERNS).any(|p| app.playback.has_staged_mute(idx, p));
let has_staged_solo = (0..MAX_PATTERNS).any(|p| app.playback.has_staged_solo(idx, p));
let has_staged_mute_solo = has_staged_mute || has_staged_solo;
let (bg, fg, prefix) = if is_cursor {
(theme.selection.cursor, theme.selection.cursor_fg, "")
} else if is_playing {
if has_staged_mute_solo {
(theme.list.staged_play_bg, theme.list.staged_play_fg, ">*")
} else if has_soloed {
(theme.list.soloed_bg, theme.list.soloed_fg, ">S")
} else if has_muted {
(theme.list.muted_bg, theme.list.muted_fg, ">M")
} else {
(theme.list.playing_bg, theme.list.playing_fg, "> ")
}
} else if is_staged {
(theme.list.staged_play_bg, theme.list.staged_play_fg, "+ ")
} else if has_staged_mute_solo {
(theme.list.staged_play_bg, theme.list.staged_play_fg, " *")
} else if has_soloed && is_selected {
(theme.list.soloed_bg, theme.list.soloed_fg, " S")
} else if has_muted && is_selected {
(theme.list.muted_bg, theme.list.muted_fg, " M")
} else if is_selected {
(theme.list.hover_bg, theme.list.hover_fg, "")
} else if is_edit {
(theme.list.edit_bg, theme.list.edit_fg, "")
} else {
(theme.ui.bg, theme.ui.text_muted, "")
};
let name = app.project_state.project.banks[idx]
@@ -250,14 +278,74 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
let is_staged_play = staged_to_play.contains(&idx);
let is_staged_stop = staged_to_stop.contains(&idx);
let (bg, fg, prefix) = match (is_cursor, is_playing, is_staged_play, is_staged_stop) {
(true, _, _, _) => (theme.selection.cursor, theme.selection.cursor_fg, ""),
(false, true, _, true) => (theme.list.staged_stop_bg, theme.list.staged_stop_fg, "- "),
(false, true, _, false) => (theme.list.playing_bg, theme.list.playing_fg, "> "),
(false, false, true, _) => (theme.list.staged_play_bg, theme.list.staged_play_fg, "+ "),
(false, false, false, _) if is_selected => (theme.list.hover_bg, theme.list.hover_fg, ""),
(false, false, false, _) if is_edit => (theme.list.edit_bg, theme.list.edit_fg, ""),
(false, false, false, _) => (theme.ui.bg, theme.ui.text_muted, ""),
// Current applied mute/solo state
let is_muted = app.mute.is_muted(bank, idx);
let is_soloed = app.mute.is_soloed(bank, idx);
// Staged mute/solo (will toggle on commit)
let has_staged_mute = app.playback.has_staged_mute(bank, idx);
let has_staged_solo = app.playback.has_staged_solo(bank, idx);
// Preview state (what it will be after commit)
let preview_muted = is_muted ^ has_staged_mute;
let preview_soloed = is_soloed ^ has_staged_solo;
let is_effectively_muted = app.mute.is_effectively_muted(bank, idx);
let (bg, fg, prefix) = if is_cursor {
(theme.selection.cursor, theme.selection.cursor_fg, "")
} else if is_playing {
// Playing patterns
if is_staged_stop {
(theme.list.staged_stop_bg, theme.list.staged_stop_fg, "- ")
} else if has_staged_solo {
// Staged solo toggle on playing pattern
if preview_soloed {
(theme.list.soloed_bg, theme.list.soloed_fg, "+S")
} else {
(theme.list.playing_bg, theme.list.playing_fg, "-S")
}
} else if has_staged_mute {
// Staged mute toggle on playing pattern
if preview_muted {
(theme.list.muted_bg, theme.list.muted_fg, "+M")
} else {
(theme.list.playing_bg, theme.list.playing_fg, "-M")
}
} else if is_soloed {
(theme.list.soloed_bg, theme.list.soloed_fg, ">S")
} else if is_muted {
(theme.list.muted_bg, theme.list.muted_fg, ">M")
} else if is_effectively_muted {
(theme.list.muted_bg, theme.list.muted_fg, "> ")
} else {
(theme.list.playing_bg, theme.list.playing_fg, "> ")
}
} else if is_staged_play {
(theme.list.staged_play_bg, theme.list.staged_play_fg, "+ ")
} else if has_staged_solo {
// Staged solo on non-playing pattern
if preview_soloed {
(theme.list.soloed_bg, theme.list.soloed_fg, "+S")
} else {
(theme.ui.bg, theme.ui.text_muted, "-S")
}
} else if has_staged_mute {
// Staged mute on non-playing pattern
if preview_muted {
(theme.list.muted_bg, theme.list.muted_fg, "+M")
} else {
(theme.ui.bg, theme.ui.text_muted, "-M")
}
} else if is_soloed {
(theme.list.soloed_bg, theme.list.soloed_fg, " S")
} else if is_muted {
(theme.list.muted_bg, theme.list.muted_fg, " M")
} else if is_selected {
(theme.list.hover_bg, theme.list.hover_fg, "")
} else if is_edit {
(theme.list.edit_bg, theme.list.edit_fg, "")
} else {
(theme.ui.bg, theme.ui.text_muted, "")
};
let pattern = &app.project_state.project.banks[bank].patterns[idx];

View File

@@ -1,4 +1,4 @@
pub use cagire_ratatui::{
ActivePatterns, ConfirmModal, FileBrowserModal, ModalFrame, NavMinimap, NavTile, Orientation,
SampleBrowser, Scope, Spectrum, TextInputModal, VuMeter,
ActivePatterns, ConfirmModal, FileBrowserModal, ModalFrame, MuteStatus, NavMinimap, NavTile,
Orientation, SampleBrowser, Scope, Spectrum, TextInputModal, VuMeter,
};