changxi 0.3.0

TUI EPUB Reader
use crate::app::App;
use crate::core::search::SearchType;
use crate::ui::{Component, centered_rect, get_border_type};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::{
    Frame,
    layout::{Constraint, Direction, Layout, Margin, Rect},
    widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Tabs},
};
use ratatui_image::picker::Picker;

pub struct SearchView;

impl Component for SearchView {
    fn render(&self, f: &mut Frame, area: Rect, app: &mut App, _picker: &mut Picker) {
        let border_type = get_border_type(&app.config);

        let area = centered_rect(80, 80, area);

        f.render_widget(Clear, area);

        let block = Block::default()
            .borders(Borders::ALL)
            .border_type(border_type)
            .title(" Search ");
        f.render_widget(block, area);

        let inner_area = area.inner(Margin {
            vertical: 1,
            horizontal: 1,
        });

        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Length(3), // Tabs
                Constraint::Length(3), // Input
                Constraint::Min(0),    // Results
            ])
            .split(inner_area);

        // Render Tabs
        let titles = vec!["Local", "Global", "Bookmarks", "Chapters"];
        let index = match app.search_type {
            SearchType::Local => 0,
            SearchType::Global => 1,
            SearchType::Bookmark => 2,
            SearchType::Chapter => 3,
        };

        let tabs = Tabs::new(titles)
            .block(Block::default().borders(Borders::BOTTOM))
            .select(index)
            .highlight_style(
                Style::default()
                    .fg(Color::Yellow)
                    .add_modifier(Modifier::BOLD),
            );
        f.render_widget(tabs, chunks[0]);

        // Render Input
        let input_title = if app.search_case_sensitive {
            " Query [Case Sensitive] "
        } else {
            " Query [Case Insensitive] "
        };
        let input_block = Block::default()
            .borders(Borders::ALL)
            .border_type(border_type)
            .title(input_title);
        let input = Paragraph::new(app.search_query.as_str()).block(input_block);
        f.render_widget(input, chunks[1]);

        // Set cursor position
        f.set_cursor_position((
            chunks[1].x + app.search_cursor_position as u16 + 1,
            chunks[1].y + 1,
        ));

        // Render Results
        let results: Vec<ListItem> = app
            .search_results
            .iter()
            .map(|res| {
                let mut line = Vec::new();
                if !res.chapter_title.is_empty() {
                    line.push(Span::styled(
                        format!("[{}] ", res.chapter_title),
                        Style::default().fg(Color::Cyan),
                    ));
                }
                line.push(Span::raw(&res.context_before));
                line.push(Span::styled(
                    &res.match_text,
                    Style::default()
                        .fg(Color::Yellow)
                        .add_modifier(Modifier::BOLD),
                ));
                line.push(Span::raw(&res.context_after));

                ListItem::new(Line::from(line))
            })
            .collect();

        let border_type = get_border_type(&app.config);

        let results_list = List::new(results)
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .border_type(border_type)
                    .title(" Results "),
            )
            .highlight_style(
                Style::default()
                    .bg(Color::DarkGray)
                    .add_modifier(Modifier::BOLD),
            )
            .highlight_symbol(">> ");

        f.render_stateful_widget(results_list, chunks[2], &mut app.search_list_state);
    }
}