Feat: documentation

This commit is contained in:
2026-02-16 23:19:06 +01:00
parent 773c7bbd1c
commit 540f59dcf5
18 changed files with 565 additions and 227 deletions

View File

@@ -291,12 +291,14 @@ impl App {
let palette = scheme.to_palette();
let rotated = cagire_ratatui::theme::transform::rotate_palette(&palette, self.ui.hue_rotation);
crate::theme::set(rotated);
self.ui.invalidate_help_cache();
}
AppCommand::SetHueRotation(degrees) => {
self.ui.hue_rotation = degrees;
let palette = self.ui.color_scheme.to_palette();
let rotated = cagire_ratatui::theme::transform::rotate_palette(&palette, degrees);
crate::theme::set(rotated);
self.ui.invalidate_help_cache();
}
AppCommand::ToggleRuntimeHighlight => {
self.ui.runtime_highlight = !self.ui.runtime_highlight;

View File

@@ -17,10 +17,8 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
ctx.app.panel.visible = false;
ctx.app.panel.focus = PanelFocus::Main;
} else {
if ctx.app.panel.side.is_none() {
let state = SampleBrowserState::new(&ctx.app.audio.config.sample_paths);
ctx.app.panel.side = Some(SidePanel::SampleBrowser(state));
}
let state = SampleBrowserState::new(&ctx.app.audio.config.sample_paths);
ctx.app.panel.side = Some(SidePanel::SampleBrowser(state));
ctx.app.panel.visible = true;
ctx.app.panel.focus = PanelFocus::Side;
}
@@ -127,13 +125,21 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
if let Some(range) = ctx.app.editor_ctx.selection_range() {
let steps: Vec<usize> = range.collect();
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
action: ConfirmAction::DeleteSteps { bank, pattern, steps },
action: ConfirmAction::DeleteSteps {
bank,
pattern,
steps,
},
selected: false,
}));
} else {
let step = ctx.app.editor_ctx.step;
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
action: ConfirmAction::DeleteStep { bank, pattern, step },
action: ConfirmAction::DeleteStep {
bank,
pattern,
step,
},
selected: false,
}));
}
@@ -172,7 +178,11 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
.and_then(|s| s.name.clone())
.unwrap_or_default();
ctx.dispatch(AppCommand::OpenModal(Modal::Rename {
target: RenameTarget::Step { bank, pattern, step },
target: RenameTarget::Step {
bank,
pattern,
step,
},
name: current_name,
}));
}

View File

@@ -10,12 +10,12 @@ pub const DOCS: &[DocEntry] = &[
Section("Getting Started"),
Topic("Welcome", include_str!("../../docs/welcome.md")),
Topic(
"Moving Around",
"Navigation",
include_str!("../../docs/getting-started/navigation.md"),
),
Topic(
"How Does It Work?",
include_str!("../../docs/getting-started/how_it_works.md"),
"The Big Picture",
include_str!("../../docs/getting-started/big_picture.md"),
),
Topic(
"Banks & Patterns",
@@ -33,6 +33,22 @@ pub const DOCS: &[DocEntry] = &[
"Editing a Step",
include_str!("../../docs/getting-started/editing.md"),
),
Topic(
"The Audio Engine",
include_str!("../../docs/getting-started/engine.md"),
),
Topic(
"Options",
include_str!("../../docs/getting-started/options.md"),
),
Topic(
"Saving & Loading",
include_str!("../../docs/getting-started/saving.md"),
),
Topic(
"The Sample Browser",
include_str!("../../docs/getting-started/samples.md"),
),
// Forth fundamentals
Section("Forth"),
Topic(
@@ -60,10 +76,7 @@ pub const DOCS: &[DocEntry] = &[
Topic("Settings", include_str!("../../docs/engine/settings.md")),
Topic("Sources", include_str!("../../docs/engine/sources.md")),
Topic("Samples", include_str!("../../docs/engine/samples.md")),
Topic(
"Wavetables",
include_str!("../../docs/engine/wavetable.md"),
),
Topic("Wavetables", include_str!("../../docs/engine/wavetable.md")),
Topic("Filters", include_str!("../../docs/engine/filters.md")),
Topic(
"Modulation",
@@ -78,10 +91,7 @@ pub const DOCS: &[DocEntry] = &[
"Audio-Rate Mod",
include_str!("../../docs/engine/audio_modulation.md"),
),
Topic(
"Words & Sounds",
include_str!("../../docs/engine/words.md"),
),
Topic("Words & Sounds", include_str!("../../docs/engine/words.md")),
// MIDI
Section("MIDI"),
Topic("Introduction", include_str!("../../docs/midi/intro.md")),
@@ -101,10 +111,7 @@ pub const DOCS: &[DocEntry] = &[
"Generators",
include_str!("../../docs/tutorials/generators.md"),
),
Topic(
"Timing with at",
include_str!("../../docs/tutorials/at.md"),
),
Topic("Timing with at", include_str!("../../docs/tutorials/at.md")),
Topic(
"Using Variables",
include_str!("../../docs/tutorials/variables.md"),

View File

@@ -165,4 +165,8 @@ impl UiState {
pub fn dismiss_minimap(&mut self) {
self.minimap = MinimapMode::Hidden;
}
pub fn invalidate_help_cache(&self) {
self.help_parsed.borrow_mut().iter_mut().for_each(|slot| *slot = None);
}
}

View File

@@ -432,7 +432,7 @@ fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
dim,
)));
lines.push(Line::from(Span::styled(
" Add folders containing .wav files",
" Add folders containing audio files",
dim,
)));
} else {

View File

@@ -56,11 +56,19 @@ pub fn adjust_resolved_for_line(
) -> Vec<(SourceSpan, String)> {
resolved
.iter()
.filter_map(|(s, display)| clip_span(*s, line_start, line_len).map(|cs| (cs, display.clone())))
.filter_map(|(s, display)| {
clip_span(*s, line_start, line_len).map(|cs| (cs, display.clone()))
})
.collect()
}
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &SequencerSnapshot, elapsed: Duration) {
pub fn render(
frame: &mut Frame,
app: &App,
link: &LinkState,
snapshot: &SequencerSnapshot,
elapsed: Duration,
) {
let term = frame.area();
let theme = theme::get();
@@ -212,7 +220,11 @@ fn render_side_panel(frame: &mut Frame, app: &App, area: Rect) {
});
let duration = sample.total_frames as f32 / app.audio.config.sample_rate;
let ch_label = if sample.channels == 1 { "mono" } else { "stereo" };
let ch_label = if sample.channels == 1 {
"mono"
} else {
"stereo"
};
let info = Paragraph::new(format!(" {duration:.1}s · {ch_label}"))
.style(Style::new().fg(theme::get().ui.text_dim));
frame.render_widget(info, info_area);
@@ -349,7 +361,9 @@ fn render_header(
} else {
theme.header.stats_fg
};
let dim = Style::new().bg(theme.header.stats_bg).fg(theme.header.stats_fg);
let dim = Style::new()
.bg(theme.header.stats_bg)
.fg(theme.header.stats_fg);
let stats_line = Line::from(vec![
Span::styled(format!(" CPU {cpu_pct:.0}%"), dim.fg(cpu_color)),
Span::styled(format!(" V:{voices} L:{peers} "), dim),
@@ -407,9 +421,9 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
Page::Engine => vec![
("Tab", "Section"),
("←→", "Switch/Adjust"),
("↑↓", "Navigate"),
("Enter", "Select"),
("A", "Add path"),
("R", "Restart"),
("h", "Hush"),
("?", "Keys"),
],
Page::Options => vec![
@@ -484,14 +498,18 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
frame.render_widget(footer, area);
}
fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term: Rect) -> Option<Rect> {
fn render_modal(
frame: &mut Frame,
app: &App,
snapshot: &SequencerSnapshot,
term: Rect,
) -> Option<Rect> {
let theme = theme::get();
let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
let inner = match &app.ui.modal {
Modal::None => return None,
Modal::Confirm { action, selected } => {
ConfirmModal::new("Confirm", &action.message(), *selected)
.render_centered(frame, term)
ConfirmModal::new("Confirm", &action.message(), *selected).render_centered(frame, term)
}
Modal::FileBrowser(state) => {
use crate::state::file_browser::FileBrowserMode;
@@ -577,26 +595,42 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
} => {
use crate::state::PatternPropsField;
let inner =
ModalFrame::new(&format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1))
.width(50)
.height(12)
.border_color(theme.modal.input)
.render_centered(frame, term);
let inner = ModalFrame::new(&format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1))
.width(50)
.height(12)
.border_color(theme.modal.input)
.render_centered(frame, term);
let speed_label = speed.label();
let fields: Vec<(&str, &str, bool)> = vec![
("Name", name.as_str(), *field == PatternPropsField::Name),
("Length", length.as_str(), *field == PatternPropsField::Length),
(
"Length",
length.as_str(),
*field == PatternPropsField::Length,
),
("Speed", &speed_label, *field == PatternPropsField::Speed),
("Quantization", quantization.label(), *field == PatternPropsField::Quantization),
("Sync Mode", sync_mode.label(), *field == PatternPropsField::SyncMode),
(
"Quantization",
quantization.label(),
*field == PatternPropsField::Quantization,
),
(
"Sync Mode",
sync_mode.label(),
*field == PatternPropsField::SyncMode,
),
];
render_props_form(frame, inner, &fields);
let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1);
let hints = hint_line(&[("↑↓", "nav"), ("←→", "change"), ("Enter", "save"), ("Esc", "cancel")]);
let hints = hint_line(&[
("↑↓", "nav"),
("←→", "change"),
("Enter", "save"),
("Esc", "cancel"),
]);
frame.render_widget(Paragraph::new(hints), hint_area);
inner
@@ -625,7 +659,8 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
lines
};
let key_lines = keys.len() as u16;
let modal_height = (3 + desc_lines + 1 + key_lines + 2).min(term.height.saturating_sub(4));
let modal_height =
(3 + desc_lines + 1 + key_lines + 2).min(term.height.saturating_sub(4));
let title = if page_count > 1 {
format!(" {} ({}/{}) ", app.page.name(), page_idx + 1, page_count)
@@ -654,16 +689,13 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
}
let line = Line::from(vec![
Span::raw(" "),
Span::styled(
format!("{:>8}", key),
Style::new().fg(theme.hint.key),
),
Span::styled(
format!(" {action}"),
Style::new().fg(theme.hint.text),
),
Span::styled(format!("{:>8}", key), Style::new().fg(theme.hint.key)),
Span::styled(format!(" {action}"), Style::new().fg(theme.hint.text)),
]);
frame.render_widget(Paragraph::new(line), Rect::new(inner.x + 1, y, inner.width.saturating_sub(2), 1));
frame.render_widget(
Paragraph::new(line),
Rect::new(inner.x + 1, y, inner.width.saturating_sub(2), 1),
);
y += 1;
}
@@ -677,7 +709,10 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
}
hints_vec.push(("Enter", "don't show again"));
let hints = hint_line(&hints_vec);
frame.render_widget(Paragraph::new(hints).alignment(Alignment::Center), hint_area);
frame.render_widget(
Paragraph::new(hints).alignment(Alignment::Center),
hint_area,
);
inner
}
@@ -702,7 +737,11 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let fields: Vec<(&str, &str, bool)> = vec![
("Pulses", pulses.as_str(), *field == EuclideanField::Pulses),
("Steps", steps.as_str(), *field == EuclideanField::Steps),
("Rotation", rotation.as_str(), *field == EuclideanField::Rotation),
(
"Rotation",
rotation.as_str(),
*field == EuclideanField::Rotation,
),
];
render_props_form(frame, inner, &fields);
@@ -723,7 +762,12 @@ 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 hints = hint_line(&[("↑↓", "nav"), ("←→", "adjust"), ("Enter", "apply"), ("Esc", "cancel")]);
let hints = hint_line(&[
("↑↓", "nav"),
("←→", "adjust"),
("Enter", "apply"),
("Esc", "cancel"),
]);
frame.render_widget(Paragraph::new(hints), hint_area);
inner
@@ -791,12 +835,7 @@ fn render_modal_preview(
};
let resolved_display: Vec<(SourceSpan, String)> = trace
.map(|t| {
t.resolved
.iter()
.map(|(s, v)| (*s, v.display()))
.collect()
})
.map(|t| t.resolved.iter().map(|(s, v)| (*s, v.display())).collect())
.unwrap_or_default();
let mut line_start = 0usize;
@@ -804,21 +843,10 @@ fn render_modal_preview(
.lines()
.map(|line_str| {
let tokens = if let Some(t) = trace {
let exec = adjust_spans_for_line(
&t.executed_spans,
line_start,
line_str.len(),
);
let sel = adjust_spans_for_line(
&t.selected_spans,
line_start,
line_str.len(),
);
let res = adjust_resolved_for_line(
&resolved_display,
line_start,
line_str.len(),
);
let exec = adjust_spans_for_line(&t.executed_spans, line_start, line_str.len());
let sel = adjust_spans_for_line(&t.selected_spans, line_start, line_str.len());
let res =
adjust_resolved_for_line(&resolved_display, line_start, line_str.len());
highlight_line_with_runtime(line_str, &exec, &sel, &res, user_words)
} else {
highlight_line_with_runtime(line_str, &[], &[], &[], user_words)
@@ -898,12 +926,7 @@ fn render_modal_editor(
}
let resolved_display: Vec<(SourceSpan, String)> = trace
.map(|t| {
t.resolved
.iter()
.map(|(s, v)| (*s, v.display()))
.collect()
})
.map(|t| t.resolved.iter().map(|(s, v)| (*s, v.display())).collect())
.unwrap_or_default();
let highlighter = |row: usize, line: &str| -> Vec<(Style, String, bool)> {
@@ -919,8 +942,8 @@ fn render_modal_editor(
highlight::highlight_line_with_runtime(line, &exec, &sel, &res, user_words)
};
let show_search = app.editor_ctx.editor.search_active()
|| !app.editor_ctx.editor.search_query().is_empty();
let show_search =
app.editor_ctx.editor.search_active() || !app.editor_ctx.editor.search_query().is_empty();
let reserved_lines = 1 + if show_search { 1 } else { 0 };
let editor_height = inner.height.saturating_sub(reserved_lines);
@@ -1061,10 +1084,7 @@ fn render_modal_keybindings(frame: &mut Frame, app: &App, scroll: usize, term: R
height: 1,
};
let hints = hint_line(&[("↑↓", "scroll"), ("PgUp/Dn", "page"), ("Esc/?", "close")]);
frame.render_widget(
Paragraph::new(hints).alignment(Alignment::Right),
hint_area,
);
frame.render_widget(Paragraph::new(hints).alignment(Alignment::Right), hint_area);
inner
}