Feat: better user feedback on patterns page
All checks were successful
Deploy Website / deploy (push) Has been skipped

This commit is contained in:
2026-03-04 23:41:11 +01:00
parent 4e1c04f9c7
commit 35370a6f2c
11 changed files with 142 additions and 49 deletions

View File

@@ -13,6 +13,20 @@ use crate::widgets::{render_scroll_indicators, IndicatorAlign};
const MIN_ROW_HEIGHT: u16 = 1;
fn pulse_value(phase: f32) -> f32 {
phase.sin() * 0.5 + 0.5
}
fn pulse_color(from: Color, to: Color, t: f32) -> Color {
match (from, to) {
(Color::Rgb(r1, g1, b1), Color::Rgb(r2, g2, b2)) => {
let l = |a: u8, b: u8| (a as f32 + (b as f32 - a as f32) * t) as u8;
Color::Rgb(l(r1, r2), l(g1, g2), l(b1, b2))
}
_ => from,
}
}
/// Replaces the background color of spans beyond `filled_cols` with `unfilled_bg`.
fn apply_progress_bg(spans: Vec<Span<'_>>, filled_cols: usize, unfilled_bg: Color) -> Vec<Span<'_>> {
let mut result = Vec::with_capacity(spans.len() + 1);
@@ -54,7 +68,32 @@ pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area:
Layout::horizontal([Constraint::Fill(1), Constraint::Length(22)]).areas(bottom_area);
render_banks(frame, app, snapshot, banks_area);
render_patterns(frame, app, snapshot, patterns_area);
let armed_summary = app.playback.armed_summary();
let (patterns_main, launch_bar_area) = if armed_summary.is_some() {
let [main, bar] =
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(patterns_area);
(main, Some(bar))
} else {
(patterns_area, None)
};
render_patterns(frame, app, snapshot, patterns_main);
if let (Some(bar_area), Some(summary)) = (launch_bar_area, armed_summary) {
let pulse = pulse_value(app.ui.pulse_phase);
let pulsed_fg = pulse_color(theme.list.staged_play_fg, theme.list.staged_play_bg, pulse * 0.6);
let text = format!("\u{25b6} {summary} \u{2014} c to launch");
let bar = Paragraph::new(text)
.alignment(Alignment::Center)
.style(
Style::new()
.fg(pulsed_fg)
.bg(theme.list.staged_play_bg)
.add_modifier(Modifier::BOLD),
);
frame.render_widget(bar, bar_area);
}
let bank = app.patterns_nav.bank_cursor;
let pattern_idx = app.patterns_nav.pattern_cursor;
@@ -82,6 +121,7 @@ pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area:
fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
let theme = theme::get();
let pulse = pulse_value(app.ui.pulse_phase);
let is_focused = matches!(app.patterns_nav.column, PatternsColumn::Banks);
let border_color = if is_focused { theme.ui.header } else { theme.ui.border };
@@ -215,6 +255,12 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
} else {
style
};
let style = if (is_staged || has_staged_mute_solo) && !is_cursor && !is_in_range {
let pulsed = pulse_color(fg, bg, pulse * 0.6);
style.fg(pulsed)
} else {
style
};
let bg_block = Block::default().style(Style::new().bg(bg));
frame.render_widget(bg_block, row_area);
@@ -247,6 +293,7 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
use crate::model::PatternSpeed;
let pulse = pulse_value(app.ui.pulse_phase);
let theme = theme::get();
let is_focused = matches!(app.patterns_nav.column, PatternsColumn::Patterns);
@@ -454,6 +501,14 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
};
let dim_style = base_style.remove_modifier(Modifier::BOLD);
let is_armed = is_staged_play || is_staged_stop || has_staged_mute || has_staged_solo || has_staged_props;
let (name_style, dim_style) = if is_armed && !is_cursor && !is_in_range {
let pulsed = pulse_color(fg, bg, pulse * 0.6);
(name_style.fg(pulsed), dim_style.fg(pulsed))
} else {
(name_style, dim_style)
};
let mut spans = vec![Span::styled(format!("{}{:02}", prefix, idx + 1), name_style)];
if !name.is_empty() {
spans.push(Span::styled(format!(" {name}"), name_style));