feat: add create_style function with light/dark theme support
This commit is contained in:
1525
docs/superpowers/plans/2026-05-13-epub-reader.md
Normal file
1525
docs/superpowers/plans/2026-05-13-epub-reader.md
Normal file
File diff suppressed because it is too large
Load Diff
162
docs/superpowers/specs/2026-05-13-epub-reader-design.md
Normal file
162
docs/superpowers/specs/2026-05-13-epub-reader-design.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# ePub Reader — Design Document
|
||||
|
||||
## Overview
|
||||
|
||||
A lightweight ePub reader desktop application built with Rust + egui, compiled as a single Windows executable (x86_64-pc-windows-gnu / MSYS2+MinGW). No console window. Built-in Chinese font support. Kindle-like reading experience with medium feature set.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Choice |
|
||||
|---|---|
|
||||
| GUI Framework | `eframe` (egui official framework) |
|
||||
| Rendering Backend | `wgpu` |
|
||||
| ePub Parsing | `epub-rs` |
|
||||
| Font | Bundled Noto Sans SC (or similar open-source Chinese font) |
|
||||
| Build Target | `x86_64-pc-windows-gnu` |
|
||||
| Binary | Single `.exe`, `#![windows_subsystem = "windows"]` |
|
||||
|
||||
## Core Data Model
|
||||
|
||||
```rust
|
||||
struct AppState {
|
||||
book: Option<Book>,
|
||||
current_section: usize,
|
||||
current_page: usize,
|
||||
settings: Settings,
|
||||
bookmarks: Vec<Bookmark>,
|
||||
sidebar_open: bool,
|
||||
}
|
||||
|
||||
struct Book {
|
||||
title: String,
|
||||
author: String,
|
||||
cover: Option<Vec<u8>>,
|
||||
sections: Vec<Section>,
|
||||
toc: Vec<TocEntry>,
|
||||
}
|
||||
|
||||
struct Section {
|
||||
title: String,
|
||||
content: String, // plain text (HTML tags stripped)
|
||||
pages: Vec<usize>, // character offsets for page boundaries
|
||||
}
|
||||
|
||||
struct Settings {
|
||||
font_size: f32, // default 20.0
|
||||
theme: Theme,
|
||||
}
|
||||
|
||||
enum Theme { Light, Dark }
|
||||
|
||||
struct Bookmark {
|
||||
section: usize,
|
||||
page: usize,
|
||||
label: String,
|
||||
timestamp: i64,
|
||||
}
|
||||
|
||||
struct TocEntry {
|
||||
label: String,
|
||||
section: usize,
|
||||
children: Vec<TocEntry>,
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ eframe::App │
|
||||
│ ┌──────────┐ ┌───────────────────────┐ │
|
||||
│ │ WelcomePg │ │ ReadingView │ │
|
||||
│ │ (no book) │ │ ├─ TopToolbar │ │
|
||||
│ │ ──────── │ │ ├─ Sidebar (TOC) │ │
|
||||
│ │ open btn │ │ ├─ TextArea │ │
|
||||
│ │ recents │ │ └─ BottomProgressBar │ │
|
||||
│ └──────────┘ └───────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ AppState │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## UI Layout
|
||||
|
||||
### Welcome Screen (no book open)
|
||||
- Centered "Open ePub File" button
|
||||
- Recently opened books list (read from settings file)
|
||||
|
||||
### Reading Screen (Kindle-like)
|
||||
- **Top toolbar**: back button, book title, font size controls, menu, bookmark toggle, theme toggle
|
||||
- **Left sidebar** (collapsible): table of contents tree
|
||||
- **Center**: text content area with pagination
|
||||
- Click left 30% → prev page, right 30% → next page, center 40% → toggle toolbar visibility
|
||||
- Keyboard: ← → for prev/next page
|
||||
- **Bottom bar**: current chapter name, progress slider, page number indicator
|
||||
|
||||
## Features (Medium Scope)
|
||||
|
||||
### Core
|
||||
1. Open `.epub` file via native file dialog
|
||||
2. Parse ePub (OPF manifest, NCX/TOC, Spine)
|
||||
3. Render chapter content with pagination
|
||||
4. Next/previous page navigation (click & keyboard)
|
||||
5. Table of contents sidebar with chapter navigation
|
||||
6. Remember last reading position (bookmark per file)
|
||||
|
||||
### Settings & Display
|
||||
7. Font size adjustment (A⁺/A⁻)
|
||||
8. Light/Dark theme toggle
|
||||
9. Chinese font support (bundled Noto Sans SC)
|
||||
|
||||
### Bookmarks
|
||||
10. Add/remove bookmarks at current page
|
||||
11. Bookmark list view
|
||||
|
||||
### Persistence
|
||||
12. `settings.json` saves: font_size, theme, recent files, reading positions, bookmarks
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. User clicks "Open" → native file dialog → select `.epub`
|
||||
2. `EpubLoader` reads zip, extracts OPF metadata, reads spine order, parses NCX for TOC
|
||||
3. For each spine item: extract HTML content, strip tags → plain text stored in `Section.content`
|
||||
4. On section load: calculate pagination based on current font_size + panel width → store page offsets
|
||||
5. On page change: slice `Section.content[pages[current_page]..pages[current_page+1]]` → render to egui `Label`
|
||||
6. On font_size change: recalculate all pagination for current section
|
||||
7. On theme toggle: switch `egui::Style` between light/dark visuals
|
||||
|
||||
## Pagination Algorithm
|
||||
|
||||
```
|
||||
fn paginate(text: &str, font_size: f32, panel_width: f32) -> Vec<usize>
|
||||
chars_per_line = floor(panel_width / (font_size * 0.6))
|
||||
lines_per_page = floor(panel_height / (font_size * 1.5))
|
||||
chars_per_page = chars_per_line * lines_per_page
|
||||
Iterate text, split at chars_per_page boundaries (prefer word/char boundaries)
|
||||
```
|
||||
|
||||
## File Storage
|
||||
|
||||
- `settings.json` (next to exe OR in standard config dir)
|
||||
- font_size, theme
|
||||
- recent_files: Vec<{path, title}>
|
||||
- reading_positions: HashMap<path, {section, page}>
|
||||
- bookmarks: HashMap<path, Vec<Bookmark>>
|
||||
- Last window size/position
|
||||
|
||||
## Error Handling
|
||||
|
||||
- ePub parsing failure → show error dialog with message
|
||||
- File not found (recent list) → remove from list silently
|
||||
- Font loading failure → fall back to system monospace
|
||||
- Settings file corruption → reset to defaults, log warning
|
||||
|
||||
## Out of Scope (v1)
|
||||
|
||||
- Text search within book
|
||||
- Highlighting / annotations
|
||||
- Notes/export
|
||||
- TTS
|
||||
- Epub with complex CSS styling / reflowable layout math
|
||||
- DRM-protected ePubs
|
||||
27
src/theme.rs
27
src/theme.rs
@@ -1,3 +1,5 @@
|
||||
use eframe::egui;
|
||||
use eframe::egui::Style;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
@@ -12,6 +14,19 @@ impl Default for Theme {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_style(theme: &Theme) -> Style {
|
||||
match theme {
|
||||
Theme::Light => Style {
|
||||
visuals: egui::Visuals::light(),
|
||||
..Default::default()
|
||||
},
|
||||
Theme::Dark => Style {
|
||||
visuals: egui::Visuals::dark(),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub font_size: f32,
|
||||
@@ -69,6 +84,18 @@ mod tests {
|
||||
assert_eq!(restored.theme, s.theme);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_style_light_vs_dark() {
|
||||
let light = create_style(&Theme::Light);
|
||||
let dark = create_style(&Theme::Dark);
|
||||
// Light and dark should have different window fills
|
||||
assert_ne!(light.visuals.window_fill, dark.visuals.window_fill);
|
||||
// Dark mode should have darker window
|
||||
assert!(dark.visuals.window_fill.r() < light.visuals.window_fill.r());
|
||||
assert!(dark.visuals.window_fill.g() < light.visuals.window_fill.g());
|
||||
assert!(dark.visuals.window_fill.b() < light.visuals.window_fill.b());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_serialize() {
|
||||
let json = serde_json::to_string(&Theme::Dark).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user