use ratatui::layout::Rect; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph}; use ratatui::Frame; use crate::app::App; use crate::engine::LinkState; use crate::state::OptionsFocus; use crate::theme; pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) { let theme = theme::get(); let block = Block::default() .borders(Borders::ALL) .title(" Options ") .border_style(Style::new().fg(theme.modal.input)); let inner = block.inner(area); frame.render_widget(block, area); let padded = Rect { x: inner.x + 2, y: inner.y + 1, width: inner.width.saturating_sub(4), height: inner.height.saturating_sub(2), }; let focus = app.options.focus; let content_width = padded.width as usize; // Build link header with status let enabled = link.is_enabled(); let peers = link.peers(); let (status_text, status_color) = if !enabled { ("DISABLED", theme.link_status.disabled) } else if peers > 0 { ("CONNECTED", theme.link_status.connected) } else { ("LISTENING", theme.link_status.listening) }; let peer_text = if enabled && peers > 0 { if peers == 1 { " · 1 peer".to_string() } else { format!(" · {peers} peers") } } else { String::new() }; let link_header = Line::from(vec![ Span::styled( "ABLETON LINK", Style::new().fg(theme.ui.header).add_modifier(Modifier::BOLD), ), Span::raw(" "), Span::styled( status_text, Style::new().fg(status_color).add_modifier(Modifier::BOLD), ), Span::styled(peer_text, Style::new().fg(theme.ui.text_muted)), ]); // Prepare values let flash_str = format!("{:.0}%", app.ui.flash_brightness * 100.0); let quantum_str = format!("{:.0}", link.quantum()); let tempo_str = format!("{:.1} BPM", link.tempo()); let beat_str = format!("{:.2}", link.beat()); let phase_str = format!("{:.2}", link.phase()); let tempo_style = Style::new().fg(theme.values.tempo).add_modifier(Modifier::BOLD); let value_style = Style::new().fg(theme.values.value); // Build flat list of all lines let lines: Vec = vec![ // DISPLAY section (lines 0-8) render_section_header("DISPLAY", &theme), render_divider(content_width, &theme), render_option_line( "Theme", app.ui.color_scheme.label(), focus == OptionsFocus::ColorScheme, &theme, ), render_option_line( "Refresh rate", app.audio.config.refresh_rate.label(), focus == OptionsFocus::RefreshRate, &theme, ), render_option_line( "Runtime highlight", if app.ui.runtime_highlight { "On" } else { "Off" }, focus == OptionsFocus::RuntimeHighlight, &theme, ), render_option_line( "Show scope", if app.audio.config.show_scope { "On" } else { "Off" }, focus == OptionsFocus::ShowScope, &theme, ), render_option_line( "Show spectrum", if app.audio.config.show_spectrum { "On" } else { "Off" }, focus == OptionsFocus::ShowSpectrum, &theme, ), render_option_line( "Completion", if app.ui.show_completion { "On" } else { "Off" }, focus == OptionsFocus::ShowCompletion, &theme, ), render_option_line("Flash brightness", &flash_str, focus == OptionsFocus::FlashBrightness, &theme), // Blank line (line 9) Line::from(""), // ABLETON LINK section (lines 10-15) link_header, render_divider(content_width, &theme), render_option_line( "Enabled", if link.is_enabled() { "On" } else { "Off" }, focus == OptionsFocus::LinkEnabled, &theme, ), render_option_line( "Start/Stop sync", if link.is_start_stop_sync_enabled() { "On" } else { "Off" }, focus == OptionsFocus::StartStopSync, &theme, ), render_option_line("Quantum", &quantum_str, focus == OptionsFocus::Quantum, &theme), // Blank line (line 16) Line::from(""), // SESSION section (lines 17-22) render_section_header("SESSION", &theme), render_divider(content_width, &theme), render_readonly_line("Tempo", &tempo_str, tempo_style, &theme), render_readonly_line("Beat", &beat_str, value_style, &theme), render_readonly_line("Phase", &phase_str, value_style, &theme), ]; let total_lines = lines.len(); let max_visible = padded.height as usize; // Map focus to line index let focus_line: usize = match focus { OptionsFocus::ColorScheme => 2, OptionsFocus::RefreshRate => 3, OptionsFocus::RuntimeHighlight => 4, OptionsFocus::ShowScope => 5, OptionsFocus::ShowSpectrum => 6, OptionsFocus::ShowCompletion => 7, OptionsFocus::FlashBrightness => 8, OptionsFocus::LinkEnabled => 12, OptionsFocus::StartStopSync => 13, OptionsFocus::Quantum => 14, }; // Calculate scroll offset to keep focused line visible (centered when possible) let scroll_offset = if total_lines <= max_visible { 0 } else { focus_line .saturating_sub(max_visible / 2) .min(total_lines.saturating_sub(max_visible)) }; // Render visible portion let visible_end = (scroll_offset + max_visible).min(total_lines); let visible_lines: Vec = lines .into_iter() .skip(scroll_offset) .take(visible_end - scroll_offset) .collect(); frame.render_widget(Paragraph::new(visible_lines), padded); // Render scroll indicators let indicator_style = Style::new().fg(theme.ui.text_dim); let indicator_x = padded.x + padded.width.saturating_sub(1); if scroll_offset > 0 { let up_indicator = Paragraph::new("▲").style(indicator_style); frame.render_widget( up_indicator, Rect::new(indicator_x, padded.y, 1, 1), ); } if visible_end < total_lines { let down_indicator = Paragraph::new("▼").style(indicator_style); frame.render_widget( down_indicator, Rect::new(indicator_x, padded.y + padded.height.saturating_sub(1), 1, 1), ); } } fn render_section_header(title: &str, theme: &theme::ThemeColors) -> Line<'static> { Line::from(Span::styled( title.to_string(), Style::new().fg(theme.ui.header).add_modifier(Modifier::BOLD), )) } fn render_divider(width: usize, theme: &theme::ThemeColors) -> Line<'static> { Line::from(Span::styled( "─".repeat(width), Style::new().fg(theme.ui.border), )) } fn render_option_line<'a>(label: &'a str, value: &'a str, focused: bool, theme: &theme::ThemeColors) -> Line<'a> { let highlight = Style::new().fg(theme.hint.key).add_modifier(Modifier::BOLD); let normal = Style::new().fg(theme.ui.text_primary); let label_style = Style::new().fg(theme.ui.text_muted); let prefix = if focused { "> " } else { " " }; let prefix_style = if focused { highlight } else { normal }; let value_display = if focused { format!("< {value} >") } else { format!(" {value} ") }; let value_style = if focused { highlight } else { normal }; let label_width = 20; let padded_label = format!("{label:(label: &'a str, value: &'a str, value_style: Style, theme: &theme::ThemeColors) -> Line<'a> { let label_style = Style::new().fg(theme.ui.text_muted); let label_width = 20; let padded_label = format!("{label: