WIP: clap

This commit is contained in:
2026-02-20 22:14:21 +01:00
parent 12752e0167
commit 00d6eb2f1f
76 changed files with 9103 additions and 143 deletions

View File

@@ -46,24 +46,31 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
};
// Calculate section heights
let devices_lines = devices_section_height(app) as usize;
let settings_lines: usize = 8; // header(1) + divider(1) + 6 rows
let plugin_mode = app.plugin_mode;
let devices_lines = if plugin_mode {
0
} else {
devices_section_height(app) as usize
};
let settings_lines: usize = if plugin_mode { 5 } else { 8 }; // plugin: header(1) + divider(1) + 3 rows
let samples_lines: usize = 6; // header(1) + divider(1) + content(3) + hint(1)
let total_lines = devices_lines + 1 + settings_lines + 1 + samples_lines;
let sections_gap = if plugin_mode { 1 } else { 2 }; // 1 gap without devices, 2 gaps with
let total_lines = devices_lines + settings_lines + samples_lines + sections_gap;
let max_visible = padded.height as usize;
// Calculate scroll offset based on focused section
let settings_start = if plugin_mode { 0 } else { devices_lines + 1 };
let (focus_start, focus_height) = match app.audio.section {
EngineSection::Devices => (0, devices_lines),
EngineSection::Settings => (devices_lines + 1, settings_lines),
EngineSection::Samples => (devices_lines + 1 + settings_lines + 1, samples_lines),
EngineSection::Settings => (settings_start, settings_lines),
EngineSection::Samples => (settings_start + settings_lines + 1, samples_lines),
};
let scroll_offset = if total_lines <= max_visible {
0
} else {
// Keep focused section in view (top-aligned when possible)
let focus_end = focus_start + focus_height;
if focus_end <= max_visible {
0
@@ -75,25 +82,26 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
let viewport_top = padded.y as i32;
let viewport_bottom = (padded.y + padded.height) as i32;
// Render each section at adjusted position
let mut y = viewport_top - scroll_offset as i32;
// Devices section
let devices_top = y;
let devices_bottom = y + devices_lines as i32;
if devices_bottom > viewport_top && devices_top < viewport_bottom {
let clipped_y = devices_top.max(viewport_top) as u16;
let clipped_height =
(devices_bottom.min(viewport_bottom) - devices_top.max(viewport_top)) as u16;
let devices_area = Rect {
x: padded.x,
y: clipped_y,
width: padded.width,
height: clipped_height,
};
render_devices(frame, app, devices_area);
// Devices section (skip in plugin mode)
if !plugin_mode {
let devices_top = y;
let devices_bottom = y + devices_lines as i32;
if devices_bottom > viewport_top && devices_top < viewport_bottom {
let clipped_y = devices_top.max(viewport_top) as u16;
let clipped_height =
(devices_bottom.min(viewport_bottom) - devices_top.max(viewport_top)) as u16;
let devices_area = Rect {
x: padded.x,
y: clipped_y,
width: padded.width,
height: clipped_height,
};
render_devices(frame, app, devices_area);
}
y += devices_lines as i32 + 1;
}
y += devices_lines as i32 + 1; // +1 for blank line
// Settings section
let settings_top = y;
@@ -310,8 +318,6 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
let label_style = Style::new().fg(theme.engine.label);
let value_style = Style::new().fg(theme.engine.value);
let channels_focused = section_focused && app.audio.setting_kind == SettingKind::Channels;
let buffer_focused = section_focused && app.audio.setting_kind == SettingKind::BufferSize;
let polyphony_focused = section_focused && app.audio.setting_kind == SettingKind::Polyphony;
let nudge_focused = section_focused && app.audio.setting_kind == SettingKind::Nudge;
let nudge_ms = app.metrics.nudge_ms;
@@ -321,8 +327,15 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
format!("{nudge_ms:+.1} ms")
};
let rows = vec![
Row::new(vec![
let mut rows = Vec::new();
if !app.plugin_mode {
let channels_focused =
section_focused && app.audio.setting_kind == SettingKind::Channels;
let buffer_focused =
section_focused && app.audio.setting_kind == SettingKind::BufferSize;
rows.push(Row::new(vec![
Span::styled(
if channels_focused {
"> Channels"
@@ -337,8 +350,8 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
highlight,
normal,
),
]),
Row::new(vec![
]));
rows.push(Row::new(vec![
Span::styled(
if buffer_focused {
"> Buffer"
@@ -357,38 +370,42 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
highlight,
normal,
),
]),
Row::new(vec![
Span::styled(
if polyphony_focused {
"> Voices"
} else {
" Voices"
},
label_style,
),
render_selector(
&format!("{}", app.audio.config.max_voices),
polyphony_focused,
highlight,
normal,
),
]),
Row::new(vec![
Span::styled(
if nudge_focused { "> Nudge" } else { " Nudge" },
label_style,
),
render_selector(&nudge_label, nudge_focused, highlight, normal),
]),
Row::new(vec![
Span::styled(" Sample rate", label_style),
Span::styled(
format!("{:.0} Hz", app.audio.config.sample_rate),
value_style,
),
]),
Row::new(vec![
]));
}
rows.push(Row::new(vec![
Span::styled(
if polyphony_focused {
"> Voices"
} else {
" Voices"
},
label_style,
),
render_selector(
&format!("{}", app.audio.config.max_voices),
polyphony_focused,
highlight,
normal,
),
]));
rows.push(Row::new(vec![
Span::styled(
if nudge_focused { "> Nudge" } else { " Nudge" },
label_style,
),
render_selector(&nudge_label, nudge_focused, highlight, normal),
]));
rows.push(Row::new(vec![
Span::styled(" Sample rate", label_style),
Span::styled(
format!("{:.0} Hz", app.audio.config.sample_rate),
value_style,
),
]));
if !app.plugin_mode {
rows.push(Row::new(vec![
Span::styled(" Audio host", label_style),
Span::styled(
if app.audio.config.host_name.is_empty() {
@@ -398,8 +415,8 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
},
value_style,
),
]),
];
]));
}
let table = Table::new(rows, [Constraint::Length(14), Constraint::Fill(1)]);
frame.render_widget(table, content_area);

View File

@@ -1,14 +1,18 @@
use crate::page::Page;
pub fn bindings_for(page: Page) -> Vec<(&'static str, &'static str, &'static str)> {
pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'static str, &'static str)> {
let mut bindings = vec![
("F1F6", "Go to view", "Dict/Patterns/Options/Help/Sequencer/Engine"),
("Ctrl+←→↑↓", "Navigate", "Switch between adjacent views"),
("q", "Quit", "Quit application"),
];
if !plugin_mode {
bindings.push(("q", "Quit", "Quit application"));
}
bindings.extend([
("s", "Save", "Save project"),
("l", "Load", "Load project"),
("?", "Keybindings", "Show this help"),
];
]);
// Page-specific bindings
match page {

View File

@@ -78,7 +78,7 @@ pub fn render(
frame.render_widget(Block::new().style(Style::default().bg(bg_color)), term);
if app.ui.show_title {
title_view::render(frame, term, &app.ui);
title_view::render(frame, term, &app.ui, app.plugin_mode);
let mut fx = app.ui.title_fx.borrow_mut();
if let Some(effect) = fx.as_mut() {
@@ -1036,7 +1036,7 @@ fn render_modal_keybindings(frame: &mut Frame, app: &App, scroll: usize, term: R
.border_color(theme.modal.editor)
.render_centered(frame, term);
let bindings = super::keybindings::bindings_for(app.page);
let bindings = super::keybindings::bindings_for(app.page, app.plugin_mode);
let visible_rows = inner.height.saturating_sub(2) as usize;
let rows: Vec<Row> = bindings

View File

@@ -8,7 +8,7 @@ use tui_big_text::{BigText, PixelSize};
use crate::state::ui::UiState;
use crate::theme;
pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) {
pub fn render(frame: &mut Frame, area: Rect, ui: &UiState, plugin_mode: bool) {
let theme = theme::get();
frame.render_widget(&ui.sparkles, area);
@@ -39,15 +39,17 @@ pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) {
Line::from(Span::styled("AGPL-3.0", license_style)),
];
let keybindings = [
let mut keybindings = vec![
("Ctrl+Arrows", "Navigate Views"),
("Enter", "Edit Step"),
("Space", "Play/Stop"),
("s", "Save"),
("l", "Load"),
("q", "Quit"),
("?", "Keybindings"),
];
if !plugin_mode {
keybindings.push(("q", "Quit"));
}
keybindings.push(("?", "Keybindings"));
let key_style = Style::new().fg(theme.modal.confirm);
let desc_style = Style::new().fg(theme.ui.text_primary);