Fix: UI/UX
Some checks failed
CI / check (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Failing after 1m28s
Deploy Website / deploy (push) Has been skipped
CI / check (macos-14, aarch64-apple-darwin) (push) Has been cancelled
CI / check (windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
Some checks failed
CI / check (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Failing after 1m28s
Deploy Website / deploy (push) Has been skipped
CI / check (macos-14, aarch64-apple-darwin) (push) Has been cancelled
CI / check (windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
This commit is contained in:
@@ -48,6 +48,7 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
|
||||
};
|
||||
|
||||
// Calculate section heights
|
||||
let intro_lines: usize = 3;
|
||||
let plugin_mode = app.plugin_mode;
|
||||
let devices_lines = if plugin_mode {
|
||||
0
|
||||
@@ -55,17 +56,19 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
|
||||
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 sample_content = app.audio.config.sample_paths.len().max(2); // at least 2 for empty message
|
||||
let samples_lines: usize = 2 + sample_content; // header(2) + content
|
||||
|
||||
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 total_lines = intro_lines + 1 + 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 intro_offset = intro_lines + 1;
|
||||
let settings_start = if plugin_mode { intro_offset } else { intro_offset + devices_lines + 1 };
|
||||
let (focus_start, focus_height) = match app.audio.section {
|
||||
EngineSection::Devices => (0, devices_lines),
|
||||
EngineSection::Devices => (intro_offset, devices_lines),
|
||||
EngineSection::Settings => (settings_start, settings_lines),
|
||||
EngineSection::Samples => (settings_start + settings_lines + 1, samples_lines),
|
||||
};
|
||||
@@ -86,6 +89,29 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
|
||||
|
||||
let mut y = viewport_top - scroll_offset as i32;
|
||||
|
||||
// Intro text
|
||||
let intro_top = y;
|
||||
let intro_bottom = y + intro_lines as i32;
|
||||
if intro_bottom > viewport_top && intro_top < viewport_bottom {
|
||||
let clipped_y = intro_top.max(viewport_top) as u16;
|
||||
let clipped_height =
|
||||
(intro_bottom.min(viewport_bottom) - intro_top.max(viewport_top)) as u16;
|
||||
let intro_area = Rect {
|
||||
x: padded.x,
|
||||
y: clipped_y,
|
||||
width: padded.width,
|
||||
height: clipped_height,
|
||||
};
|
||||
let dim = Style::new().fg(theme.engine.dim);
|
||||
let intro = Paragraph::new(vec![
|
||||
Line::from(Span::styled(" Audio devices, settings, and sample paths.", dim)),
|
||||
Line::from(Span::styled(" Supports .wav, .ogg, .mp3 samples and .sf2 soundfonts.", dim)),
|
||||
Line::from(Span::styled(" Press R to restart the audio engine after changes.", dim)),
|
||||
]);
|
||||
frame.render_widget(intro, intro_area);
|
||||
}
|
||||
y += intro_lines as i32 + 1;
|
||||
|
||||
// Devices section (skip in plugin mode)
|
||||
if !plugin_mode {
|
||||
let devices_top = y;
|
||||
@@ -495,21 +521,26 @@ fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let theme = theme::get();
|
||||
let section_focused = app.audio.section == EngineSection::Samples;
|
||||
|
||||
let [header_area, content_area, _, hint_area] = Layout::vertical([
|
||||
let [header_area, content_area] = Layout::vertical([
|
||||
Constraint::Length(2),
|
||||
Constraint::Min(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
let path_count = app.audio.config.sample_paths.len();
|
||||
let sample_count = app.audio.config.sample_count;
|
||||
let sample_count: usize = app.audio.total_sample_count();
|
||||
let header_text = format!("SAMPLES {path_count} paths · {sample_count} indexed");
|
||||
render_section_header(frame, &header_text, section_focused, header_area);
|
||||
|
||||
let dim = Style::new().fg(theme.engine.dim);
|
||||
let path_style = Style::new().fg(theme.engine.path);
|
||||
let cursor_style = Style::new()
|
||||
.fg(theme.engine.focused)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
let cursor = app.audio.sample_list.cursor;
|
||||
let scroll_offset = app.audio.sample_list.scroll_offset;
|
||||
let visible_rows = content_area.height as usize;
|
||||
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
if app.audio.config.sample_paths.is_empty() {
|
||||
@@ -522,35 +553,32 @@ fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
|
||||
dim,
|
||||
)));
|
||||
} else {
|
||||
for (i, path) in app.audio.config.sample_paths.iter().take(4).enumerate() {
|
||||
for (i, path) in app
|
||||
.audio
|
||||
.config
|
||||
.sample_paths
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(scroll_offset)
|
||||
.take(visible_rows)
|
||||
{
|
||||
let is_cursor = section_focused && i == cursor;
|
||||
let prefix = if is_cursor { "> " } else { " " };
|
||||
let count = app.audio.config.sample_counts.get(i).copied().unwrap_or(0);
|
||||
let path_str = path.to_string_lossy();
|
||||
let display = truncate_name(&path_str, 40);
|
||||
let count_str = format!(" ({count})");
|
||||
let max_path = (content_area.width as usize)
|
||||
.saturating_sub(prefix.len() + count_str.len());
|
||||
let display = truncate_name(&path_str, max_path);
|
||||
let style = if is_cursor { cursor_style } else { path_style };
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!(" {} ", i + 1), dim),
|
||||
Span::styled(display, path_style),
|
||||
Span::styled(prefix.to_string(), if is_cursor { cursor_style } else { dim }),
|
||||
Span::styled(display, style),
|
||||
Span::styled(count_str, dim),
|
||||
]));
|
||||
}
|
||||
if path_count > 4 {
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" ... and {} more", path_count - 4),
|
||||
dim,
|
||||
)));
|
||||
}
|
||||
}
|
||||
frame.render_widget(Paragraph::new(lines), content_area);
|
||||
|
||||
let hint_style = if section_focused {
|
||||
Style::new().fg(theme.engine.hint_active)
|
||||
} else {
|
||||
Style::new().fg(theme.engine.hint_inactive)
|
||||
};
|
||||
let hint = Line::from(vec![
|
||||
Span::styled("A", hint_style),
|
||||
Span::styled(":add ", Style::new().fg(theme.engine.dim)),
|
||||
Span::styled("D", hint_style),
|
||||
Span::styled(":remove", Style::new().fg(theme.engine.dim)),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hint), hint_area);
|
||||
}
|
||||
|
||||
fn render_selector(value: &str, focused: bool, highlight: Style, normal: Style) -> Span<'static> {
|
||||
|
||||
@@ -551,15 +551,16 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
||||
Page::Patterns => vec![
|
||||
("Enter", "Select"),
|
||||
("Space", "Play"),
|
||||
("c", "Commit"),
|
||||
("r", "Rename"),
|
||||
("?", "Keys"),
|
||||
],
|
||||
Page::Engine => vec![
|
||||
("Tab", "Section"),
|
||||
("←→", "Switch/Adjust"),
|
||||
("Enter", "Select"),
|
||||
("A", "Add Samples"),
|
||||
("D", "Remove"),
|
||||
("R", "Restart"),
|
||||
("h", "Hush"),
|
||||
("?", "Keys"),
|
||||
],
|
||||
Page::Options => vec![
|
||||
|
||||
Reference in New Issue
Block a user