Skip to main content

wiki_tui/components/
search_language_popup.rs

1use std::sync::Arc;
2
3use crossterm::event::KeyCode;
4use ratatui::{
5    layout::{Constraint, Direction, Layout, Rect},
6    style::{Modifier, Style, Stylize},
7    widgets::{Clear, List, ListItem},
8};
9use tui_input::{backend::crossterm::EventHandler, Input};
10use wiki_api::languages::{Language, LANGUAGES};
11
12use crate::{
13    action::{Action, ActionPacket, ActionResult, SearchAction},
14    config::{Config, Theme},
15    terminal::Frame,
16    ui::{centered_rect, StatefulList},
17};
18
19use super::Component;
20
21const FOCUS_INPUT: u8 = 0;
22const FOCUS_LIST: u8 = 1;
23
24pub struct SearchLanguageSelectionComponent {
25    input: Input,
26    focus: u8,
27    list: StatefulList<Language>,
28
29    config: Arc<Config>,
30    theme: Arc<Theme>,
31}
32
33impl SearchLanguageSelectionComponent {
34    pub fn new(config: Arc<Config>, theme: Arc<Theme>) -> Self {
35        Self {
36            input: Input::default(),
37            list: StatefulList::with_items(Vec::new()),
38            focus: 0,
39
40            config,
41            theme,
42        }
43    }
44
45    fn update_list(&mut self) {
46        let input_value = self.input.value();
47        let sorted_languages = LANGUAGES
48            .iter()
49            .filter(|lang| {
50                let lang = lang.name().to_lowercase();
51                let query = input_value.to_lowercase();
52                lang.contains(&query)
53            })
54            .map(|x| x.to_owned())
55            .collect::<Vec<Language>>();
56        self.list = StatefulList::with_items(sorted_languages);
57    }
58}
59
60impl Component for SearchLanguageSelectionComponent {
61    fn handle_key_events(&mut self, key: crossterm::event::KeyEvent) -> ActionResult {
62        if self.config.bindings.global.submit.matches_event(key) {
63            if let Some(lang) = self.list.selected() {
64                let mut packet =
65                    ActionPacket::single(Action::SwitchContextSearch).action(Action::PopPopup);
66
67                if self.config.ui.popup_search_language_changed {
68                    packet = packet.action(Action::PopupMessage(
69                        "Information".to_string(),
70                        format!("Changed the language for searches to '{}'", lang.name()),
71                    ));
72                }
73
74                return packet
75                    .action(Action::Search(SearchAction::ChangeLanguage(
76                        lang.to_owned(),
77                    )))
78                    .into();
79            }
80            return ActionResult::Ignored;
81        }
82
83        if self.config.bindings.global.pop_popup.matches_event(key) {
84            return Action::PopPopup.into();
85        }
86
87        match key.code {
88            KeyCode::Tab | KeyCode::BackTab => {
89                if self.focus == FOCUS_INPUT {
90                    self.focus = FOCUS_LIST;
91                } else if self.focus == FOCUS_LIST {
92                    self.focus = FOCUS_INPUT;
93                }
94
95                tracing::debug!("focus now: '{}'", self.focus);
96
97                ActionResult::consumed()
98            }
99            KeyCode::Char('i') if self.focus == FOCUS_LIST => {
100                self.focus = FOCUS_INPUT;
101                ActionResult::consumed()
102            }
103
104            KeyCode::F(2) => Action::PopPopup.into(),
105
106            _ if self.focus == FOCUS_INPUT => {
107                self.input.handle_event(&crossterm::event::Event::Key(key));
108                self.update_list();
109                ActionResult::consumed()
110            }
111            _ => ActionResult::Ignored,
112        }
113    }
114
115    fn update(&mut self, action: Action) -> ActionResult {
116        match action {
117            Action::ScrollUp(n) => {
118                for _ in 0..n {
119                    self.list.previous()
120                }
121                ActionResult::consumed()
122            }
123            Action::ScrollDown(n) => {
124                for _ in 0..n {
125                    self.list.next()
126                }
127                ActionResult::consumed()
128            }
129            Action::UnselectScroll => {
130                self.list.unselect();
131                ActionResult::consumed()
132            }
133            _ => ActionResult::Ignored,
134        }
135    }
136
137    fn render(&mut self, f: &mut Frame<'_>, area: Rect) {
138        let popup_block = self
139            .theme
140            .default_block()
141            .title("Switch Search Language")
142            .style(Style::default().bg(self.theme.bg));
143        let area = centered_rect(area, 25, 60);
144        f.render_widget(Clear, area);
145        f.render_widget(popup_block, area);
146
147        let (input_area, list_area) = {
148            let chunks = Layout::default()
149                .direction(Direction::Vertical)
150                .margin(1)
151                .constraints([Constraint::Length(1), Constraint::Percentage(100)])
152                .split(area);
153            (chunks[0], chunks[1])
154        };
155
156        let scroll = self.input.visual_scroll(input_area.width as usize);
157        let cursor = self.input.visual_cursor();
158        let value = self.input.value();
159
160        let input_widget = self
161            .theme
162            .default_paragraph(format!(
163                "{}{}",
164                value,
165                "_".repeat((input_area.width as usize).saturating_sub(value.len()))
166            ))
167            .scroll((0, scroll as u16));
168        f.render_widget(input_widget, input_area);
169
170        if self.focus == FOCUS_INPUT {
171            f.set_cursor_position((
172                input_area.x + (cursor.max(scroll) - scroll) as u16,
173                input_area.y,
174            ));
175        }
176
177        let list_items = self
178            .list
179            .get_items()
180            .iter()
181            .map(|x| ListItem::new(x.name().to_owned()).fg(self.theme.fg));
182        let list_widget = List::new(list_items).highlight_style(if self.focus == FOCUS_LIST {
183            Style::default()
184                .fg(self.theme.selected_fg)
185                .bg(self.theme.selected_bg)
186                .add_modifier(Modifier::ITALIC)
187        } else {
188            Style::default()
189        });
190        f.render_stateful_widget(list_widget, list_area, self.list.get_state_mut());
191    }
192}