feat: add typography style system with profiles, alignment, line spacing, first-line indent

This commit is contained in:
Developer
2026-05-14 21:02:34 +08:00
parent d69229b1ca
commit 7d056e2670
5 changed files with 160 additions and 18 deletions

View File

@@ -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();
}

View File

@@ -5,6 +5,7 @@ mod book;
mod font;
mod persistence;
mod reader;
mod style;
mod texture;
mod theme;

View File

@@ -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()
);
},
);
});
}
}

84
src/style.rs Normal file
View File

@@ -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<StyleProfile> {
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()
}

View File

@@ -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<crate::style::StyleProfile>,
pub recent_files: Vec<String>,
pub reading_positions: std::collections::HashMap<String, ReadingPosition>,
pub bookmarks: std::collections::HashMap<String, Vec<Bookmark>>,
@@ -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(),