diff --git a/src/reader.rs b/src/reader.rs index 0f8aab2..3af9ad7 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -1,3 +1,223 @@ +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);