Monster commit: native version

This commit is contained in:
2026-01-30 15:03:49 +01:00
parent c2e6dfe88b
commit 44d1e9af24
35 changed files with 1491 additions and 366 deletions

View File

@@ -18,6 +18,7 @@ use crate::engine::{LinkState, SequencerSnapshot};
use crate::model::{SourceSpan, StepContext, Value};
use crate::page::Page;
use crate::state::{FlashKind, Modal, PanelFocus, PatternField, SidePanel, StackCache};
use crate::theme::{browser, flash, header, hint, modal, search, status, table, ui};
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
use crate::widgets::{
ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal,
@@ -132,10 +133,15 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &Sequenc
let term = frame.area();
let bg_color = if app.ui.event_flash > 0.0 {
let i = (app.ui.event_flash * app.ui.flash_brightness * 60.0) as u8;
Color::Rgb(i, i, i)
let t = (app.ui.event_flash * app.ui.flash_brightness).min(1.0);
let (base_r, base_g, base_b) = ui::BG_RGB;
let (tgt_r, tgt_g, tgt_b) = flash::EVENT_RGB;
let r = base_r + ((tgt_r as f32 - base_r as f32) * t) as u8;
let g = base_g + ((tgt_g as f32 - base_g as f32) * t) as u8;
let b = base_b + ((tgt_b as f32 - base_b as f32) * t) as u8;
Color::Rgb(r, g, b)
} else {
Color::Reset
ui::BG
};
let blank = " ".repeat(term.width as usize);
@@ -294,11 +300,11 @@ fn render_header(
// Transport block
let (transport_bg, transport_text) = if app.playback.playing {
(Color::Rgb(30, 80, 30), " ▶ PLAYING ")
(status::PLAYING_BG, " ▶ PLAYING ")
} else {
(Color::Rgb(80, 30, 30), " ■ STOPPED ")
(status::STOPPED_BG, " ■ STOPPED ")
};
let transport_style = Style::new().bg(transport_bg).fg(Color::White);
let transport_style = Style::new().bg(transport_bg).fg(ui::TEXT_PRIMARY);
frame.render_widget(
Paragraph::new(transport_text)
.style(transport_style)
@@ -308,15 +314,8 @@ fn render_header(
// Fill indicator
let fill = app.live_keys.fill();
let fill_style = if fill {
Style::new()
.bg(Color::Rgb(30, 30, 35))
.fg(Color::Rgb(100, 220, 100))
} else {
Style::new()
.bg(Color::Rgb(30, 30, 35))
.fg(Color::Rgb(60, 60, 70))
};
let fill_fg = if fill { status::FILL_ON } else { status::FILL_OFF };
let fill_style = Style::new().bg(status::FILL_BG).fg(fill_fg);
frame.render_widget(
Paragraph::new(if fill { "F" } else { "·" })
.style(fill_style)
@@ -326,8 +325,8 @@ fn render_header(
// Tempo block
let tempo_style = Style::new()
.bg(Color::Rgb(60, 30, 60))
.fg(Color::White)
.bg(header::TEMPO_BG)
.fg(ui::TEXT_PRIMARY)
.add_modifier(Modifier::BOLD);
frame.render_widget(
Paragraph::new(format!(" {:.1} BPM ", link.tempo()))
@@ -342,7 +341,7 @@ fn render_header(
.as_deref()
.map(|n| format!(" {n} "))
.unwrap_or_else(|| format!(" Bank {:02} ", app.editor_ctx.bank + 1));
let bank_style = Style::new().bg(Color::Rgb(30, 60, 70)).fg(Color::White);
let bank_style = Style::new().bg(header::BANK_BG).fg(ui::TEXT_PRIMARY);
frame.render_widget(
Paragraph::new(bank_name)
.style(bank_style)
@@ -373,7 +372,7 @@ fn render_header(
" {} · {} steps{}{}{} ",
pattern_name, pattern.length, speed_info, page_info, iter_info
);
let pattern_style = Style::new().bg(Color::Rgb(30, 50, 50)).fg(Color::White);
let pattern_style = Style::new().bg(header::PATTERN_BG).fg(ui::TEXT_PRIMARY);
frame.render_widget(
Paragraph::new(pattern_text)
.style(pattern_style)
@@ -386,9 +385,7 @@ fn render_header(
let peers = link.peers();
let voices = app.metrics.active_voices;
let stats_text = format!(" CPU {cpu_pct:.0}% V:{voices} L:{peers} ");
let stats_style = Style::new()
.bg(Color::Rgb(35, 35, 40))
.fg(Color::Rgb(150, 150, 160));
let stats_style = Style::new().bg(header::STATS_BG).fg(header::STATS_FG);
frame.render_widget(
Paragraph::new(stats_text)
.style(stats_style)
@@ -415,10 +412,10 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
Line::from(vec![
Span::styled(
page_indicator.to_string(),
Style::new().fg(Color::White).add_modifier(Modifier::DIM),
Style::new().fg(ui::TEXT_PRIMARY).add_modifier(Modifier::DIM),
),
Span::raw(" "),
Span::styled(msg.clone(), Style::new().fg(Color::Yellow)),
Span::styled(msg.clone(), Style::new().fg(modal::CONFIRM)),
])
} else {
let bindings: Vec<(&str, &str)> = match app.page {
@@ -480,7 +477,7 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
let mut spans = vec![
Span::styled(
page_indicator.to_string(),
Style::new().fg(Color::White).add_modifier(Modifier::DIM),
Style::new().fg(ui::TEXT_PRIMARY).add_modifier(Modifier::DIM),
),
Span::raw(" ".repeat(base_gap + if extra > 0 { 1 } else { 0 })),
];
@@ -488,11 +485,11 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
for (i, (key, action)) in bindings.into_iter().enumerate() {
spans.push(Span::styled(
key.to_string(),
Style::new().fg(Color::Yellow),
Style::new().fg(hint::KEY),
));
spans.push(Span::styled(
format!(":{action}"),
Style::new().fg(Color::Rgb(120, 125, 135)),
Style::new().fg(hint::TEXT),
));
if i < n - 1 {
@@ -542,8 +539,8 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
use crate::state::file_browser::FileBrowserMode;
use crate::widgets::FileBrowserModal;
let (title, border_color) = match state.mode {
FileBrowserMode::Save => ("Save As", Color::Green),
FileBrowserMode::Load => ("Load From", Color::Blue),
FileBrowserMode::Save => ("Save As", flash::SUCCESS_FG),
FileBrowserMode::Load => ("Load From", browser::DIRECTORY),
};
let entries: Vec<(String, bool, bool)> = state
.entries
@@ -561,7 +558,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
Modal::RenameBank { bank, name } => {
TextInputModal::new(&format!("Rename Bank {:02}", bank + 1), name)
.width(40)
.border_color(Color::Magenta)
.border_color(modal::RENAME)
.render_centered(frame, term);
}
Modal::RenamePattern {
@@ -574,13 +571,13 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
name,
)
.width(40)
.border_color(Color::Magenta)
.border_color(modal::RENAME)
.render_centered(frame, term);
}
Modal::RenameStep { step, name, .. } => {
TextInputModal::new(&format!("Name Step {:02}", step + 1), name)
.width(40)
.border_color(Color::Cyan)
.border_color(modal::INPUT)
.render_centered(frame, term);
}
Modal::SetPattern { field, input } => {
@@ -591,14 +588,14 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
TextInputModal::new(title, input)
.hint(hint)
.width(45)
.border_color(Color::Yellow)
.border_color(modal::CONFIRM)
.render_centered(frame, term);
}
Modal::SetTempo(input) => {
TextInputModal::new("Set Tempo (20-300 BPM)", input)
.hint("Enter BPM")
.width(30)
.border_color(Color::Magenta)
.border_color(modal::RENAME)
.render_centered(frame, term);
}
Modal::AddSamplePath(state) => {
@@ -611,7 +608,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
FileBrowserModal::new("Add Sample Path", &state.input, &entries)
.selected(state.selected)
.scroll_offset(state.scroll_offset)
.border_color(Color::Magenta)
.border_color(modal::RENAME)
.width(60)
.height(18)
.render_centered(frame, term);
@@ -636,14 +633,14 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let inner = ModalFrame::new(&title)
.width(width)
.height(height)
.border_color(Color::Rgb(120, 125, 135))
.border_color(modal::PREVIEW)
.render_centered(frame, term);
let script = pattern.resolve_script(step_idx).unwrap_or("");
if script.is_empty() {
let empty = Paragraph::new("(empty)")
.alignment(Alignment::Center)
.style(Style::new().fg(Color::Rgb(80, 85, 95)));
.style(Style::new().fg(ui::TEXT_DIM));
let centered_area = Rect {
y: inner.y + inner.height / 2,
height: 1,
@@ -698,10 +695,10 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let flash_kind = app.ui.flash_kind();
let border_color = match flash_kind {
Some(FlashKind::Error) => Color::Red,
Some(FlashKind::Info) => Color::White,
Some(FlashKind::Success) => Color::Green,
None => Color::Rgb(100, 160, 180),
Some(FlashKind::Error) => flash::ERROR_FG,
Some(FlashKind::Info) => ui::TEXT_PRIMARY,
Some(FlashKind::Success) => flash::SUCCESS_FG,
None => modal::EDITOR,
};
let title = if let Some(ref name) = step.and_then(|s| s.name.as_ref()) {
@@ -768,9 +765,9 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
if let Some(sa) = search_area {
let style = if app.editor_ctx.editor.search_active() {
Style::default().fg(Color::Yellow)
Style::default().fg(search::ACTIVE)
} else {
Style::default().fg(Color::DarkGray)
Style::default().fg(search::INACTIVE)
};
let cursor = if app.editor_ctx.editor.search_active() {
"_"
@@ -783,9 +780,9 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
if let Some(kind) = flash_kind {
let bg = match kind {
FlashKind::Error => Color::Rgb(60, 10, 10),
FlashKind::Info => Color::Rgb(30, 30, 40),
FlashKind::Success => Color::Rgb(10, 30, 10),
FlashKind::Error => flash::ERROR_BG,
FlashKind::Info => flash::INFO_BG,
FlashKind::Success => flash::SUCCESS_BG,
};
let flash_block = Block::default().style(Style::default().bg(bg));
frame.render_widget(flash_block, editor_area);
@@ -794,8 +791,8 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
.editor
.render(frame, editor_area, &highlighter);
let dim = Style::default().fg(Color::DarkGray);
let key = Style::default().fg(Color::Yellow);
let dim = Style::default().fg(hint::TEXT);
let key = Style::default().fg(hint::KEY);
if app.editor_ctx.editor.search_active() {
let hint = Line::from(vec![
@@ -863,7 +860,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let block = Block::bordered()
.title(format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1))
.border_style(Style::default().fg(Color::Cyan));
.border_style(Style::default().fg(modal::INPUT));
let inner = block.inner(area);
frame.render_widget(Clear, area);
@@ -898,14 +895,14 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let (label_style, value_style) = if *selected {
(
Style::default()
.fg(Color::Cyan)
.fg(hint::KEY)
.add_modifier(Modifier::BOLD),
Style::default().fg(Color::White).bg(Color::DarkGray),
Style::default().fg(ui::TEXT_PRIMARY).bg(ui::SURFACE),
)
} else {
(
Style::default().fg(Color::Gray),
Style::default().fg(Color::White),
Style::default().fg(ui::TEXT_MUTED),
Style::default().fg(ui::TEXT_PRIMARY),
)
};
@@ -920,17 +917,17 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
}
let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1);
let hint = Line::from(vec![
Span::styled("↑↓", Style::default().fg(Color::Yellow)),
Span::styled(" nav ", Style::default().fg(Color::DarkGray)),
Span::styled("←→", Style::default().fg(Color::Yellow)),
Span::styled(" change ", Style::default().fg(Color::DarkGray)),
Span::styled("Enter", Style::default().fg(Color::Yellow)),
Span::styled(" save ", Style::default().fg(Color::DarkGray)),
Span::styled("Esc", Style::default().fg(Color::Yellow)),
Span::styled(" cancel", Style::default().fg(Color::DarkGray)),
let hint_line = Line::from(vec![
Span::styled("↑↓", Style::default().fg(hint::KEY)),
Span::styled(" nav ", Style::default().fg(hint::TEXT)),
Span::styled("←→", Style::default().fg(hint::KEY)),
Span::styled(" change ", Style::default().fg(hint::TEXT)),
Span::styled("Enter", Style::default().fg(hint::KEY)),
Span::styled(" save ", Style::default().fg(hint::TEXT)),
Span::styled("Esc", Style::default().fg(hint::KEY)),
Span::styled(" cancel", Style::default().fg(hint::TEXT)),
]);
frame.render_widget(Paragraph::new(hint), hint_area);
frame.render_widget(Paragraph::new(hint_line), hint_area);
}
Modal::KeybindingsHelp { scroll } => {
let width = (term.width * 80 / 100).clamp(60, 100);
@@ -940,7 +937,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let inner = ModalFrame::new(&title)
.width(width)
.height(height)
.border_color(Color::Rgb(100, 160, 180))
.border_color(modal::EDITOR)
.render_centered(frame, term);
let bindings = super::keybindings::bindings_for(app.page);
@@ -952,15 +949,11 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
.skip(*scroll)
.take(visible_rows)
.map(|(i, (key, name, desc))| {
let bg = if i % 2 == 0 {
Color::Rgb(25, 25, 30)
} else {
Color::Rgb(35, 35, 42)
};
let bg = if i % 2 == 0 { table::ROW_EVEN } else { table::ROW_ODD };
Row::new(vec![
Cell::from(*key).style(Style::default().fg(Color::Yellow)),
Cell::from(*name).style(Style::default().fg(Color::Cyan)),
Cell::from(*desc).style(Style::default().fg(Color::White)),
Cell::from(*key).style(Style::default().fg(modal::CONFIRM)),
Cell::from(*name).style(Style::default().fg(modal::INPUT)),
Cell::from(*desc).style(Style::default().fg(ui::TEXT_PRIMARY)),
])
.style(Style::default().bg(bg))
})
@@ -990,15 +983,15 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
width: inner.width,
height: 1,
};
let hint = Line::from(vec![
Span::styled("↑↓", Style::default().fg(Color::Yellow)),
Span::styled(" scroll ", Style::default().fg(Color::DarkGray)),
Span::styled("PgUp/Dn", Style::default().fg(Color::Yellow)),
Span::styled(" page ", Style::default().fg(Color::DarkGray)),
Span::styled("Esc/?", Style::default().fg(Color::Yellow)),
Span::styled(" close", Style::default().fg(Color::DarkGray)),
let keybind_hint = Line::from(vec![
Span::styled("↑↓", Style::default().fg(hint::KEY)),
Span::styled(" scroll ", Style::default().fg(hint::TEXT)),
Span::styled("PgUp/Dn", Style::default().fg(hint::KEY)),
Span::styled(" page ", Style::default().fg(hint::TEXT)),
Span::styled("Esc/?", Style::default().fg(hint::KEY)),
Span::styled(" close", Style::default().fg(hint::TEXT)),
]);
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
frame.render_widget(Paragraph::new(keybind_hint).alignment(Alignment::Right), hint_area);
}
}
}