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_modal_choice_list, render_panel_shell, ActionButtonSpec, }; use crate::{ app::{state::Palette, AppState}, config::ToastDelivery, }; 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, 76, 22) 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_modal_choice_list( frame, content_area, "notification popups", "choose where background popup notifications should appear", &[ ("off", ToastDelivery::Off), ("inside herdr", ToastDelivery::Herdr), ("via terminal", ToastDelivery::Terminal), ], app.toast_delivery(), app.settings.list.selected, p, 2, ); } } 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, ) { render_modal_choice_list( frame, area, title, description, &[("on", true), ("off", false)], current_value, selected_idx, p, 1, ); }