Skip to main content

photon_ui/components/
settings_list.rs

1use crossterm::event::{
2    KeyCode,
3    KeyModifiers,
4};
5
6use crate::{
7    Component,
8    Event,
9    Focusable,
10    InputResult,
11    RenderError,
12    Rendered,
13    theme::{
14        Palette,
15        Style,
16        Theme,
17        stylize,
18    },
19};
20
21/// A scrollable list of toggleable settings rendered as checkboxes.
22///
23/// Each item is a `(name, value)` pair where `value` is `true` for checked
24/// `[x]` and `false` for unchecked `[ ]`. Items can be toggled with Enter
25/// or Space, and an optional `on_change` callback is fired on every toggle.
26pub struct SettingsList {
27    items: Vec<(String, bool)>,
28    selected: usize,
29    focused: bool,
30    on_change: Option<fn(usize, bool)>,
31}
32
33impl SettingsList {
34    /// Create a new settings list from `(name, value)` pairs.
35    pub fn new(items: Vec<(String, bool)>) -> Self {
36        Self {
37            items: items,
38            selected: 0,
39            focused: false,
40            on_change: None,
41        }
42    }
43
44    /// Attach a callback invoked when an item is toggled.
45    ///
46    /// The callback receives `(index, new_value)`.
47    pub fn on_change(mut self, cb: fn(usize, bool)) -> Self {
48        self.on_change = Some(cb);
49        self
50    }
51
52    /// Return the current values of all items.
53    pub fn values(&self) -> Vec<bool> {
54        self.items.iter().map(|(_, v)| *v).collect()
55    }
56
57    /// Set the selected item index (clamped to valid range).
58    pub fn set_selected(&mut self, index: usize) {
59        self.selected = index.min(self.items.len().saturating_sub(1));
60    }
61}
62
63impl Focusable for SettingsList {
64    fn focused(&self) -> bool {
65        self.focused
66    }
67
68    fn set_focused(&mut self, focused: bool) {
69        self.focused = focused;
70    }
71}
72
73impl Component for SettingsList {
74    fn render(&self, width: u16) -> Result<Rendered, RenderError> {
75        let theme = Theme::current();
76        let accent_style = Style::new().fg(theme.accent()).bold();
77        let primary_style = Style::new().fg(theme.text_primary());
78        let dim_style = Style::new().fg(theme.text_secondary());
79
80        let mut lines = Vec::new();
81        for (i, (name, value)) in self.items.iter().enumerate() {
82            let is_selected = i == self.selected;
83            let style = if is_selected && self.focused {
84                &accent_style
85            } else if is_selected {
86                &primary_style
87            } else {
88                &dim_style
89            };
90
91            let prefix = if is_selected { "> " } else { "  " };
92            let check = if *value { "[x]" } else { "[ ]" };
93            let line = stylize(&format!("{}{} {}", prefix, check, name), style);
94            lines.push(crate::utils::truncate_to_width(&line, width, "…"));
95        }
96        Ok(Rendered {
97            lines,
98            cursor: None,
99            images: Vec::new(),
100        })
101    }
102
103    fn handle_input(&mut self, event: &Event) -> InputResult {
104        if let Event::Key(key) = event {
105            match key.code {
106                | KeyCode::Down => {
107                    if self.selected + 1 < self.items.len() {
108                        self.selected += 1;
109                    }
110                    InputResult::Handled
111                },
112                | KeyCode::Up => {
113                    if self.selected > 0 {
114                        self.selected -= 1;
115                    }
116                    InputResult::Handled
117                },
118                | KeyCode::Char('j') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
119                    if self.selected + 1 < self.items.len() {
120                        self.selected += 1;
121                    }
122                    InputResult::Handled
123                },
124                | KeyCode::Char('k') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
125                    if self.selected > 0 {
126                        self.selected -= 1;
127                    }
128                    InputResult::Handled
129                },
130                | KeyCode::Enter | KeyCode::Char(' ') => {
131                    if let Some((_, value)) = self.items.get_mut(self.selected) {
132                        *value = !*value;
133                        if let Some(cb) = self.on_change {
134                            cb(self.selected, *value);
135                        }
136                    }
137                    InputResult::Handled
138                },
139                | _ => InputResult::Ignored,
140            }
141        } else {
142            InputResult::Ignored
143        }
144    }
145
146    fn as_focusable(&self) -> Option<&dyn Focusable> {
147        Some(self)
148    }
149
150    fn as_focusable_mut(&mut self) -> Option<&mut dyn Focusable> {
151        Some(self)
152    }
153}