use std::collections::HashSet; use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::Style; 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 = 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 = 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 { let hints = hint_line(&[ ("Esc", "unfocus"), ("C-e", "eval"), ]); 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() || !app.project_state.project.banks[app.editor_ctx.bank] .prelude .trim() .is_empty(); if has_prelude { constraints.push(Constraint::Fill(1)); } if constraints.is_empty() { return; } let areas: Vec = 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 = app.dict.lock().keys().cloned().collect(); super::main_view::render_prelude_preview(frame, app, &user_words, areas[idx]); } }