uxterm/widget/
view.rs

1use std::io::{stdout, Write};
2use std::process::Command;
3
4use crossterm::{
5    cursor::{MoveTo, MoveToNextLine},
6    event::{self, Event as CEvent, KeyCode, KeyEventKind},
7    execute, queue,
8    style::Print,
9    terminal::{disable_raw_mode, enable_raw_mode},
10};
11
12use crate::widget::{
13    button::Button, checkbox::Checkbox, input::Input, label::Label, slider::Slider,
14};
15
16#[derive(Debug, Clone, Copy)]
17pub enum Event {
18    Key(char),
19    ArrowLeft,
20    ArrowRight,
21    ArrowUp,
22    ArrowDown,
23}
24
25#[derive(Debug)]
26pub enum WidgetValue {
27    Bool(bool),
28    Int(i32),
29    Text(String),
30}
31
32pub enum WidgetType {
33    Button(Button),
34    Checkbox(Checkbox),
35    Slider(Slider),
36    Label(Label),
37    View(View),
38    Input(Input),
39}
40
41impl From<Button> for WidgetType {
42    fn from(value: Button) -> Self {
43        Self::Button(value)
44    }
45}
46impl From<Checkbox> for WidgetType {
47    fn from(value: Checkbox) -> Self {
48        Self::Checkbox(value)
49    }
50}
51impl From<Slider> for WidgetType {
52    fn from(value: Slider) -> Self {
53        Self::Slider(value)
54    }
55}
56impl From<Label> for WidgetType {
57    fn from(value: Label) -> Self {
58        Self::Label(value)
59    }
60}
61impl From<Input> for WidgetType {
62    fn from(value: Input) -> Self {
63        Self::Input(value)
64    }
65}
66impl From<View> for WidgetType {
67    fn from(value: View) -> Self {
68        Self::View(value)
69    }
70}
71
72impl WidgetType {
73    pub fn render(
74        &self,
75        focused: bool,
76        indent: usize,
77        _focus_index: usize,
78        _focusable_index: &mut usize,
79        current_y: &mut u16,
80    ) -> String {
81        let prefix = if focused { ">" } else { " " };
82        let pad = " ".repeat(indent);
83
84        match self {
85            WidgetType::Button(b) => {
86                *current_y += 1;
87                format!("{pad}{}{label}", prefix, label = b.render())
88            }
89            WidgetType::Checkbox(c) => {
90                *current_y += 1;
91                format!("{pad}{}{label}", prefix, label = c.render())
92            }
93            WidgetType::Slider(s) => {
94                *current_y += 1;
95                format!("{pad}{}{label}", prefix, label = s.render())
96            }
97            WidgetType::Label(l) => {
98                *current_y += 1;
99                format!("{pad} {}", l.render())
100            }
101            WidgetType::View(v) => v.render(indent + 2, _focus_index, _focusable_index, current_y),
102            WidgetType::Input(i) => format!("{pad}{}{label}", prefix, label = i.render(focused)),
103        }
104    }
105
106    pub fn handle_event(&mut self, event: &Event, focus_index: usize) {
107        match self {
108            WidgetType::Checkbox(c) => {
109                if let Event::Key(' ') = event {
110                    c.toggle();
111                }
112            }
113            WidgetType::Slider(s) => match event {
114                Event::Key('+') => {
115                    if s.value < s.max {
116                        s.value += 1;
117                    }
118                }
119                Event::Key('-') => {
120                    if s.value > s.min {
121                        s.value -= 1;
122                    }
123                }
124                _ => {}
125            },
126            WidgetType::Input(i) => {
127                match event {
128                    Event::Key(c) => match c {
129                        '\x08' => i.handle_backspace(), // Backspace
130                        '\x1b' => {}                    // Escape (optional)
131                        '\n' => {}                      // Enter (optional)
132                        _ => i.handle_char(*c),         // All other characters
133                    },
134                    Event::ArrowLeft => i.move_cursor_left(),
135                    Event::ArrowRight => i.move_cursor_right(),
136                    _ => {}
137                }
138            }
139
140            WidgetType::View(v) => {
141                v.handle_event(event, focus_index);
142            }
143            _ => {}
144        }
145    }
146
147    pub fn is_focusable(&self) -> bool {
148        match self {
149            WidgetType::View(v) => count_focusables(v) > 0,
150            _ => matches!(
151                self,
152                WidgetType::Button(_)
153                    | WidgetType::Checkbox(_)
154                    | WidgetType::Slider(_)
155                    | WidgetType::Input(_)
156            ),
157        }
158    }
159
160    pub fn value(&self) -> Option<(String, WidgetValue)> {
161        match self {
162            WidgetType::Checkbox(c) => Some((c.label.clone(), WidgetValue::Bool(c.checked))),
163            WidgetType::Slider(s) => {
164                Some((format!("Slider({})", s.label), WidgetValue::Int(s.value)))
165            }
166            WidgetType::Input(i) => Some((
167                i.label.clone(),
168                WidgetValue::Text(i.get_value().to_string()),
169            )),
170            WidgetType::View(_) => None,
171            _ => None,
172        }
173    }
174}
175
176pub struct View {
177    pub label: String,
178    pub widgets: Vec<WidgetType>,
179}
180
181impl View {
182    pub fn new(label: &str) -> Self {
183        View {
184            label: label.to_string(),
185            widgets: Vec::new(),
186        }
187    }
188
189    pub fn add(&mut self, widget: impl Into<WidgetType>) {
190        self.widgets.push(widget.into());
191    }
192
193    pub fn flatten_focusable(&mut self) -> Vec<&mut WidgetType> {
194        let mut result = Vec::new();
195        for widget in &mut self.widgets {
196            match widget {
197                WidgetType::View(v) => result.extend(v.flatten_focusable()),
198                _ if widget.is_focusable() => result.push(widget),
199                _ => {}
200            }
201        }
202        result
203    }
204
205    pub fn handle_event(&mut self, event: &Event, focus_index: usize) {
206        let mut focusables = self.flatten_focusable();
207        if focusables.is_empty() {
208            return;
209        }
210
211        if let Some(widget) = focusables.get_mut(focus_index) {
212            widget.handle_event(event, focus_index);
213        }
214    }
215
216    pub fn render(
217        &self,
218        indent: usize,
219        global_focus_index: usize,
220        focusable_index: &mut usize,
221        current_y: &mut u16,
222    ) -> String {
223        let mut output = vec![format!("{}=== {} ===", " ".repeat(indent), self.label)];
224        *current_y += 1; // header line
225
226        for widget in &self.widgets {
227            match widget {
228                WidgetType::View(v) => {
229                    output.push(v.render(
230                        indent + 2,
231                        global_focus_index,
232                        focusable_index,
233                        current_y,
234                    ));
235                }
236                _ => {
237                    let is_focusable = widget.is_focusable();
238                    let focused = is_focusable && *focusable_index == global_focus_index;
239
240                    output.push(widget.render(
241                        focused,
242                        indent,
243                        global_focus_index,
244                        &mut 0, // dummy focusable index
245                        current_y,
246                    ));
247
248                    // Advance vertical position: Input takes 3 lines, others take 1
249                    *current_y += match widget {
250                        WidgetType::Input(_) => 3,
251                        _ => 1,
252                    };
253
254                    if is_focusable {
255                        *focusable_index += 1;
256                    }
257                }
258            }
259        }
260
261        output.join("\n")
262    }
263
264    pub fn get_values(&self) -> Vec<(String, WidgetValue)> {
265        let mut values = Vec::new();
266        for widget in &self.widgets {
267            match widget {
268                WidgetType::View(v) => values.extend(v.get_values()),
269                _ => {
270                    if let Some(val) = widget.value() {
271                        values.push(val);
272                    }
273                }
274            }
275        }
276        values
277    }
278
279    pub fn run(&mut self) -> std::io::Result<Vec<String>> {
280        enable_raw_mode()?;
281        let mut stdout = stdout();
282        let mut needs_redraw = true;
283        let mut global_focus_index = 0;
284
285        loop {
286            let mut flat_index = 0;
287            let mut current_y = 0;
288
289            if needs_redraw {
290                clear_screen();
291                execute!(stdout, MoveTo(0, 0))?;
292
293                for line in self
294                    .render(0, global_focus_index, &mut flat_index, &mut current_y)
295                    .split('\n')
296                {
297                    queue!(stdout, Print(line), MoveToNextLine(1))?;
298                }
299
300                stdout.flush()?;
301
302                needs_redraw = false;
303            }
304
305            if let CEvent::Key(key_event) = event::read()? {
306                if key_event.kind != KeyEventKind::Press {
307                    continue;
308                }
309
310                match key_event.code {
311                    KeyCode::Esc => {
312                        clear_screen();
313                        break;
314                    }
315                    KeyCode::Tab => {
316                        let total = count_focusables(self);
317                        global_focus_index = (global_focus_index + 1) % total;
318                        needs_redraw = true;
319                    }
320                    KeyCode::BackTab => {
321                        let total = count_focusables(self);
322                        global_focus_index = (global_focus_index + total - 1) % total;
323                        needs_redraw = true;
324                    }
325                    KeyCode::Char(c) => {
326                        self.handle_event(&crate::Event::Key(c), global_focus_index);
327                        needs_redraw = true;
328                    }
329                    KeyCode::Backspace => {
330                        self.handle_event(&crate::Event::Key('\x08'), global_focus_index);
331                        needs_redraw = true;
332                    }
333                    KeyCode::Left => {
334                        self.handle_event(&crate::Event::ArrowLeft, global_focus_index);
335                        needs_redraw = true;
336                    }
337                    KeyCode::Right => {
338                        self.handle_event(&crate::Event::ArrowRight, global_focus_index);
339                        needs_redraw = true;
340                    }
341                    _ => {}
342                }
343            }
344        }
345
346        disable_raw_mode()?;
347
348        let selected: Vec<String> = self
349            .get_values()
350            .into_iter()
351            .filter_map(|(label, value)| match value {
352                crate::WidgetValue::Bool(true) => Some(label),
353                crate::WidgetValue::Text(text) => Some(format!("{}: {}", label, text)),
354                _ => None,
355            })
356            .collect();
357
358        Ok(selected)
359    }
360}
361
362fn count_focusables(view: &View) -> usize {
363    view.widgets
364        .iter()
365        .map(|w| match w {
366            WidgetType::View(v) => count_focusables(v),
367            _ if w.is_focusable() => 1,
368            _ => 0,
369        })
370        .sum()
371}
372
373fn clear_screen() {
374    if cfg!(target_os = "windows") {
375        let _ = Command::new("cmd").args(&["/C", "cls"]).status();
376    } else {
377        let _ = Command::new("clear").status();
378    }
379}