diff --git a/src/app.rs b/src/app.rs index 21a8ca4..5c07217 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,6 @@ use crate::book::Book; use crate::persistence; +use crate::style::StyleProfile; use crate::theme::{self, Settings}; use eframe::egui; use std::path::PathBuf; @@ -102,6 +103,12 @@ impl App { let _ = persistence::save_settings(&self.settings_dir, &self.settings); } + fn active_profile(&self) -> &StyleProfile { + let idx = self.settings.profiles.iter().position(|p| p.name == self.settings.active_profile); + let idx = idx.unwrap_or(0); + &self.settings.profiles[idx] + } + fn save_reading_position(&mut self) { if let Some(ref path) = self.state.file_path { let path_str = path.to_string_lossy().to_string(); @@ -181,6 +188,10 @@ impl eframe::App for App { .map(|p| p.to_string_lossy().to_string()) .unwrap_or_default(); + let mut style = self.active_profile().clone(); + let theme_copy = self.settings.theme; + let use_kraft = self.settings.use_kraft_bg; + egui::CentralPanel::default().show(ctx, |ui| { let book = self.state.book.as_mut().unwrap(); let action = crate::reader::reading_view( @@ -189,11 +200,11 @@ impl eframe::App for App { &mut self.state.current_section, &mut self.state.current_page, &mut self.state.sidebar_open, - &mut self.settings.font_size, - &self.settings.theme, + &mut style, + &theme_copy, &file_path, self.kraft_texture.as_ref(), - self.settings.use_kraft_bg, + use_kraft, ); if action.go_back { @@ -203,6 +214,14 @@ impl eframe::App for App { self.state.current_page = 0; } + if action.cycle_profile { + let idx = self.settings.profiles.iter() + .position(|p| p.name == self.settings.active_profile) + .unwrap_or(0); + let next = (idx + 1) % self.settings.profiles.len(); + self.settings.active_profile = self.settings.profiles[next].name.clone(); + } + if action.toggle_kraft_bg { self.settings.use_kraft_bg = !self.settings.use_kraft_bg; } @@ -224,6 +243,18 @@ impl eframe::App for App { } }); + // Sync style changes back to active profile (outside closure) + if let Some(p) = self.settings.profiles.iter_mut() + .find(|p| p.name == self.settings.active_profile) + { + p.font_size = style.font_size; + p.alignment = style.alignment; + p.line_spacing = style.line_spacing; + p.paragraph_spacing = style.paragraph_spacing; + p.first_line_indent = style.first_line_indent; + } + self.settings.font_size = style.font_size; + self.save_reading_position(); self.save_settings(); } diff --git a/src/main.rs b/src/main.rs index 3bc425c..255b9d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod book; mod font; mod persistence; mod reader; +mod style; mod texture; mod theme; diff --git a/src/reader.rs b/src/reader.rs index 2e9de30..c2392e9 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -1,10 +1,10 @@ use eframe::egui; use crate::book::Book; +use crate::style::{StyleProfile, TextAlignment}; use crate::theme::{self, Theme}; -pub fn recalculate_pages(book: &mut Book, font_size: f32, panel_width: f32, panel_height: f32) { +pub fn recalculate_pages(book: &mut Book, font_size: f32, line_height: 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 { @@ -26,6 +26,7 @@ pub struct ReaderAction { pub toggle_theme: bool, pub toggle_bookmark: bool, pub toggle_kraft_bg: bool, + pub cycle_profile: bool, pub page_next: bool, pub page_prev: bool, } @@ -36,7 +37,7 @@ pub fn reading_view( current_section: &mut usize, current_page: &mut usize, sidebar_open: &mut bool, - font_size: &mut f32, + style: &mut StyleProfile, theme: &Theme, _file_path: &str, kraft_texture: Option<&egui::TextureHandle>, @@ -47,12 +48,14 @@ pub fn reading_view( toggle_theme: false, toggle_bookmark: false, toggle_kraft_bg: false, + cycle_profile: false, page_next: false, page_prev: false, }; let panel_size = ui.available_size(); - recalculate_pages(book, *font_size, panel_size.x, panel_size.y); + let line_h = style.line_height(); + recalculate_pages(book, style.font_size, line_h, panel_size.x, panel_size.y); let colors = theme::reader_colors(theme); @@ -90,10 +93,16 @@ pub fn reading_view( action.toggle_bookmark = true; } if ui.button("A⁻").on_hover_text("缩小字体").clicked() { - *font_size = (*font_size - 2.0).max(10.0); + style.font_size = (style.font_size - 2.0).max(10.0); } if ui.button("A⁺").on_hover_text("放大字体").clicked() { - *font_size = (*font_size + 2.0).min(48.0); + style.font_size = (style.font_size + 2.0).min(48.0); + } + if ui.button(&style.name) + .on_hover_text(format!("样式: {} (点击切换)", style.name)) + .clicked() + { + action.cycle_profile = true; } let kraft_icon = if use_kraft_bg { "📄" } else { "📋" }; if ui.button(kraft_icon).on_hover_text("牛皮纸背景").clicked() { @@ -189,15 +198,28 @@ pub fn reading_view( 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(text_rect, |ui: &mut egui::Ui| { - ui.add( - egui::Label::new( - egui::RichText::new(&text) - .size(*font_size) - .color(colors.text) - ).wrap() - ) + let raw_text: String = section.content.chars().skip(start).take(end - start).collect(); + let indented = crate::style::apply_indent(&raw_text, style.first_line_indent); + + 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| { + ui.add( + egui::Label::new( + egui::RichText::new(&indented) + .size(style.font_size) + .line_height(Some(style.line_height())) + .color(colors.text) + ).wrap() + ); + }, + ); }); } } diff --git a/src/style.rs b/src/style.rs new file mode 100644 index 0000000..0e9d9a3 --- /dev/null +++ b/src/style.rs @@ -0,0 +1,84 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum TextAlignment { + Left, + Center, + Right, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StyleProfile { + pub name: String, + pub alignment: TextAlignment, + pub line_spacing: f32, + pub paragraph_spacing: f32, + pub first_line_indent: f32, + pub font_size: f32, +} + +impl StyleProfile { + pub fn presets() -> Vec { + vec![ + StyleProfile { + name: "Kindle 默认".into(), + alignment: TextAlignment::Left, + line_spacing: 1.8, + paragraph_spacing: 1.0, + first_line_indent: 2.0, + font_size: 20.0, + }, + StyleProfile { + name: "紧凑".into(), + alignment: TextAlignment::Left, + line_spacing: 1.4, + paragraph_spacing: 0.5, + first_line_indent: 2.0, + font_size: 18.0, + }, + StyleProfile { + name: "宽松".into(), + alignment: TextAlignment::Left, + line_spacing: 2.2, + paragraph_spacing: 1.5, + first_line_indent: 2.0, + font_size: 22.0, + }, + StyleProfile { + name: "居中".into(), + alignment: TextAlignment::Center, + line_spacing: 1.6, + paragraph_spacing: 1.0, + first_line_indent: 0.0, + font_size: 20.0, + }, + ] + } + + pub fn line_height(&self) -> f32 { + self.font_size * self.line_spacing + } + +} + +pub fn apply_indent(text: &str, indent_chars: f32) -> String { + if indent_chars <= 0.0 { + return text.to_string(); + } + let indent = "\u{3000}".repeat(indent_chars as usize); + let mut result = String::with_capacity(text.len()); + let mut para_start = true; + for ch in text.chars() { + if ch == '\n' { + result.push('\n'); + para_start = true; + } else if para_start { + result.push_str(&indent); + result.push(ch); + para_start = false; + } else { + result.push(ch); + } + } + result.trim().to_string() +} diff --git a/src/theme.rs b/src/theme.rs index 4c44a7f..5998e4c 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -171,6 +171,8 @@ pub struct Settings { pub font_size: f32, pub theme: Theme, pub use_kraft_bg: bool, + pub active_profile: String, + pub profiles: Vec, pub recent_files: Vec, pub reading_positions: std::collections::HashMap, pub bookmarks: std::collections::HashMap>, @@ -183,6 +185,8 @@ impl Default for Settings { font_size: 20.0, theme: Theme::Light, use_kraft_bg: false, + active_profile: "Kindle 默认".into(), + profiles: crate::style::StyleProfile::presets(), recent_files: Vec::new(), reading_positions: std::collections::HashMap::new(), bookmarks: std::collections::HashMap::new(),