Feat: add hidden mode and new documentation
Some checks failed
Deploy Website / deploy (push) Failing after 29s
Some checks failed
Deploy Website / deploy (push) Failing after 29s
This commit is contained in:
@@ -2,7 +2,7 @@ use crate::page::Page;
|
||||
|
||||
pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'static str, &'static str)> {
|
||||
let mut bindings = vec![
|
||||
("F1–F6", "Go to view", "Dict/Patterns/Options/Help/Sequencer/Engine"),
|
||||
("F1–F7", "Go to view", "Dict/Patterns/Options/Help/Sequencer/Engine/Script"),
|
||||
("Ctrl+←→↑↓", "Navigate", "Switch between adjacent views"),
|
||||
];
|
||||
if !plugin_mode {
|
||||
@@ -128,6 +128,14 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
|
||||
bindings.push(("Ctrl+F", "Search", "Activate search"));
|
||||
bindings.push(("Esc", "Clear", "Clear search"));
|
||||
}
|
||||
Page::Script => {
|
||||
bindings.push(("Enter", "Focus", "Focus editor for typing"));
|
||||
bindings.push(("Esc", "Unfocus", "Unfocus editor to use page keybindings"));
|
||||
bindings.push(("Ctrl+E", "Evaluate", "Compile and check for errors (focused)"));
|
||||
bindings.push(("S", "Set Speed", "Set script speed via text input (unfocused)"));
|
||||
bindings.push(("L", "Set Length", "Set script length via text input (unfocused)"));
|
||||
bindings.push(("Ctrl+S", "Stack", "Toggle stack preview (focused)"));
|
||||
}
|
||||
}
|
||||
|
||||
bindings
|
||||
|
||||
@@ -87,7 +87,7 @@ fn render_top_layout(
|
||||
render_sequencer(frame, app, snapshot, areas[idx]);
|
||||
}
|
||||
|
||||
fn render_audio_viz(frame: &mut Frame, app: &App, area: Rect) {
|
||||
pub(crate) fn render_audio_viz(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let mut panels: Vec<VizPanel> = Vec::new();
|
||||
if app.audio.config.show_scope { panels.push(VizPanel::Scope); }
|
||||
if app.audio.config.show_spectrum { panels.push(VizPanel::Spectrum); }
|
||||
@@ -491,7 +491,7 @@ fn viz_gain(data: &[f32], config: &crate::state::audio::AudioConfig) -> f32 {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_scope(frame: &mut Frame, app: &App, area: Rect, orientation: Orientation) {
|
||||
pub(crate) fn render_scope(frame: &mut Frame, app: &App, area: Rect, orientation: Orientation) {
|
||||
let theme = theme::get();
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
@@ -507,7 +507,7 @@ fn render_scope(frame: &mut Frame, app: &App, area: Rect, orientation: Orientati
|
||||
frame.render_widget(scope, inner);
|
||||
}
|
||||
|
||||
fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
|
||||
pub(crate) fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let theme = theme::get();
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
@@ -525,7 +525,7 @@ fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
|
||||
frame.render_widget(spectrum, inner);
|
||||
}
|
||||
|
||||
fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) {
|
||||
pub(crate) fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let theme = theme::get();
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
@@ -600,7 +600,7 @@ fn render_script_preview(
|
||||
frame.render_widget(Paragraph::new(lines), inner);
|
||||
}
|
||||
|
||||
fn render_prelude_preview(
|
||||
pub(crate) fn render_prelude_preview(
|
||||
frame: &mut Frame,
|
||||
app: &App,
|
||||
user_words: &HashSet<String>,
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod main_view;
|
||||
pub mod options_view;
|
||||
pub mod patterns_view;
|
||||
mod render;
|
||||
pub mod script_view;
|
||||
pub mod title_view;
|
||||
|
||||
pub use render::{horizontal_padding, render};
|
||||
|
||||
@@ -25,7 +25,8 @@ use crate::widgets::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view,
|
||||
dict_view, engine_view, help_view, main_view, options_view, patterns_view, script_view,
|
||||
title_view,
|
||||
};
|
||||
|
||||
fn clip_span(span: SourceSpan, line_start: usize, line_len: usize) -> Option<SourceSpan> {
|
||||
@@ -188,6 +189,7 @@ pub fn render(
|
||||
Page::Options => options_view::render(frame, app, link, page_area),
|
||||
Page::Help => help_view::render(frame, app, page_area),
|
||||
Page::Dict => dict_view::render(frame, app, page_area),
|
||||
Page::Script => script_view::render(frame, app, snapshot, page_area),
|
||||
}
|
||||
|
||||
if let Some(side_area) = panel_area {
|
||||
@@ -202,6 +204,7 @@ pub fn render(
|
||||
if app.ui.show_minimap() {
|
||||
let tiles: Vec<NavTile> = Page::ALL
|
||||
.iter()
|
||||
.filter(|p| p.visible_in_minimap())
|
||||
.map(|p| {
|
||||
let (col, row) = p.grid_pos();
|
||||
NavTile {
|
||||
@@ -449,6 +452,7 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
||||
Page::Options => " OPTIONS ",
|
||||
Page::Help => " HELP ",
|
||||
Page::Dict => " DICT ",
|
||||
Page::Script => " SCRIPT ",
|
||||
};
|
||||
|
||||
let content = if let Some(ref msg) = app.ui.status_message {
|
||||
@@ -509,6 +513,13 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
||||
("/", "Search"),
|
||||
("?", "Keys"),
|
||||
],
|
||||
Page::Script => vec![
|
||||
("Esc", "Save & Back"),
|
||||
("C-e", "Eval"),
|
||||
("[ ]", "Speed"),
|
||||
("C-s", "Stack"),
|
||||
("?", "Keys"),
|
||||
],
|
||||
};
|
||||
|
||||
let page_width = page_indicator.chars().count();
|
||||
@@ -608,6 +619,18 @@ fn render_modal(
|
||||
.border_color(theme.modal.confirm)
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::SetScript { field, input } => {
|
||||
use crate::state::ScriptField;
|
||||
let (title, hint) = match field {
|
||||
ScriptField::Length => ("Set Script Length (1-256)", "Enter number"),
|
||||
ScriptField::Speed => ("Set Script Speed", "e.g. 1/3, 2/5, 1x, 2x"),
|
||||
};
|
||||
TextInputModal::new(title, input)
|
||||
.hint(hint)
|
||||
.width(45)
|
||||
.border_color(theme.modal.confirm)
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::JumpToStep(input) => {
|
||||
let pattern_len = app.current_edit_pattern().length;
|
||||
let title = format!("Jump to Step (1-{})", pattern_len);
|
||||
|
||||
166
src/views/script_view.rs
Normal file
166
src/views/script_view.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::Style;
|
||||
use ratatui::text::Span;
|
||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::engine::SequencerSnapshot;
|
||||
use crate::model::SourceSpan;
|
||||
use crate::theme;
|
||||
use crate::views::highlight;
|
||||
use crate::views::render::{adjust_resolved_for_line, adjust_spans_for_line};
|
||||
use crate::widgets::hint_line;
|
||||
|
||||
pub fn layout(area: Rect) -> [Rect; 2] {
|
||||
Layout::horizontal([Constraint::Percentage(60), Constraint::Percentage(40)]).areas(area)
|
||||
}
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||
let [editor_area, sidebar_area] = layout(area);
|
||||
|
||||
render_editor(frame, app, snapshot, editor_area);
|
||||
render_sidebar(frame, app, sidebar_area);
|
||||
}
|
||||
|
||||
fn render_editor(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||
let theme = theme::get();
|
||||
let focused = app.script_editor.focused;
|
||||
let speed_label = app.project_state.project.script_speed.label();
|
||||
let length = app.project_state.project.script_length;
|
||||
let title = format!(" Periodic Script ({speed_label}, {length} steps) ");
|
||||
|
||||
let border_color = if focused { theme.modal.editor } else { theme.ui.border };
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(title)
|
||||
.border_style(Style::new().fg(border_color));
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
if inner.height < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let editor_height = inner.height.saturating_sub(1);
|
||||
let editor_area = Rect::new(inner.x, inner.y, inner.width, editor_height);
|
||||
let hint_area = Rect::new(inner.x, inner.y + editor_height, inner.width, 1);
|
||||
|
||||
let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
|
||||
|
||||
let trace = if app.ui.runtime_highlight && app.playback.playing {
|
||||
snapshot.script_trace()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let text_lines = app.script_editor.editor.lines();
|
||||
let mut line_offsets: Vec<usize> = Vec::with_capacity(text_lines.len());
|
||||
let mut offset = 0;
|
||||
for line in text_lines.iter() {
|
||||
line_offsets.push(offset);
|
||||
offset += line.len() + 1;
|
||||
}
|
||||
|
||||
let resolved_display: Vec<(SourceSpan, String)> = trace
|
||||
.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)> {
|
||||
let line_start = line_offsets[row];
|
||||
let (exec, sel, res) = match trace {
|
||||
Some(t) => (
|
||||
adjust_spans_for_line(&t.executed_spans, line_start, line.len()),
|
||||
adjust_spans_for_line(&t.selected_spans, line_start, line.len()),
|
||||
adjust_resolved_for_line(&resolved_display, line_start, line.len()),
|
||||
),
|
||||
None => (Vec::new(), Vec::new(), Vec::new()),
|
||||
};
|
||||
highlight::highlight_line_with_runtime(line, &exec, &sel, &res, &user_words)
|
||||
};
|
||||
|
||||
app.script_editor.editor.render(frame, editor_area, &highlighter);
|
||||
|
||||
if !focused {
|
||||
let hints = hint_line(&[
|
||||
("Enter", "edit"),
|
||||
("S", "speed"),
|
||||
("L", "length"),
|
||||
("s", "save"),
|
||||
("l", "load"),
|
||||
("?", "keys"),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hints).alignment(Alignment::Right), hint_area);
|
||||
} else if app.script_editor.show_stack {
|
||||
let stack_text = app
|
||||
.script_editor
|
||||
.stack_cache
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|c| c.result.clone())
|
||||
.unwrap_or_else(|| "Stack: []".to_string());
|
||||
let hints = hint_line(&[("Esc", "unfocus"), ("C-e", "eval"), ("C-s", "hide stack")]);
|
||||
let [hint_left, stack_right] = Layout::horizontal([
|
||||
Constraint::Length(hints.width() as u16),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.areas(hint_area);
|
||||
frame.render_widget(Paragraph::new(hints), hint_left);
|
||||
let dim = Style::default().fg(theme.hint.text);
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(stack_text, dim)).alignment(Alignment::Right),
|
||||
stack_right,
|
||||
);
|
||||
} else {
|
||||
let hints = hint_line(&[
|
||||
("Esc", "unfocus"),
|
||||
("C-e", "eval"),
|
||||
("C-s", "stack"),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hints).alignment(Alignment::Right), hint_area);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_sidebar(frame: &mut Frame, app: &App, area: Rect) {
|
||||
use crate::widgets::Orientation;
|
||||
|
||||
let mut constraints = Vec::new();
|
||||
if app.audio.config.show_scope {
|
||||
constraints.push(Constraint::Fill(1));
|
||||
}
|
||||
if app.audio.config.show_spectrum {
|
||||
constraints.push(Constraint::Fill(1));
|
||||
}
|
||||
if app.audio.config.show_lissajous {
|
||||
constraints.push(Constraint::Fill(1));
|
||||
}
|
||||
let has_prelude = !app.project_state.project.prelude.trim().is_empty();
|
||||
if has_prelude {
|
||||
constraints.push(Constraint::Fill(1));
|
||||
}
|
||||
if constraints.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let areas: Vec<Rect> = Layout::vertical(&constraints).split(area).to_vec();
|
||||
let mut idx = 0;
|
||||
|
||||
if app.audio.config.show_scope {
|
||||
super::main_view::render_scope(frame, app, areas[idx], Orientation::Horizontal);
|
||||
idx += 1;
|
||||
}
|
||||
if app.audio.config.show_spectrum {
|
||||
super::main_view::render_spectrum(frame, app, areas[idx]);
|
||||
idx += 1;
|
||||
}
|
||||
if app.audio.config.show_lissajous {
|
||||
super::main_view::render_lissajous(frame, app, areas[idx]);
|
||||
idx += 1;
|
||||
}
|
||||
if has_prelude {
|
||||
let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
|
||||
super::main_view::render_prelude_preview(frame, app, &user_words, areas[idx]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user