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