use eframe::egui; use crate::book::Book; use crate::style::{StyleProfile, TextAlignment}; use crate::theme::{self, BgType, Theme}; pub fn recalculate_pages(book: &mut Book, font_size: f32, line_height: f32, panel_width: f32, panel_height: f32, style: &StyleProfile) { let char_width = font_size * 1.0; let safety = 0.95; let chars_per_line = if char_width > 0.0 { ((panel_width / char_width) * safety).max(1.0) as usize } else { 1 }; let lines_per_page = if line_height > 0.0 { ((panel_height / line_height) * safety).max(1.0) as usize } else { 1 }; let chars_per_page = chars_per_line * lines_per_page; for section in &mut book.sections { let styled = style.apply_to_text(§ion.content); section.pages = calculate_pages(&styled, chars_per_page); } } pub struct ReaderAction { pub go_back: bool, pub toggle_theme: bool, pub toggle_bookmark: bool, pub switch_bg: Option, pub switch_to_profile: Option, pub page_next: bool, pub page_prev: bool, } pub fn reading_view( ui: &mut egui::Ui, book: &mut Book, current_section: &mut usize, current_page: &mut usize, sidebar_open: &mut bool, style: &mut StyleProfile, theme: &Theme, bg_type: BgType, _file_path: &str, profile_names: &[String], bookmarks: &[crate::theme::Bookmark], ) -> (ReaderAction, Option) { let mut action = ReaderAction { go_back: false, toggle_theme: false, toggle_bookmark: false, switch_bg: None, switch_to_profile: None, page_next: false, page_prev: false, }; let mut jump_to_bookmark: Option = None; let colors = theme::reader_colors(theme); let has_bookmark = bookmarks.iter().any(|b| b.section == *current_section && b.page == *current_page); // --- Sidebar (TOC + Bookmarks) --- let sidebar_tab_id = ui.make_persistent_id("sidebar_tab"); let mut sidebar_tab: usize = ui.data_mut(|d| *d.get_temp_mut_or_default::(sidebar_tab_id)); if *sidebar_open { egui::SidePanel::left("toc_sidebar") .resizable(true) .default_width(240.0) .show_inside(ui, |ui| { ui.horizontal(|ui| { let toc_response = ui.selectable_label(sidebar_tab == 0, "📋 目录"); let bm_response = ui.selectable_label(sidebar_tab == 1, format!("🔖 书签 ({})", bookmarks.len()) ); if toc_response.clicked() { sidebar_tab = 0; ui.data_mut(|d| *d.get_temp_mut_or_default::(sidebar_tab_id) = 0); } if bm_response.clicked() { sidebar_tab = 1; ui.data_mut(|d| *d.get_temp_mut_or_default::(sidebar_tab_id) = 1); } }); ui.separator(); if sidebar_tab == 0 { render_toc(ui, &book.toc, current_section, current_page); } else { render_bookmarks(ui, bookmarks, &mut jump_to_bookmark); } }); } // --- Top toolbar --- egui::TopBottomPanel::top("reader_toolbar") .show_inside(ui, |ui| { ui.horizontal(|ui| { if ui.button("← 返回").on_hover_text("返回书架").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, theme_hint) = match theme { Theme::Dark => ("🌞", "切换到浅色主题"), Theme::Light => ("🌙", "切换到夜间主题"), Theme::Sepia => ("📜", "切换到棕褐色主题"), }; if ui.button(theme_icon).on_hover_text(theme_hint).clicked() { action.toggle_theme = true; } let bookmark_icon = if has_bookmark { "🔴" } else { "🔖" }; let bookmark_hint = if has_bookmark { "移除书签" } else { "添加书签" }; if ui.button(bookmark_icon).on_hover_text(bookmark_hint).clicked() { action.toggle_bookmark = true; } if ui.button("A⁻").on_hover_text("缩小字体").clicked() { style.font_size = (style.font_size - 2.0).max(10.0); } if ui.button("A⁺").on_hover_text("放大字体").clicked() { style.font_size = (style.font_size + 2.0).min(48.0); } egui::ComboBox::from_id_salt("profile_selector") .width(110.0) .selected_text(&style.name) .show_ui(ui, |ui| { for name in profile_names { let selected = *name == style.name; if ui.selectable_label(selected, name).clicked() { action.switch_to_profile = Some(name.clone()); } } }); egui::ComboBox::from_id_salt("bg_type_selector") .width(100.0) .selected_text(bg_type.label()) .show_ui(ui, |ui| { for &label in BgType::ALL.iter() { let selected = bg_type.label() == label; if ui.selectable_label(selected, label).clicked() { action.switch_bg = Some(theme::BgType::from_label(label)); } } }); if ui.button("☰").on_hover_text("打开/关闭目录").clicked() { *sidebar_open = !*sidebar_open; } }); }); }); // 在工具条之后、进度条之前用当前可用高度重新分页 let panel_size = ui.available_size(); let recalc_width = (panel_size.x - 48.0).max(100.0); let recalc_height = (panel_size.y - 60.0).max(200.0); recalculate_pages(book, style.font_size, style.line_height(), recalc_width, recalc_height, style); // --- 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| { // Page turn buttons (right side of bottom bar) let prev_enabled = *current_page > 0 || *current_section > 0; let next_enabled = (*current_page + 1 < book.sections[*current_section] .pages.len().saturating_sub(1)) || (*current_section + 1 < book.sections.len()); let next_btn = egui::Button::new( egui::RichText::new("下一页 ▶").size(13.0) ).min_size(egui::vec2(100.0, 28.0)); let prev_btn = egui::Button::new( egui::RichText::new("◀ 上一页").size(13.0) ).min_size(egui::vec2(100.0, 28.0)); if !next_enabled { ui.add_enabled(false, next_btn); } else if ui.add(next_btn).on_hover_text("下一页 (→键)").clicked() { action.page_next = true; } ui.add_space(8.0); if !prev_enabled { ui.add_enabled(false, prev_btn); } else if ui.add(prev_btn).on_hover_text("上一页 (←键)").clicked() { action.page_prev = true; } ui.separator(); 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 available = ui.available_size(); let (rect, response) = ui.allocate_at_least(available, egui::Sense::click()); // Add reading margins (inset) let inset = 24.0; let text_rect = egui::Rect::from_min_size( egui::pos2(rect.min.x + inset, rect.min.y), egui::vec2((rect.width() - inset * 2.0).max(100.0), rect.height()), ); // 分页已在工具条之后统一计算(使用当前可用高度),此处不再重复 if let Some(section) = book.sections.get(*current_section) { let max_page = section.pages.len().saturating_sub(2); if *current_page > max_page { *current_page = max_page; } } 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 styled_full = style.apply_to_text(§ion.content); let page_text: String = styled_full.chars().skip(start).take(end - start).collect(); let align = match style.alignment { TextAlignment::Left => egui::Align::LEFT, TextAlignment::Center => egui::Align::Center, TextAlignment::Right => egui::Align::RIGHT, }; ui.allocate_new_ui(egui::UiBuilder::new().max_rect(text_rect), |ui| { ui.with_layout( egui::Layout::top_down(align).with_main_justify(false), |ui| { let mut is_heading = false; let mut text_start = 0usize; let char_indices: Vec<_> = page_text.char_indices().collect(); let mut idx = 0; while idx < char_indices.len() { let (byte_pos, ch) = char_indices[idx]; if ch == '\x01' || ch == '\x02' { if byte_pos > text_start { let text = &page_text[text_start..byte_pos]; let mut rt = egui::RichText::new(text) .size(style.font_size) .color(colors.text); if is_heading { rt = rt.strong(); } ui.add(egui::Label::new(rt).wrap()); } is_heading = ch == '\x01'; idx += 1; if idx < char_indices.len() { text_start = char_indices[idx].0; } else { text_start = page_text.len(); } } else { idx += 1; } } if text_start < page_text.len() { let text = &page_text[text_start..]; let mut rt = egui::RichText::new(text) .size(style.font_size) .color(colors.text); if is_heading { rt = rt.strong(); } ui.add(egui::Label::new(rt).wrap()); } }, ); }); if has_bookmark { let painter = ui.painter(); let bookmark_pos = egui::pos2(rect.max.x - 30.0, rect.min.y + 10.0); painter.text( bookmark_pos, egui::Align2::RIGHT_TOP, "🔴", egui::FontId::proportional(18.0), egui::Color32::RED, ); } } } // 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 { action.page_prev = true; } else if x_ratio > 0.7 { action.page_next = true; } } } // Keyboard navigation if ui.input(|i| i.key_pressed(egui::Key::ArrowRight)) { action.page_next = true; } if ui.input(|i| i.key_pressed(egui::Key::ArrowLeft)) { action.page_prev = true; } }); (action, jump_to_bookmark) } fn render_toc( ui: &mut egui::Ui, entries: &[crate::book::TocEntry], current_section: &mut usize, current_page: &mut usize, ) { for entry in entries { let is_current = entry.section == *current_section; let label_text = if is_current { egui::RichText::new(&entry.label).color(egui::Color32::YELLOW).strong() } else { egui::RichText::new(&entry.label) }; let response = ui.add( egui::Button::new(label_text) .frame(false) .wrap() ); if response.clicked() { *current_section = entry.section; *current_page = 0; } response.on_hover_text(format!("跳转到: {}", entry.label)); if !entry.children.is_empty() { ui.indent(&entry.label, |ui| { render_toc(ui, &entry.children, current_section, current_page); }); } } } fn render_bookmarks( ui: &mut egui::Ui, bookmarks: &[crate::theme::Bookmark], jump_to_bookmark: &mut Option, ) { if bookmarks.is_empty() { ui.horizontal(|ui| { ui.label("暂无书签"); }); ui.label("点击工具栏 🔖 按钮添加书签"); return; } for (idx, bm) in bookmarks.iter().enumerate() { let label_text = egui::RichText::new(&bm.label).size(13.0); if ui.add( egui::Button::new(label_text) .frame(false) .wrap() ).on_hover_text("点击跳转到该书签").clicked() { *jump_to_bookmark = Some(idx); } } } 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 chars: Vec = text.chars().collect(); let total_chars = chars.len(); if total_chars <= chars_per_page { pages.push(total_chars); return pages; } let mut pos: usize = 0; while pos < total_chars { let next = pos + chars_per_page; if next >= total_chars { pages.push(total_chars); break; } // Search backward from next for paragraph (\n\n) or line (\n) breaks let search_start = pos + chars_per_page / 2; let search_end = (next + chars_per_page / 2).min(total_chars); let mut split = next; // Prefer double newline (paragraph), then single newline let mut found = false; for i in (search_start..search_end).rev() { if chars[i] == '\n' && i > 0 && chars[i - 1] == '\n' { split = i - 1; found = true; break; } } if !found { for i in (search_start..search_end).rev() { if chars[i] == '\n' { split = i + 1; break; } } } if split <= pos { split = next; } pos = split.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]); } }