WIP: better precision?
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||
@@ -11,6 +11,7 @@ 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);
|
||||
const SCROLL_INDICATOR_COLOR: Color = Color::Rgb(80, 85, 95);
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
let block = Block::default()
|
||||
@@ -28,43 +29,59 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
height: inner.height.saturating_sub(2),
|
||||
};
|
||||
|
||||
let [display_area, _, link_area, _, session_area] = Layout::vertical([
|
||||
Constraint::Length(8),
|
||||
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 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", 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 link_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)),
|
||||
]);
|
||||
|
||||
// Prepare values
|
||||
let flash_str = format!("{:.0}%", app.ui.flash_brightness * 100.0);
|
||||
let lines = vec![
|
||||
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(Color::Rgb(220, 180, 100))
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let value_style = Style::new().fg(Color::Rgb(140, 145, 155));
|
||||
|
||||
// Build flat list of all lines
|
||||
let lines: Vec<Line> = vec![
|
||||
// DISPLAY section (lines 0-7)
|
||||
render_section_header("DISPLAY"),
|
||||
render_divider(content_width),
|
||||
render_option_line(
|
||||
"Refresh rate",
|
||||
app.audio.config.refresh_rate.label(),
|
||||
@@ -94,68 +111,12 @@ fn render_display_section(frame: &mut Frame, app: &App, area: Rect) {
|
||||
if app.ui.show_completion { "On" } else { "Off" },
|
||||
focus == OptionsFocus::ShowCompletion,
|
||||
),
|
||||
render_option_line(
|
||||
"Flash brightness",
|
||||
&flash_str,
|
||||
focus == OptionsFocus::FlashBrightness,
|
||||
),
|
||||
];
|
||||
|
||||
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("Flash brightness", &flash_str, focus == OptionsFocus::FlashBrightness),
|
||||
// Blank line (line 8)
|
||||
Line::from(""),
|
||||
// ABLETON LINK section (lines 9-14)
|
||||
link_header,
|
||||
render_divider(content_width),
|
||||
render_option_line(
|
||||
"Enabled",
|
||||
if link.is_enabled() { "On" } else { "Off" },
|
||||
@@ -171,47 +132,84 @@ fn render_link_section(frame: &mut Frame, app: &App, link: &LinkState, area: Rec
|
||||
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![
|
||||
// Blank line (line 15)
|
||||
Line::from(""),
|
||||
// SESSION section (lines 16-21)
|
||||
render_section_header("SESSION"),
|
||||
render_divider(content_width),
|
||||
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);
|
||||
let total_lines = lines.len();
|
||||
let max_visible = padded.height as usize;
|
||||
|
||||
// Map focus to line index
|
||||
let focus_line: usize = match focus {
|
||||
OptionsFocus::RefreshRate => 2,
|
||||
OptionsFocus::RuntimeHighlight => 3,
|
||||
OptionsFocus::ShowScope => 4,
|
||||
OptionsFocus::ShowSpectrum => 5,
|
||||
OptionsFocus::ShowCompletion => 6,
|
||||
OptionsFocus::FlashBrightness => 7,
|
||||
OptionsFocus::LinkEnabled => 11,
|
||||
OptionsFocus::StartStopSync => 12,
|
||||
OptionsFocus::Quantum => 13,
|
||||
};
|
||||
|
||||
// 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<Line> = 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(SCROLL_INDICATOR_COLOR);
|
||||
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) -> Line<'static> {
|
||||
Line::from(Span::styled(
|
||||
title.to_string(),
|
||||
Style::new().fg(HEADER_COLOR).add_modifier(Modifier::BOLD),
|
||||
))
|
||||
}
|
||||
|
||||
fn render_divider(width: usize) -> Line<'static> {
|
||||
Line::from(Span::styled(
|
||||
"─".repeat(width),
|
||||
Style::new().fg(DIVIDER_COLOR),
|
||||
))
|
||||
}
|
||||
|
||||
fn render_option_line<'a>(label: &'a str, value: &'a str, focused: bool) -> Line<'a> {
|
||||
|
||||
Reference in New Issue
Block a user