A ton of bug fixes

This commit is contained in:
2026-01-28 01:09:23 +01:00
parent a9ce70d292
commit 322885b908
13 changed files with 400 additions and 130 deletions

View File

@@ -45,7 +45,7 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
let [devices_area, _, settings_area, _, samples_area] = Layout::vertical([
Constraint::Length(devices_height),
Constraint::Length(1),
Constraint::Length(7),
Constraint::Length(8),
Constraint::Length(1),
Constraint::Min(6),
])
@@ -106,7 +106,11 @@ fn truncate_name(name: &str, max_len: usize) -> String {
fn list_height(item_count: usize) -> u16 {
let visible = item_count.min(5) as u16;
if item_count > 5 { visible + 1 } else { visible }
if item_count > 5 {
visible + 1
} else {
visible
}
}
fn devices_section_height(app: &App) -> u16 {
@@ -116,10 +120,8 @@ fn devices_section_height(app: &App) -> u16 {
}
fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Rect) {
let [header_area, divider_area] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
]).areas(area);
let [header_area, divider_area] =
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area);
let header_style = if focused {
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
@@ -139,10 +141,8 @@ fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Re
fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
let section_focused = app.audio.section == EngineSection::Devices;
let [header_area, content_area] = Layout::vertical([
Constraint::Length(2),
Constraint::Min(1),
]).areas(area);
let [header_area, content_area] =
Layout::vertical([Constraint::Length(2), Constraint::Min(1)]).areas(area);
render_section_header(frame, "DEVICES", section_focused, header_area);
@@ -150,14 +150,17 @@ fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
Constraint::Percentage(48),
Constraint::Length(3),
Constraint::Percentage(48),
]).areas(content_area);
])
.areas(content_area);
let output_focused = section_focused && app.audio.device_kind == DeviceKind::Output;
let input_focused = section_focused && app.audio.device_kind == DeviceKind::Input;
render_device_column(
frame, output_col,
"Output", &app.audio.output_devices,
frame,
output_col,
"Output",
&app.audio.output_devices,
app.audio.current_output_device_index(),
app.audio.output_list.cursor,
app.audio.output_list.scroll_offset,
@@ -172,8 +175,10 @@ fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
frame.render_widget(Paragraph::new(sep_lines), separator);
render_device_column(
frame, input_col,
"Input", &app.audio.input_devices,
frame,
input_col,
"Input",
&app.audio.input_devices,
app.audio.current_input_device_index(),
app.audio.input_list.cursor,
app.audio.input_list.scroll_offset,
@@ -193,10 +198,8 @@ fn render_device_column(
focused: bool,
section_focused: bool,
) {
let [label_area, list_area] = Layout::vertical([
Constraint::Length(1),
Constraint::Min(1),
]).areas(area);
let [label_area, list_area] =
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);
let label_style = if focused {
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
@@ -212,9 +215,7 @@ fn render_device_column(
label_area,
);
let items: Vec<String> = devices.iter()
.map(|d| truncate_name(&d.name, 25))
.collect();
let items: Vec<String> = devices.iter().map(|d| truncate_name(&d.name, 25)).collect();
ListSelect::new(&items, selected_idx, cursor)
.focused(focused)
@@ -238,10 +239,25 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
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;
let nudge_label = if nudge_ms == 0.0 {
"0 ms".to_string()
} else {
format!("{nudge_ms:+.1} ms")
};
let rows = vec![
Row::new(vec![
Span::styled(if channels_focused { "> Channels" } else { " Channels" }, label_style),
Span::styled(
if channels_focused {
"> Channels"
} else {
" Channels"
},
label_style,
),
render_selector(
&format!("{}", app.audio.config.channels),
channels_focused,
@@ -250,7 +266,14 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
),
]),
Row::new(vec![
Span::styled(if buffer_focused { "> Buffer" } else { " Buffer" }, label_style),
Span::styled(
if buffer_focused {
"> Buffer"
} else {
" Buffer"
},
label_style,
),
render_selector(
&format!("{}", app.audio.config.buffer_size),
buffer_focused,
@@ -259,7 +282,14 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
),
]),
Row::new(vec![
Span::styled(if polyphony_focused { "> Voices" } else { " Voices" }, label_style),
Span::styled(
if polyphony_focused {
"> Voices"
} else {
" Voices"
},
label_style,
),
render_selector(
&format!("{}", app.audio.config.max_voices),
polyphony_focused,
@@ -267,6 +297,13 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
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(

View File

@@ -1,9 +1,10 @@
use minimad::{Composite, CompositeStyle, Compound, Line};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::style::{Color, Modifier, Style, Stylize};
use ratatui::text::{Line as RLine, Span};
use ratatui::widgets::{Block, Borders, List, ListItem, Padding, Paragraph, Wrap};
use ratatui::Frame;
use tui_big_text::{BigText, PixelSize};
use crate::app::App;
use crate::views::highlight;
@@ -11,8 +12,10 @@ use crate::views::highlight;
// To add a new help topic: drop a .md file in docs/ and add one line here.
const DOCS: &[(&str, &str)] = &[
("Welcome", include_str!("../../docs/welcome.md")),
("Audio Engine", include_str!("../../docs/audio_engine.md")),
("Keybindings", include_str!("../../docs/keybindings.md")),
("Sequencer", include_str!("../../docs/sequencer.md")),
("About", include_str!("../../docs/about.md")),
];
pub fn topic_count() -> usize {
@@ -47,8 +50,36 @@ fn render_topics(frame: &mut Frame, app: &App, area: Rect) {
frame.render_widget(list, area);
}
const WELCOME_TOPIC: usize = 0;
const BIG_TITLE_HEIGHT: u16 = 6;
fn render_content(frame: &mut Frame, app: &App, area: Rect) {
let (name, md) = DOCS[app.ui.help_topic];
let (_, md) = DOCS[app.ui.help_topic];
let is_welcome = app.ui.help_topic == WELCOME_TOPIC;
let md_area = if is_welcome {
let [title_area, rest] =
Layout::vertical([Constraint::Length(BIG_TITLE_HEIGHT), Constraint::Fill(1)])
.areas(area);
let big_title = BigText::builder()
.pixel_size(PixelSize::Quadrant)
.style(Style::new().cyan().bold())
.lines(vec!["CAGIRE".into()])
.centered()
.build();
let subtitle = Paragraph::new(RLine::from(Span::styled(
"A Forth Sequencer",
Style::new().fg(Color::White),
)))
.alignment(ratatui::layout::Alignment::Center);
let [big_area, subtitle_area] =
Layout::vertical([Constraint::Length(4), Constraint::Length(2)]).areas(title_area);
frame.render_widget(big_title, big_area);
frame.render_widget(subtitle, subtitle_area);
rest
} else {
area
};
let query = &app.ui.help_search_query;
let has_query = !query.is_empty();
let query_lower = query.to_lowercase();
@@ -57,7 +88,7 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
let has_search_bar = app.ui.help_search_active || has_query;
let search_bar_height: u16 = u16::from(has_search_bar);
let visible_height = area.height.saturating_sub(6 + search_bar_height) as usize;
let visible_height = md_area.height.saturating_sub(6 + search_bar_height) as usize;
let max_scroll = lines.len().saturating_sub(visible_height);
let scroll = app.ui.help_scroll().min(max_scroll);
@@ -76,18 +107,17 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
let content_area = if has_search_bar {
let [content, search] =
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area);
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(md_area);
render_search_bar(frame, app, search);
content
} else {
area
md_area
};
let para = Paragraph::new(visible)
.block(
Block::default()
.borders(Borders::ALL)
.title(name)
.padding(Padding::new(2, 2, 2, 2)),
)
.wrap(Wrap { trim: false });
@@ -158,8 +188,52 @@ fn code_border_style() -> Style {
Style::new().fg(Color::Rgb(60, 60, 70))
}
fn preprocess_underscores(md: &str) -> String {
let mut out = String::with_capacity(md.len());
for line in md.lines() {
let mut result = String::with_capacity(line.len());
let mut chars = line.char_indices().peekable();
let bytes = line.as_bytes();
while let Some((i, c)) = chars.next() {
if c == '`' {
result.push(c);
for (_, ch) in chars.by_ref() {
result.push(ch);
if ch == '`' {
break;
}
}
continue;
}
if c == '_' {
let before_is_space = i == 0 || bytes[i - 1] == b' ';
if before_is_space {
if let Some(end) = line[i + 1..].find('_') {
let inner = &line[i + 1..i + 1 + end];
if !inner.is_empty() {
result.push('*');
result.push_str(inner);
result.push('*');
for _ in 0..end {
chars.next();
}
chars.next(); // skip closing _
continue;
}
}
}
}
result.push(c);
}
out.push_str(&result);
out.push('\n');
}
out
}
fn parse_markdown(md: &str) -> Vec<RLine<'static>> {
let text = minimad::Text::from(md);
let processed = preprocess_underscores(md);
let text = minimad::Text::from(processed.as_str());
let mut lines = Vec::new();
let mut code_line_nr: usize = 0;
@@ -218,13 +292,13 @@ fn composite_to_line(composite: Composite) -> RLine<'static> {
}
for compound in composite.compounds {
spans.push(compound_to_span(compound, base_style));
compound_to_spans(compound, base_style, &mut spans);
}
RLine::from(spans)
}
fn compound_to_span(compound: Compound, base: Style) -> Span<'static> {
fn compound_to_spans(compound: Compound, base: Style, out: &mut Vec<Span<'static>>) {
let mut style = base;
if compound.bold {
@@ -240,5 +314,39 @@ fn compound_to_span(compound: Compound, base: Style) -> Span<'static> {
style = style.add_modifier(Modifier::CROSSED_OUT);
}
Span::styled(compound.src.to_string(), style)
let src = compound.src.to_string();
let link_style = Style::new()
.fg(Color::Rgb(120, 200, 180))
.add_modifier(Modifier::UNDERLINED);
let mut rest = src.as_str();
while let Some(start) = rest.find('[') {
let after_bracket = &rest[start + 1..];
if let Some(text_end) = after_bracket.find("](") {
let url_start = start + 1 + text_end + 2;
if let Some(url_end) = rest[url_start..].find(')') {
if start > 0 {
out.push(Span::styled(rest[..start].to_string(), style));
}
let text = &rest[start + 1..start + 1 + text_end];
let url = &rest[url_start..url_start + url_end];
if text == url {
out.push(Span::styled(url.to_string(), link_style));
} else {
out.push(Span::styled(text.to_string(), link_style));
out.push(Span::styled(
format!(" ({url})"),
Style::new().fg(Color::Rgb(100, 100, 100)),
));
}
rest = &rest[url_start + url_end + 1..];
continue;
}
}
out.push(Span::styled(rest[..start + 1].to_string(), style));
rest = &rest[start + 1..];
}
if !rest.is_empty() {
out.push(Span::styled(rest.to_string(), style));
}
}