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 = 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 = ["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); }