wiki_tui/components/
search_language_popup.rs1use 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}