changxi 0.3.0

TUI EPUB Reader
use crate::app::{App, ViewMode};
use crate::core::Reader;
use crate::ui::components::reading::render_chapter_content;
use crate::ui::components::{
    BookmarkBrowser, ChapterBrowser, CoverView, Footer, Header, LibraryView, SearchView,
};
use crate::ui::{Component, View, count_lines};
use ratatui::{
    Frame,
    layout::{Constraint, Direction, Layout, Margin},
    widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState},
};
use ratatui_image::picker::Picker;

pub struct BookView {
    header: Header,
    footer: Footer,
    cover_view: CoverView,
    chapter_browser: ChapterBrowser,
    bookmark_browser: BookmarkBrowser,
    library_view: LibraryView,
    search_view: SearchView,
}

impl BookView {
    pub fn new() -> Self {
        Self {
            header: Header,
            footer: Footer,
            cover_view: CoverView,
            chapter_browser: ChapterBrowser,
            bookmark_browser: BookmarkBrowser,
            library_view: LibraryView,
            search_view: SearchView,
        }
    }
}

impl View for BookView {
    fn render(&self, f: &mut Frame, app: &mut App, picker: &mut Picker) {
        let size = f.area();

        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Length(3),
                Constraint::Min(0),
                Constraint::Length(3),
            ])
            .split(size);

        // Header always renders at the top
        self.header.render(f, chunks[0], app, picker);

        // Main content depends on app mode
        match app.mode {
            ViewMode::Reading | ViewMode::Visual => {
                if let Ok(chapter) = app.reader.get_chapter(app.current_chapter) {
                    let panes = Layout::default()
                        .direction(Direction::Horizontal)
                        .constraints([
                            Constraint::Percentage(50),
                            Constraint::Length(1),
                            Constraint::Percentage(50),
                        ])
                        .split(chunks[1]);

                    let width = panes[0].width.saturating_sub(2) as usize;
                    let viewport_height = panes[0].height.saturating_sub(2) as usize;

                    // Elements heights calculation
                    let mut heights = Vec::new();
                    for element in &chapter.elements {
                        heights.push(count_lines(element, width));
                    }
                    let total_height: usize = heights.iter().sum();
                    app.total_height = total_height;
                    app.viewport_height = viewport_height;

                    // Handle scrolling flags
                    if app.scroll_to_end && total_height > viewport_height {
                        app.scroll = total_height.saturating_sub(2 * viewport_height);
                        app.scroll_to_end = false;
                    }
                    if app.scroll_to_start {
                        app.scroll = 0;
                        app.scroll_to_start = false;
                    }

                    let chapter_title = format!(" {} ", chapter.title);

                    // Render Left Page
                    render_chapter_content(
                        f,
                        panes[0],
                        app,
                        picker,
                        &chapter,
                        app.scroll,
                        &heights,
                        viewport_height,
                        Some(chapter_title),
                        true,
                    );

                    // Render Right Page (if there's more content)
                    let right_scroll = app.scroll + viewport_height;
                    if right_scroll < total_height {
                        render_chapter_content(
                            f,
                            panes[2],
                            app,
                            picker,
                            &chapter,
                            right_scroll,
                            &heights,
                            viewport_height,
                            None,
                            true,
                        );
                    }

                    // Render Scrollbar
                    let max_scroll = total_height.saturating_sub(viewport_height);
                    let scroll_position = app.scroll.min(max_scroll);
                    let mut scrollbar_state = ScrollbarState::new(total_height)
                        .position(scroll_position)
                        .viewport_content_length(viewport_height);
                    f.render_stateful_widget(
                        Scrollbar::new(ScrollbarOrientation::VerticalRight)
                            .begin_symbol(Some(""))
                            .end_symbol(Some("")),
                        chunks[1].inner(Margin {
                            vertical: 1,
                            horizontal: 0,
                        }),
                        &mut scrollbar_state,
                    );
                }
            }
            ViewMode::Cover => self.cover_view.render(f, chunks[1], app, picker),
            ViewMode::ChapterBrowser => self.chapter_browser.render(f, chunks[1], app, picker),
            ViewMode::BookmarkBrowser | ViewMode::BookmarkRenaming => {
                self.bookmark_browser.render(f, chunks[1], app, picker)
            }
            ViewMode::Library => self.library_view.render(f, chunks[1], app, picker),
            ViewMode::Search => self.search_view.render(f, chunks[1], app, picker),
        }

        // Footer always renders at the bottom
        self.footer.render(f, chunks[2], app, picker);
    }
}