Reworked settings menu
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
//! Secondary settings window; child dialogs are rendered above it.
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
text::Line,
|
||||
widgets::{Block, Clear, List, ListItem, ListState, Paragraph, Wrap},
|
||||
};
|
||||
|
||||
use super::theme;
|
||||
use crate::app::state::{AppState, SettingsRow, additional_settings_rows};
|
||||
|
||||
pub fn draw(frame: &mut Frame, state: &AppState) {
|
||||
let screen = frame.area();
|
||||
let width = screen.width.saturating_sub(2).min(90);
|
||||
let height = screen.height.saturating_sub(2).min(26);
|
||||
let area = Rect::new(
|
||||
screen.x + (screen.width - width) / 2,
|
||||
screen.y + (screen.height - height) / 2,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
let block = Block::bordered()
|
||||
.title(" Additional settings ")
|
||||
.title_style(theme::header_for(state))
|
||||
.border_style(theme::strong_border_for(state));
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(block, area);
|
||||
let [list_area, status_area, footer] = Layout::vertical([
|
||||
Constraint::Min(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(inner);
|
||||
|
||||
let rows = additional_settings_rows(state);
|
||||
let mut items = Vec::new();
|
||||
let mut selected = 0;
|
||||
for (index, row) in rows.iter().enumerate() {
|
||||
let section = match row {
|
||||
SettingsRow::MusicDirectory => Some("Library"),
|
||||
SettingsRow::CheckUpdate => Some("Updates"),
|
||||
SettingsRow::VisualizationClock => Some("Visualizations"),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(section) = section {
|
||||
items.push(ListItem::new(Line::styled(
|
||||
section,
|
||||
theme::header_for(state),
|
||||
)));
|
||||
}
|
||||
if index
|
||||
== state
|
||||
.additional_settings_cursor
|
||||
.min(rows.len().saturating_sub(1))
|
||||
{
|
||||
selected = items.len();
|
||||
}
|
||||
let (label, value, enabled) = match row {
|
||||
SettingsRow::MusicDirectory => (
|
||||
"Music save directory".into(),
|
||||
if state.music_dir_changing {
|
||||
"checking/changing...".into()
|
||||
} else {
|
||||
state.music_dir.to_string_lossy().into_owned()
|
||||
},
|
||||
!state.music_dir_changing,
|
||||
),
|
||||
SettingsRow::CheckUpdate => (
|
||||
"Check for updates".into(),
|
||||
format!("v{}", env!("CARGO_PKG_VERSION")),
|
||||
!state.updater.busy && !state.updater.installed,
|
||||
),
|
||||
SettingsRow::InstallUpdate => (
|
||||
"Install update".into(),
|
||||
state
|
||||
.updater
|
||||
.available
|
||||
.as_ref()
|
||||
.map(|u| format!("v{}", u.version))
|
||||
.unwrap_or_else(|| "check for updates first".into()),
|
||||
state.updater.available.is_some()
|
||||
&& !state.updater.busy
|
||||
&& !state.updater.installed,
|
||||
),
|
||||
SettingsRow::VisualizationClock => (
|
||||
"Show clock".into(),
|
||||
if state.visualizer.config.show_clock {
|
||||
"on"
|
||||
} else {
|
||||
"off"
|
||||
}
|
||||
.into(),
|
||||
true,
|
||||
),
|
||||
SettingsRow::VisualizationScript(index) => {
|
||||
let script = &state.visualizer.scripts[*index];
|
||||
let mark = if state.visualizer.selected_script_index() == Some(*index) {
|
||||
"* "
|
||||
} else {
|
||||
""
|
||||
};
|
||||
(
|
||||
format!("{mark}{}", script.name),
|
||||
script
|
||||
.path
|
||||
.file_name()
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or_default(),
|
||||
true,
|
||||
)
|
||||
}
|
||||
SettingsRow::VisualizationNew => {
|
||||
("+ New visualization script".into(), "enter".into(), true)
|
||||
}
|
||||
SettingsRow::VisualizationEdit => {
|
||||
("Edit selected visualization".into(), "enter".into(), true)
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
let item = ListItem::new(format!("{label}: {value}"));
|
||||
items.push(if enabled {
|
||||
item
|
||||
} else {
|
||||
item.style(theme::dim())
|
||||
});
|
||||
}
|
||||
frame.render_stateful_widget(
|
||||
List::new(items)
|
||||
.highlight_symbol("> ")
|
||||
.highlight_style(theme::selection_for(state)),
|
||||
list_area,
|
||||
&mut ListState::default().with_selected(Some(selected)),
|
||||
);
|
||||
let message = state
|
||||
.status_message
|
||||
.as_deref()
|
||||
.filter(|message| !message.is_empty())
|
||||
.unwrap_or(&state.updater.message);
|
||||
frame.render_widget(
|
||||
Paragraph::new(message)
|
||||
.wrap(Wrap { trim: true })
|
||||
.style(theme::dim()),
|
||||
status_area,
|
||||
);
|
||||
frame.render_widget(
|
||||
Paragraph::new("Up/Down: navigate | Enter: select | Esc: back").style(theme::dim()),
|
||||
footer,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn window_renders_controls_and_scrolls_to_last_row_in_small_terminal() {
|
||||
for (width, height) in [(80, 30), (45, 14)] {
|
||||
let mut terminal =
|
||||
ratatui::Terminal::new(ratatui::backend::TestBackend::new(width, height)).unwrap();
|
||||
let mut state = AppState::default();
|
||||
state.updater.message = "No newer stable release".into();
|
||||
terminal.draw(|frame| draw(frame, &state)).unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|c| c.symbol())
|
||||
.collect();
|
||||
assert!(text.contains("Additional settings"));
|
||||
assert!(text.contains("Music save directory"));
|
||||
if height >= 30 {
|
||||
assert!(text.contains("Check for updates"));
|
||||
assert!(text.contains("Install update"));
|
||||
assert!(text.contains("Visualizations"));
|
||||
}
|
||||
state.additional_settings_cursor = additional_settings_rows(&state).len() - 1;
|
||||
terminal.draw(|frame| draw(frame, &state)).unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|c| c.symbol())
|
||||
.collect();
|
||||
assert!(text.contains("New visualization script"));
|
||||
assert!(text.contains("No newer stable release"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user