Skip to main content

wiki_tui/components/
search_bar.rs

1use std::sync::Arc;
2
3use crossterm::event::KeyEvent;
4use ratatui::{
5    prelude::Rect,
6    style::{Color, Modifier, Style},
7    text::Text,
8};
9use tui_input::{backend::crossterm::EventHandler, Input};
10
11use crate::{
12    action::{Action, ActionResult, SearchAction},
13    config::{Config, Theme},
14    terminal::Frame,
15    ui::centered_rect,
16};
17
18use super::Component;
19
20const EMPTY_PROMPT: &str = "Search Wikipedia";
21const SEARCH_BAR_X: u16 = 50;
22
23pub const SEARCH_BAR_HEIGTH: u16 = 3;
24
25#[derive(Default)]
26pub struct SearchBarComponent {
27    input: Input,
28    config: Arc<Config>,
29    theme: Arc<Theme>,
30    pub is_focussed: bool,
31}
32
33impl SearchBarComponent {
34    pub fn clear(&mut self) {
35        self.input = Input::default();
36    }
37
38    pub fn submit(&self) -> Action {
39        Action::Search(SearchAction::StartSearch(self.input.value().to_string()))
40    }
41}
42
43impl Component for SearchBarComponent {
44    fn init(
45        &mut self,
46        _: tokio::sync::mpsc::UnboundedSender<Action>,
47        config: Arc<Config>,
48        theme: Arc<Theme>,
49    ) -> anyhow::Result<()> {
50        self.config = config;
51        self.theme = theme;
52        Ok(())
53    }
54
55    fn handle_key_events(&mut self, key: KeyEvent) -> ActionResult {
56        if self.config.bindings.global.submit.matches_event(key) {
57            return Action::SubmitSearchBar.into();
58        }
59
60        if self
61            .config
62            .bindings
63            .global
64            .exit_search_bar
65            .matches_event(key)
66        {
67            return Action::ExitSearchBar.into();
68        }
69
70        self.input.handle_event(&crossterm::event::Event::Key(key));
71        ActionResult::consumed()
72    }
73
74    fn render(&mut self, f: &mut Frame<'_>, area: Rect) {
75        let scroll = self.input.visual_scroll(area.width as usize);
76        let value = self.input.value();
77
78        let mut block = self.theme.default_block();
79        if self.is_focussed {
80            block = block.border_style(
81                Style::default()
82                    .fg(self.theme.border_highlight_fg)
83                    .bg(self.theme.border_highlight_bg),
84            )
85        }
86
87        let input = if value.is_empty() {
88            self.theme.default_paragraph(Text::styled(
89                EMPTY_PROMPT,
90                Style::default()
91                    .fg(Color::Gray)
92                    .add_modifier(Modifier::ITALIC),
93            ))
94        } else {
95            self.theme
96                .default_paragraph(self.input.value())
97                .scroll((0, scroll as u16))
98        }
99        .block(block);
100
101        let input_area = centered_rect(area, SEARCH_BAR_X, 100);
102        f.render_widget(input, input_area);
103        if self.is_focussed {
104            f.set_cursor_position((
105                // Put cursor past the end of the input text
106                input_area.x + ((self.input.visual_cursor()).max(scroll) - scroll) as u16 + 1,
107                // Move one line down, from the border to the input line
108                input_area.y + 1,
109            ));
110        }
111    }
112}