ok
This commit is contained in:
343
src/views/engine_view.rs
Normal file
343
src/views/engine_view.rs
Normal file
@@ -0,0 +1,343 @@
|
||||
use cagire_ratatui::ListSelect;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Row, Table};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::state::{DeviceKind, EngineSection, SettingKind};
|
||||
use crate::widgets::{Orientation, Scope, Spectrum};
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [left_col, _, right_col] = Layout::horizontal([
|
||||
Constraint::Percentage(55),
|
||||
Constraint::Length(2),
|
||||
Constraint::Percentage(45),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
render_settings_section(frame, app, left_col);
|
||||
render_visualizers(frame, app, right_col);
|
||||
}
|
||||
|
||||
fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" Engine ")
|
||||
.border_style(Style::new().fg(Color::Magenta));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let padded = Rect {
|
||||
x: inner.x + 1,
|
||||
y: inner.y + 1,
|
||||
width: inner.width.saturating_sub(2),
|
||||
height: inner.height.saturating_sub(1),
|
||||
};
|
||||
|
||||
let devices_height = devices_section_height(app);
|
||||
|
||||
let [devices_area, _, settings_area, _, samples_area] = Layout::vertical([
|
||||
Constraint::Length(devices_height),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(5),
|
||||
])
|
||||
.areas(padded);
|
||||
|
||||
render_devices(frame, app, devices_area);
|
||||
render_settings(frame, app, settings_area);
|
||||
render_samples(frame, app, samples_area);
|
||||
}
|
||||
|
||||
fn render_visualizers(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [scope_area, _, spectrum_area] = Layout::vertical([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Length(1),
|
||||
Constraint::Percentage(50),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
render_scope(frame, app, scope_area);
|
||||
render_spectrum(frame, app, spectrum_area);
|
||||
}
|
||||
|
||||
fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" Scope ")
|
||||
.border_style(Style::new().fg(Color::Green));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let scope = Scope::new(&app.metrics.scope)
|
||||
.orientation(Orientation::Horizontal)
|
||||
.color(Color::Green);
|
||||
frame.render_widget(scope, inner);
|
||||
}
|
||||
|
||||
fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" Spectrum ")
|
||||
.border_style(Style::new().fg(Color::Cyan));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let spectrum = Spectrum::new(&app.metrics.spectrum);
|
||||
frame.render_widget(spectrum, inner);
|
||||
}
|
||||
|
||||
fn truncate_name(name: &str, max_len: usize) -> String {
|
||||
if name.len() > max_len {
|
||||
format!("{}...", &name[..max_len.saturating_sub(3)])
|
||||
} else {
|
||||
name.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn list_height(item_count: usize) -> u16 {
|
||||
let visible = item_count.min(5) as u16;
|
||||
if item_count > 5 { visible + 1 } else { visible }
|
||||
}
|
||||
|
||||
fn devices_section_height(app: &App) -> u16 {
|
||||
let output_h = list_height(app.audio.output_devices.len());
|
||||
let input_h = list_height(app.audio.input_devices.len());
|
||||
2 + output_h.max(input_h)
|
||||
}
|
||||
|
||||
fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let section_focused = app.audio.section == EngineSection::Devices;
|
||||
let header_style = if section_focused {
|
||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(Color::Rgb(100, 160, 180)).add_modifier(Modifier::BOLD)
|
||||
};
|
||||
|
||||
let [header_area, content_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(1),
|
||||
]).areas(area);
|
||||
|
||||
frame.render_widget(Paragraph::new("Devices").style(header_style), header_area);
|
||||
|
||||
let [output_col, separator, input_col] = Layout::horizontal([
|
||||
Constraint::Percentage(48),
|
||||
Constraint::Length(3),
|
||||
Constraint::Percentage(48),
|
||||
]).areas(content_area);
|
||||
|
||||
let output_focused = section_focused && app.audio.device_kind == DeviceKind::Output;
|
||||
let input_focused = section_focused && app.audio.device_kind == DeviceKind::Input;
|
||||
|
||||
render_device_column(
|
||||
frame, app, output_col,
|
||||
"Output", &app.audio.output_devices,
|
||||
app.audio.current_output_device_index(),
|
||||
app.audio.output_list.cursor,
|
||||
app.audio.output_list.scroll_offset,
|
||||
output_focused,
|
||||
section_focused,
|
||||
);
|
||||
|
||||
let sep_style = Style::new().fg(Color::Rgb(60, 65, 75));
|
||||
let sep_lines: Vec<Line> = (0..separator.height)
|
||||
.map(|_| Line::from(Span::styled("│", sep_style)))
|
||||
.collect();
|
||||
frame.render_widget(Paragraph::new(sep_lines), separator);
|
||||
|
||||
render_device_column(
|
||||
frame, app, input_col,
|
||||
"Input", &app.audio.input_devices,
|
||||
app.audio.current_input_device_index(),
|
||||
app.audio.input_list.cursor,
|
||||
app.audio.input_list.scroll_offset,
|
||||
input_focused,
|
||||
section_focused,
|
||||
);
|
||||
}
|
||||
|
||||
fn render_device_column(
|
||||
frame: &mut Frame,
|
||||
_app: &App,
|
||||
area: Rect,
|
||||
label: &str,
|
||||
devices: &[doux::audio::AudioDeviceInfo],
|
||||
selected_idx: usize,
|
||||
cursor: usize,
|
||||
scroll_offset: usize,
|
||||
focused: bool,
|
||||
section_focused: bool,
|
||||
) {
|
||||
let [label_area, list_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(1),
|
||||
]).areas(area);
|
||||
|
||||
let label_style = if focused {
|
||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else if section_focused {
|
||||
Style::new().fg(Color::Rgb(150, 155, 165))
|
||||
} else {
|
||||
Style::new().fg(Color::Rgb(100, 105, 115))
|
||||
};
|
||||
|
||||
let arrow = if focused { "> " } else { " " };
|
||||
frame.render_widget(
|
||||
Paragraph::new(format!("{arrow}{label}")).style(label_style),
|
||||
label_area,
|
||||
);
|
||||
|
||||
let items: Vec<String> = devices.iter()
|
||||
.map(|d| truncate_name(&d.name, 25))
|
||||
.collect();
|
||||
|
||||
ListSelect::new(&items, selected_idx, cursor)
|
||||
.focused(focused)
|
||||
.scroll_offset(scroll_offset)
|
||||
.render(frame, list_area);
|
||||
}
|
||||
|
||||
fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let section_focused = app.audio.section == EngineSection::Settings;
|
||||
let header_style = if section_focused {
|
||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(Color::Rgb(100, 160, 180)).add_modifier(Modifier::BOLD)
|
||||
};
|
||||
|
||||
let [header_area, content_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);
|
||||
|
||||
frame.render_widget(Paragraph::new("Settings").style(header_style), header_area);
|
||||
|
||||
let highlight = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
||||
let normal = Style::new().fg(Color::White);
|
||||
let label_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
||||
let value_style = Style::new().fg(Color::Rgb(180, 180, 190));
|
||||
|
||||
let channels_focused = section_focused && app.audio.setting_kind == SettingKind::Channels;
|
||||
let buffer_focused = section_focused && app.audio.setting_kind == SettingKind::BufferSize;
|
||||
let polyphony_focused = section_focused && app.audio.setting_kind == SettingKind::Polyphony;
|
||||
|
||||
let rows = vec![
|
||||
Row::new(vec![
|
||||
Span::styled(if channels_focused { "> Channels" } else { " Channels" }, label_style),
|
||||
render_selector(
|
||||
&format!("{}", app.audio.config.channels),
|
||||
channels_focused,
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled(if buffer_focused { "> Buffer" } else { " Buffer" }, label_style),
|
||||
render_selector(
|
||||
&format!("{}", app.audio.config.buffer_size),
|
||||
buffer_focused,
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled(if polyphony_focused { "> Voices" } else { " Voices" }, label_style),
|
||||
render_selector(
|
||||
&format!("{}", app.audio.config.max_voices),
|
||||
polyphony_focused,
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled(" Sample rate", label_style),
|
||||
Span::styled(
|
||||
format!("{:.0} Hz", app.audio.config.sample_rate),
|
||||
value_style,
|
||||
),
|
||||
]),
|
||||
];
|
||||
|
||||
let table = Table::new(rows, [Constraint::Length(14), Constraint::Fill(1)]);
|
||||
frame.render_widget(table, content_area);
|
||||
}
|
||||
|
||||
fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let section_focused = app.audio.section == EngineSection::Samples;
|
||||
let header_style = if section_focused {
|
||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(Color::Rgb(100, 160, 180)).add_modifier(Modifier::BOLD)
|
||||
};
|
||||
|
||||
let [header_area, content_area, _, hint_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
let path_count = app.audio.config.sample_paths.len();
|
||||
let sample_count = app.audio.config.sample_count;
|
||||
let header_text = format!("Samples {path_count} paths · {sample_count} indexed");
|
||||
frame.render_widget(Paragraph::new(header_text).style(header_style), header_area);
|
||||
|
||||
let dim = Style::new().fg(Color::Rgb(80, 85, 95));
|
||||
let path_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
||||
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
if app.audio.config.sample_paths.is_empty() {
|
||||
lines.push(Line::from(Span::styled(
|
||||
" No sample paths configured",
|
||||
dim,
|
||||
)));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" Add folders containing .wav files",
|
||||
dim,
|
||||
)));
|
||||
} else {
|
||||
for (i, path) in app.audio.config.sample_paths.iter().take(4).enumerate() {
|
||||
let path_str = path.to_string_lossy();
|
||||
let display = truncate_name(&path_str, 40);
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!(" {} ", i + 1), dim),
|
||||
Span::styled(display, path_style),
|
||||
]));
|
||||
}
|
||||
if path_count > 4 {
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" ... and {} more", path_count - 4),
|
||||
dim,
|
||||
)));
|
||||
}
|
||||
}
|
||||
frame.render_widget(Paragraph::new(lines), content_area);
|
||||
|
||||
let hint_style = if section_focused {
|
||||
Style::new().fg(Color::Rgb(180, 180, 100))
|
||||
} else {
|
||||
Style::new().fg(Color::Rgb(60, 60, 70))
|
||||
};
|
||||
let hint = Line::from(vec![
|
||||
Span::styled("A", hint_style),
|
||||
Span::styled(":add ", Style::new().fg(Color::Rgb(80, 85, 95))),
|
||||
Span::styled("D", hint_style),
|
||||
Span::styled(":remove", Style::new().fg(Color::Rgb(80, 85, 95))),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hint), hint_area);
|
||||
}
|
||||
|
||||
fn render_selector(value: &str, focused: bool, highlight: Style, normal: Style) -> Span<'static> {
|
||||
let style = if focused { highlight } else { normal };
|
||||
if focused {
|
||||
Span::styled(format!("< {value} >"), style)
|
||||
} else {
|
||||
Span::styled(format!(" {value} "), style)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user