diff options
Diffstat (limited to 'src/ui/settings.rs')
| -rw-r--r-- | src/ui/settings.rs | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/src/ui/settings.rs b/src/ui/settings.rs new file mode 100644 index 00000000..1123f408 --- /dev/null +++ b/src/ui/settings.rs @@ -0,0 +1,238 @@ +use ratatui::{ + layout::{Constraint, Layout, Rect}, + style::{Modifier, Style}, + text::{Line, Span}, + widgets::{List, ListItem, ListState, Paragraph, Tabs}, + Frame, +}; + +use super::widgets::{ + action_button_row_rects, centered_popup_rect, modal_stack_areas, panel_contrast_fg, + render_action_button, render_panel_shell, ActionButtonSpec, +}; +use crate::app::{state::Palette, AppState}; + +pub(super) fn render_settings_overlay(app: &AppState, frame: &mut Frame, area: Rect) { + use crate::app::state::SettingsSection; + + let p = &app.palette; + let Some(popup) = centered_popup_rect(area, 56, 20) else { + return; + }; + + super::dim_background(frame, area); + + let Some(inner) = render_panel_shell(frame, popup, p.accent, p.panel_bg) else { + return; + }; + if inner.height < 4 || inner.width < 10 { + return; + } + + let stack = modal_stack_areas(inner, 3, 2, 0, 1); + let header_rows = Layout::vertical([ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + ]) + .areas::<3>(stack.header); + + frame.render_widget( + Paragraph::new(Line::from(vec![Span::styled( + " settings", + Style::default().fg(p.text).add_modifier(Modifier::BOLD), + )])), + header_rows[0], + ); + + let tabs = Tabs::new(SettingsSection::ALL.iter().map(|s| s.label())) + .select( + SettingsSection::ALL + .iter() + .position(|section| *section == app.settings.section) + .unwrap_or(0), + ) + .style(Style::default().fg(p.overlay1)) + .highlight_style( + Style::default() + .fg(panel_contrast_fg(p)) + .bg(p.accent) + .add_modifier(Modifier::BOLD), + ) + .divider(" ") + .padding(" ", " "); + frame.render_widget(tabs, header_rows[1]); + + let sep = "─".repeat(inner.width as usize); + frame.render_widget( + Paragraph::new(Span::styled(&sep, Style::default().fg(p.surface0))), + header_rows[2], + ); + + let content_area = stack.content; + + match app.settings.section { + SettingsSection::Theme => { + render_settings_theme(app, frame, content_area); + } + SettingsSection::Sound => { + render_settings_toggle( + frame, + content_area, + p, + "sound alerts", + "play sounds when agents change state in background", + app.sound_enabled(), + app.settings.list.selected, + ); + } + SettingsSection::Toast => { + render_settings_toggle( + frame, + content_area, + p, + "visual toasts", + "show top-right notifications for background events", + app.toast_config.enabled, + app.settings.list.selected, + ); + } + } + + if let Some(footer_area) = stack.footer { + let footer_rows = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]) + .areas::<2>(footer_area); + let (apply_rect, close_rect) = settings_button_rects(inner); + render_action_button( + frame, + apply_rect, + Some("↵"), + "apply", + Style::default() + .fg(panel_contrast_fg(p)) + .bg(p.accent) + .add_modifier(Modifier::BOLD), + ); + render_action_button( + frame, + close_rect, + Some("esc"), + "close", + Style::default() + .fg(p.text) + .bg(p.surface0) + .add_modifier(Modifier::BOLD), + ); + + frame.render_widget( + Paragraph::new(Line::from(vec![ + Span::styled(" ↑↓", Style::default().fg(p.overlay0)), + Span::styled(" select ", Style::default().fg(p.overlay1)), + Span::styled("tab", Style::default().fg(p.overlay0)), + Span::styled(" section", Style::default().fg(p.overlay1)), + ])), + footer_rows[0], + ); + } +} + +pub(crate) fn settings_button_rects(inner: Rect) -> (Rect, Rect) { + let rects = action_button_row_rects( + inner, + &[ + ActionButtonSpec { + hint: Some("↵"), + label: "apply", + }, + ActionButtonSpec { + hint: Some("esc"), + label: "close", + }, + ], + 2, + inner.height.saturating_sub(1), + ); + (rects[0], rects[1]) +} + +fn render_settings_theme(app: &AppState, frame: &mut Frame, area: Rect) { + use crate::app::state::THEME_NAMES; + + let p = &app.palette; + let items: Vec<ListItem> = THEME_NAMES + .iter() + .map(|name| { + let is_current = name.to_lowercase().replace([' ', '_'], "-") + == app.theme_name.to_lowercase().replace([' ', '_'], "-"); + let marker = if is_current { " ✓" } else { "" }; + ListItem::new(Line::from(vec![ + Span::styled(*name, Style::default().fg(p.subtext0)), + Span::styled(marker, Style::default().fg(p.green)), + ])) + }) + .collect(); + + let list = List::new(items) + .highlight_style( + Style::default() + .bg(p.surface0) + .fg(p.text) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(" ▸ ") + .style(Style::default().fg(p.subtext0)); + + let mut state = ListState::default().with_selected(Some(app.settings.list.selected)); + frame.render_stateful_widget(list, area, &mut state); +} + +fn render_settings_toggle( + frame: &mut Frame, + area: Rect, + p: &Palette, + title: &str, + description: &str, + current_value: bool, + selected_idx: usize, +) { + let [desc_area, _, list_area] = Layout::vertical([ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Min(2), + ]) + .areas::<3>(area); + + let max_desc_len = (desc_area.width as usize).saturating_sub(2); + let desc_text = if description.len() > max_desc_len { + format!(" {}…", &description[..max_desc_len.saturating_sub(2)]) + } else { + format!(" {description}") + }; + frame.render_widget( + Paragraph::new(Span::styled(desc_text, Style::default().fg(p.overlay1))), + desc_area, + ); + + let items: Vec<ListItem> = ["on", "off"] + .into_iter() + .map(|label| { + let is_active = (label == "on") == current_value; + let marker = if is_active { " ✓" } else { "" }; + ListItem::new(Line::from(vec![ + Span::styled(format!("{title}: {label}"), Style::default().fg(p.subtext0)), + Span::styled(marker, Style::default().fg(p.green)), + ])) + }) + .collect(); + + let list = List::new(items) + .highlight_style( + Style::default() + .bg(p.surface0) + .fg(p.text) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(" ▸ "); + let mut state = ListState::default().with_selected(Some(selected_idx.min(1))); + frame.render_stateful_widget(list, list_area, &mut state); +} |
