This commit is contained in:
2026-01-26 00:24:17 +01:00
parent ce98acacd0
commit fcb6adb6af
12 changed files with 862 additions and 614 deletions

246
src/views/options_view.rs Normal file
View File

@@ -0,0 +1,246 @@
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, 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;
const LABEL_COLOR: Color = Color::Rgb(120, 125, 135);
const HEADER_COLOR: Color = Color::Rgb(100, 160, 180);
const DIVIDER_COLOR: Color = Color::Rgb(60, 65, 70);
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
let block = Block::default()
.borders(Borders::ALL)
.title(" Options ")
.border_style(Style::new().fg(Color::Cyan));
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 [display_area, _, link_area, _, session_area] = Layout::vertical([
Constraint::Length(7),
Constraint::Length(1),
Constraint::Length(5),
Constraint::Length(1),
Constraint::Min(5),
])
.areas(padded);
render_display_section(frame, app, display_area);
render_link_section(frame, app, link, link_area);
render_session_section(frame, link, session_area);
}
fn render_display_section(frame: &mut Frame, app: &App, area: Rect) {
let [header_area, divider_area, content_area] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(1),
])
.areas(area);
let header = Line::from(Span::styled(
"DISPLAY",
Style::new().fg(HEADER_COLOR).add_modifier(Modifier::BOLD),
));
frame.render_widget(Paragraph::new(header), header_area);
let divider = "".repeat(area.width as usize);
frame.render_widget(
Paragraph::new(divider).style(Style::new().fg(DIVIDER_COLOR)),
divider_area,
);
let focus = app.options.focus;
let lines = vec![
render_option_line(
"Refresh rate",
app.audio.config.refresh_rate.label(),
focus == OptionsFocus::RefreshRate,
),
render_option_line(
"Runtime highlight",
if app.ui.runtime_highlight { "On" } else { "Off" },
focus == OptionsFocus::RuntimeHighlight,
),
render_option_line(
"Show scope",
if app.audio.config.show_scope { "On" } else { "Off" },
focus == OptionsFocus::ShowScope,
),
render_option_line(
"Show spectrum",
if app.audio.config.show_spectrum {
"On"
} else {
"Off"
},
focus == OptionsFocus::ShowSpectrum,
),
render_option_line(
"Completion",
if app.ui.show_completion { "On" } else { "Off" },
focus == OptionsFocus::ShowCompletion,
),
];
frame.render_widget(Paragraph::new(lines), content_area);
}
fn render_link_section(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
let [header_area, divider_area, content_area] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(1),
])
.areas(area);
let enabled = link.is_enabled();
let peers = link.peers();
let (status_text, status_color) = if !enabled {
("DISABLED", Color::Rgb(120, 60, 60))
} else if peers > 0 {
("CONNECTED", Color::Rgb(60, 120, 60))
} else {
("LISTENING", Color::Rgb(120, 120, 60))
};
let peer_text = if enabled && peers > 0 {
if peers == 1 {
" · 1 peer".to_string()
} else {
format!(" · {peers} peers")
}
} else {
String::new()
};
let header = Line::from(vec![
Span::styled(
"ABLETON LINK",
Style::new().fg(HEADER_COLOR).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(LABEL_COLOR)),
]);
frame.render_widget(Paragraph::new(header), header_area);
let divider = "".repeat(area.width as usize);
frame.render_widget(
Paragraph::new(divider).style(Style::new().fg(DIVIDER_COLOR)),
divider_area,
);
let focus = app.options.focus;
let quantum_str = format!("{:.0}", link.quantum());
let lines = vec![
render_option_line(
"Enabled",
if link.is_enabled() { "On" } else { "Off" },
focus == OptionsFocus::LinkEnabled,
),
render_option_line(
"Start/Stop sync",
if link.is_start_stop_sync_enabled() {
"On"
} else {
"Off"
},
focus == OptionsFocus::StartStopSync,
),
render_option_line("Quantum", &quantum_str, focus == OptionsFocus::Quantum),
];
frame.render_widget(Paragraph::new(lines), content_area);
}
fn render_session_section(frame: &mut Frame, link: &LinkState, area: Rect) {
let [header_area, divider_area, content_area] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(1),
])
.areas(area);
let header = Line::from(Span::styled(
"SESSION",
Style::new().fg(HEADER_COLOR).add_modifier(Modifier::BOLD),
));
frame.render_widget(Paragraph::new(header), header_area);
let divider = "".repeat(area.width as usize);
frame.render_widget(
Paragraph::new(divider).style(Style::new().fg(DIVIDER_COLOR)),
divider_area,
);
let tempo_style = Style::new()
.fg(Color::Rgb(220, 180, 100))
.add_modifier(Modifier::BOLD);
let value_style = Style::new().fg(Color::Rgb(140, 145, 155));
let tempo_str = format!("{:.1} BPM", link.tempo());
let beat_str = format!("{:.2}", link.beat());
let phase_str = format!("{:.2}", link.phase());
let lines = vec![
render_readonly_line("Tempo", &tempo_str, tempo_style),
render_readonly_line("Beat", &beat_str, value_style),
render_readonly_line("Phase", &phase_str, value_style),
];
frame.render_widget(Paragraph::new(lines), content_area);
}
fn render_option_line<'a>(label: &'a str, value: &'a str, focused: bool) -> Line<'a> {
let highlight = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
let normal = Style::new().fg(Color::White);
let label_style = Style::new().fg(LABEL_COLOR);
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_width$}");
Line::from(vec![
Span::styled(prefix, prefix_style),
Span::styled(padded_label, label_style),
Span::styled(value_display, value_style),
])
}
fn render_readonly_line<'a>(label: &'a str, value: &'a str, value_style: Style) -> Line<'a> {
let label_style = Style::new().fg(LABEL_COLOR);
let label_width = 20;
let padded_label = format!("{label:<label_width$}");
Line::from(vec![
Span::raw(" "),
Span::styled(padded_label, label_style),
Span::styled(format!(" {value}"), value_style),
])
}