use eframe::egui; use crate::book::Book; use crate::theme::Theme; pub fn recalculate_pages(book: &mut Book, font_size: f32, panel_width: f32, panel_height: f32) { let char_width = font_size * 0.6; let line_height = font_size * 1.5; let chars_per_line = if char_width > 0.0 { (panel_width / char_width).max(1.0) as usize } else { 1 }; let lines_per_page = if line_height > 0.0 { (panel_height / line_height).max(1.0) as usize } else { 1 }; let chars_per_page = chars_per_line * lines_per_page; for section in &mut book.sections { section.pages = calculate_pages(§ion.content, chars_per_page); } } pub struct ReaderAction { pub go_back: bool, pub toggle_theme: bool, pub toggle_bookmark: bool, } pub fn reading_view( ui: &mut egui::Ui, book: &mut Book, current_section: &mut usize, current_page: &mut usize, sidebar_open: &mut bool, font_size: &mut f32, theme: &Theme, _file_path: &str, ) -> ReaderAction { let mut action = ReaderAction { go_back: false, toggle_theme: false, toggle_bookmark: false, }; let panel_size = ui.available_size(); recalculate_pages(book, *font_size, panel_size.x, panel_size.y); // --- Sidebar (TOC) --- if *sidebar_open { egui::SidePanel::left("toc_sidebar") .resizable(true) .default_width(200.0) .show_inside(ui, |ui| { ui.heading("目录"); ui.separator(); render_toc(ui, &book.toc, &book.sections, current_section); }); } // --- Top toolbar --- egui::TopBottomPanel::top("reader_toolbar") .show_inside(ui, |ui| { ui.horizontal(|ui| { if ui.button("← 返回").clicked() { action.go_back = true; } ui.separator(); ui.label(&book.title); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let theme_icon = match theme { Theme::Dark => "☀️", Theme::Light => "🌙", }; if ui.button(theme_icon).clicked() { action.toggle_theme = true; } if ui.button("🔖").clicked() { action.toggle_bookmark = true; } if ui.button("A⁻").clicked() { *font_size = (*font_size - 2.0).max(10.0); } if ui.button("A⁺").clicked() { *font_size = (*font_size + 2.0).min(48.0); } if ui.button("☰").clicked() { *sidebar_open = !*sidebar_open; } }); }); }); // --- Bottom progress bar --- let total_pages = if *current_section < book.sections.len() { let p = &book.sections[*current_section].pages; if p.len() > 1 { p.len() - 1 } else { 0 } } else { 0 }; egui::TopBottomPanel::bottom("reader_progress") .show_inside(ui, |ui| { ui.horizontal(|ui| { let section_title = book.sections.get(*current_section) .map(|s| s.title.as_str()) .unwrap_or(""); ui.label(section_title); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let label = if total_pages > 0 { format!("{}/{}", *current_page + 1, total_pages) } else { "1/1".to_string() }; ui.label(label); let mut progress = if total_pages > 0 { *current_page as f32 / total_pages as f32 } else { 0.0 }; if ui.add(egui::Slider::new(&mut progress, 0.0..=1.0).text("")).changed() && total_pages > 0 { *current_page = (progress * total_pages as f32).round() as usize; } }); }); }); // --- Center text area --- egui::CentralPanel::default().show_inside(ui, |ui| { let (rect, response) = ui.allocate_at_least(ui.available_size(), egui::Sense::click()); if let Some(section) = book.sections.get(*current_section) { if *current_page < section.pages.len().saturating_sub(1) { let start = section.pages[*current_page]; let end = section.pages[*current_page + 1]; let text: String = section.content.chars().skip(start).take(end - start).collect(); ui.put(rect, |ui: &mut egui::Ui| { let color = match theme { Theme::Dark => egui::Color32::WHITE, Theme::Light => egui::Color32::BLACK, }; ui.add( egui::Label::new( egui::RichText::new(&text).size(*font_size).color(color) ).wrap() ) }); } } // Click navigation if response.clicked() { if let Some(click_pos) = response.interact_pointer_pos() { let x_ratio = (click_pos.x - rect.min.x) / rect.width(); if x_ratio < 0.3 { if *current_page > 0 { *current_page -= 1; } else if *current_section > 0 { *current_section -= 1; *current_page = book.sections[*current_section] .pages.len().saturating_sub(2); } } else if x_ratio > 0.7 { if *current_page + 1 < book.sections[*current_section] .pages.len().saturating_sub(1) { *current_page += 1; } else if *current_section + 1 < book.sections.len() { *current_section += 1; *current_page = 0; } } } } // Keyboard navigation if ui.input(|i| i.key_pressed(egui::Key::ArrowRight)) { if *current_page + 1 < book.sections[*current_section] .pages.len().saturating_sub(1) { *current_page += 1; } else if *current_section + 1 < book.sections.len() { *current_section += 1; *current_page = 0; } } if ui.input(|i| i.key_pressed(egui::Key::ArrowLeft)) { if *current_page > 0 { *current_page -= 1; } else if *current_section > 0 { *current_section -= 1; *current_page = book.sections[*current_section] .pages.len().saturating_sub(2); } } }); action } fn render_toc( ui: &mut egui::Ui, entries: &[crate::book::TocEntry], _sections: &[crate::book::Section], current_section: &mut usize, ) { for entry in entries { let is_current = entry.section == *current_section; let response = if is_current { ui.colored_label(egui::Color32::YELLOW, &entry.label) } else { ui.label(&entry.label) }; if response.clicked() { *current_section = entry.section; } if !entry.children.is_empty() { ui.indent(&entry.label, |ui| { render_toc(ui, &entry.children, _sections, current_section); }); } } } pub fn calculate_pages(text: &str, chars_per_page: usize) -> Vec { let mut pages = Vec::new(); pages.push(0); if text.is_empty() || chars_per_page == 0 { return pages; } let total_chars = text.chars().count(); if total_chars <= chars_per_page { pages.push(total_chars); return pages; } let mut pos = 0; while pos < total_chars { pos = (pos + chars_per_page).min(total_chars); pages.push(pos); } pages } #[cfg(test)] mod tests { use super::*; #[test] fn test_pagination_empty() { let pages = calculate_pages("", 100); assert_eq!(pages, vec![0]); } #[test] fn test_pagination_shorter_than_page() { let pages = calculate_pages("Hello World", 100); assert_eq!(pages, vec![0, 11]); } #[test] fn test_pagination_exact_fit() { let pages = calculate_pages("ABCD", 4); assert_eq!(pages, vec![0, 4]); } #[test] fn test_pagination_multiple_pages() { let text = "A".repeat(100); let pages = calculate_pages(&text, 30); assert_eq!(pages, vec![0, 30, 60, 90, 100]); } #[test] fn test_pagination_single_char() { let pages = calculate_pages("A", 1); assert_eq!(pages, vec![0, 1]); } #[test] fn test_pagination_zero_chars_per_page() { let pages = calculate_pages("test", 0); assert_eq!(pages, vec![0]); } }