功能更新: 目录点击跳转、翻页按钮移至底部右侧、所有按钮添加悬停提示
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1 +1,9 @@
|
|||||||
/target/
|
/target/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
*.lnk
|
||||||
37
src/app.rs
37
src/app.rs
@@ -19,6 +19,33 @@ pub struct AppState {
|
|||||||
pub error_message: Option<String>,
|
pub error_message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn prev_page(&mut self) {
|
||||||
|
if let Some(ref book) = self.book {
|
||||||
|
if self.current_page > 0 {
|
||||||
|
self.current_page -= 1;
|
||||||
|
} else if self.current_section > 0 {
|
||||||
|
self.current_section -= 1;
|
||||||
|
self.current_page = book.sections[self.current_section]
|
||||||
|
.pages.len().saturating_sub(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_page(&mut self) {
|
||||||
|
if let Some(ref book) = self.book {
|
||||||
|
if self.current_page + 1 < book.sections[self.current_section]
|
||||||
|
.pages.len().saturating_sub(1)
|
||||||
|
{
|
||||||
|
self.current_page += 1;
|
||||||
|
} else if self.current_section + 1 < book.sections.len() {
|
||||||
|
self.current_section += 1;
|
||||||
|
self.current_page = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
let settings_dir = persistence::settings_dir();
|
let settings_dir = persistence::settings_dir();
|
||||||
@@ -174,10 +201,18 @@ impl eframe::App for App {
|
|||||||
if action.toggle_theme {
|
if action.toggle_theme {
|
||||||
self.settings.theme = match self.settings.theme {
|
self.settings.theme = match self.settings.theme {
|
||||||
theme::Theme::Light => theme::Theme::Dark,
|
theme::Theme::Light => theme::Theme::Dark,
|
||||||
theme::Theme::Dark => theme::Theme::Light,
|
theme::Theme::Dark => theme::Theme::Sepia,
|
||||||
|
theme::Theme::Sepia => theme::Theme::Light,
|
||||||
};
|
};
|
||||||
ctx.set_style(theme::create_style(&self.settings.theme));
|
ctx.set_style(theme::create_style(&self.settings.theme));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if action.page_prev {
|
||||||
|
self.state.prev_page();
|
||||||
|
}
|
||||||
|
if action.page_next {
|
||||||
|
self.state.next_page();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
self.save_reading_position();
|
self.save_reading_position();
|
||||||
|
|||||||
118
src/reader.rs
118
src/reader.rs
@@ -1,6 +1,6 @@
|
|||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use crate::book::Book;
|
use crate::book::Book;
|
||||||
use crate::theme::Theme;
|
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, panel_width: f32, panel_height: f32) {
|
||||||
let char_width = font_size * 0.6;
|
let char_width = font_size * 0.6;
|
||||||
@@ -25,6 +25,8 @@ pub struct ReaderAction {
|
|||||||
pub go_back: bool,
|
pub go_back: bool,
|
||||||
pub toggle_theme: bool,
|
pub toggle_theme: bool,
|
||||||
pub toggle_bookmark: bool,
|
pub toggle_bookmark: bool,
|
||||||
|
pub page_next: bool,
|
||||||
|
pub page_prev: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reading_view(
|
pub fn reading_view(
|
||||||
@@ -41,11 +43,15 @@ pub fn reading_view(
|
|||||||
go_back: false,
|
go_back: false,
|
||||||
toggle_theme: false,
|
toggle_theme: false,
|
||||||
toggle_bookmark: false,
|
toggle_bookmark: false,
|
||||||
|
page_next: false,
|
||||||
|
page_prev: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let panel_size = ui.available_size();
|
let panel_size = ui.available_size();
|
||||||
recalculate_pages(book, *font_size, panel_size.x, panel_size.y);
|
recalculate_pages(book, *font_size, panel_size.x, panel_size.y);
|
||||||
|
|
||||||
|
let colors = theme::reader_colors(theme);
|
||||||
|
|
||||||
// --- Sidebar (TOC) ---
|
// --- Sidebar (TOC) ---
|
||||||
if *sidebar_open {
|
if *sidebar_open {
|
||||||
egui::SidePanel::left("toc_sidebar")
|
egui::SidePanel::left("toc_sidebar")
|
||||||
@@ -54,7 +60,7 @@ pub fn reading_view(
|
|||||||
.show_inside(ui, |ui| {
|
.show_inside(ui, |ui| {
|
||||||
ui.heading("目录");
|
ui.heading("目录");
|
||||||
ui.separator();
|
ui.separator();
|
||||||
render_toc(ui, &book.toc, &book.sections, current_section);
|
render_toc(ui, &book.toc, current_section, current_page);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,29 +68,30 @@ pub fn reading_view(
|
|||||||
egui::TopBottomPanel::top("reader_toolbar")
|
egui::TopBottomPanel::top("reader_toolbar")
|
||||||
.show_inside(ui, |ui| {
|
.show_inside(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.button("← 返回").clicked() {
|
if ui.button("← 返回").on_hover_text("返回书架").clicked() {
|
||||||
action.go_back = true;
|
action.go_back = true;
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.label(&book.title);
|
ui.label(&book.title);
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
let theme_icon = match theme {
|
let (theme_icon, theme_hint) = match theme {
|
||||||
Theme::Dark => "☀️",
|
Theme::Dark => ("🌞", "切换到浅色主题"),
|
||||||
Theme::Light => "🌙",
|
Theme::Light => ("🌙", "切换到夜间主题"),
|
||||||
|
Theme::Sepia => ("📜", "切换到棕褐色主题"),
|
||||||
};
|
};
|
||||||
if ui.button(theme_icon).clicked() {
|
if ui.button(theme_icon).on_hover_text(theme_hint).clicked() {
|
||||||
action.toggle_theme = true;
|
action.toggle_theme = true;
|
||||||
}
|
}
|
||||||
if ui.button("🔖").clicked() {
|
if ui.button("🔖").on_hover_text("添加/移除书签").clicked() {
|
||||||
action.toggle_bookmark = true;
|
action.toggle_bookmark = true;
|
||||||
}
|
}
|
||||||
if ui.button("A⁻").clicked() {
|
if ui.button("A⁻").on_hover_text("缩小字体").clicked() {
|
||||||
*font_size = (*font_size - 2.0).max(10.0);
|
*font_size = (*font_size - 2.0).max(10.0);
|
||||||
}
|
}
|
||||||
if ui.button("A⁺").clicked() {
|
if ui.button("A⁺").on_hover_text("放大字体").clicked() {
|
||||||
*font_size = (*font_size + 2.0).min(48.0);
|
*font_size = (*font_size + 2.0).min(48.0);
|
||||||
}
|
}
|
||||||
if ui.button("☰").clicked() {
|
if ui.button("☰").on_hover_text("打开/关闭目录").clicked() {
|
||||||
*sidebar_open = !*sidebar_open;
|
*sidebar_open = !*sidebar_open;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -105,6 +112,36 @@ pub fn reading_view(
|
|||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
ui.label(section_title);
|
ui.label(section_title);
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
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 {
|
let label = if total_pages > 0 {
|
||||||
format!("{}/{}", *current_page + 1, total_pages)
|
format!("{}/{}", *current_page + 1, total_pages)
|
||||||
} else {
|
} else {
|
||||||
@@ -131,13 +168,11 @@ pub fn reading_view(
|
|||||||
let end = section.pages[*current_page + 1];
|
let end = section.pages[*current_page + 1];
|
||||||
let text: String = section.content.chars().skip(start).take(end - start).collect();
|
let text: String = section.content.chars().skip(start).take(end - start).collect();
|
||||||
ui.put(rect, |ui: &mut egui::Ui| {
|
ui.put(rect, |ui: &mut egui::Ui| {
|
||||||
let color = match theme {
|
|
||||||
Theme::Dark => egui::Color32::WHITE,
|
|
||||||
Theme::Light => egui::Color32::BLACK,
|
|
||||||
};
|
|
||||||
ui.add(
|
ui.add(
|
||||||
egui::Label::new(
|
egui::Label::new(
|
||||||
egui::RichText::new(&text).size(*font_size).color(color)
|
egui::RichText::new(&text)
|
||||||
|
.size(*font_size)
|
||||||
|
.color(colors.text)
|
||||||
).wrap()
|
).wrap()
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -149,45 +184,19 @@ pub fn reading_view(
|
|||||||
if let Some(click_pos) = response.interact_pointer_pos() {
|
if let Some(click_pos) = response.interact_pointer_pos() {
|
||||||
let x_ratio = (click_pos.x - rect.min.x) / rect.width();
|
let x_ratio = (click_pos.x - rect.min.x) / rect.width();
|
||||||
if x_ratio < 0.3 {
|
if x_ratio < 0.3 {
|
||||||
if *current_page > 0 {
|
action.page_prev = true;
|
||||||
*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 {
|
} else if x_ratio > 0.7 {
|
||||||
if *current_page + 1 < book.sections[*current_section]
|
action.page_next = true;
|
||||||
.pages.len().saturating_sub(1)
|
|
||||||
{
|
|
||||||
*current_page += 1;
|
|
||||||
} else if *current_section + 1 < book.sections.len() {
|
|
||||||
*current_section += 1;
|
|
||||||
*current_page = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard navigation
|
// Keyboard navigation
|
||||||
if ui.input(|i| i.key_pressed(egui::Key::ArrowRight)) {
|
if ui.input(|i| i.key_pressed(egui::Key::ArrowRight)) {
|
||||||
if *current_page + 1 < book.sections[*current_section]
|
action.page_next = true;
|
||||||
.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 ui.input(|i| i.key_pressed(egui::Key::ArrowLeft)) {
|
||||||
if *current_page > 0 {
|
action.page_prev = true;
|
||||||
*current_page -= 1;
|
|
||||||
} else if *current_section > 0 {
|
|
||||||
*current_section -= 1;
|
|
||||||
*current_page = book.sections[*current_section]
|
|
||||||
.pages.len().saturating_sub(2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -197,22 +206,29 @@ pub fn reading_view(
|
|||||||
fn render_toc(
|
fn render_toc(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
entries: &[crate::book::TocEntry],
|
entries: &[crate::book::TocEntry],
|
||||||
_sections: &[crate::book::Section],
|
|
||||||
current_section: &mut usize,
|
current_section: &mut usize,
|
||||||
|
current_page: &mut usize,
|
||||||
) {
|
) {
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
let is_current = entry.section == *current_section;
|
let is_current = entry.section == *current_section;
|
||||||
let response = if is_current {
|
let label_text = if is_current {
|
||||||
ui.colored_label(egui::Color32::YELLOW, &entry.label)
|
egui::RichText::new(&entry.label).color(egui::Color32::YELLOW).strong()
|
||||||
} else {
|
} else {
|
||||||
ui.label(&entry.label)
|
egui::RichText::new(&entry.label)
|
||||||
};
|
};
|
||||||
|
let response = ui.add(
|
||||||
|
egui::Button::new(label_text)
|
||||||
|
.frame(false)
|
||||||
|
.wrap()
|
||||||
|
);
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
*current_section = entry.section;
|
*current_section = entry.section;
|
||||||
|
*current_page = 0;
|
||||||
}
|
}
|
||||||
|
response.on_hover_text(format!("跳转到: {}", entry.label));
|
||||||
if !entry.children.is_empty() {
|
if !entry.children.is_empty() {
|
||||||
ui.indent(&entry.label, |ui| {
|
ui.indent(&entry.label, |ui| {
|
||||||
render_toc(ui, &entry.children, _sections, current_section);
|
render_toc(ui, &entry.children, current_section, current_page);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
189
src/theme.rs
189
src/theme.rs
@@ -1,11 +1,12 @@
|
|||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use eframe::egui::Style;
|
use eframe::egui::{Color32, CornerRadius, Stroke, Style, Visuals};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Theme {
|
pub enum Theme {
|
||||||
Light,
|
Light,
|
||||||
Dark,
|
Dark,
|
||||||
|
Sepia,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Theme {
|
impl Default for Theme {
|
||||||
@@ -14,16 +15,154 @@ impl Default for Theme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct ReaderColors {
|
||||||
|
pub bg: Color32,
|
||||||
|
pub text: Color32,
|
||||||
|
pub panel_bg: Color32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reader_colors(theme: &Theme) -> ReaderColors {
|
||||||
|
match theme {
|
||||||
|
Theme::Light => ReaderColors {
|
||||||
|
bg: Color32::from_rgb(245, 245, 245),
|
||||||
|
text: Color32::from_rgb(33, 33, 33),
|
||||||
|
panel_bg: Color32::from_rgb(255, 255, 255),
|
||||||
|
},
|
||||||
|
Theme::Dark => ReaderColors {
|
||||||
|
bg: Color32::from_rgb(18, 18, 18),
|
||||||
|
text: Color32::from_rgb(224, 224, 224),
|
||||||
|
panel_bg: Color32::from_rgb(30, 30, 30),
|
||||||
|
},
|
||||||
|
Theme::Sepia => ReaderColors {
|
||||||
|
bg: Color32::from_rgb(244, 236, 216),
|
||||||
|
text: Color32::from_rgb(91, 70, 54),
|
||||||
|
panel_bg: Color32::from_rgb(251, 244, 226),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn corner_radius_4() -> CornerRadius {
|
||||||
|
CornerRadius::same(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn widget_visuals(
|
||||||
|
bg_fill: Color32,
|
||||||
|
weak_bg_fill: Color32,
|
||||||
|
bg_stroke: Stroke,
|
||||||
|
fg_stroke: Stroke,
|
||||||
|
expansion: f32,
|
||||||
|
) -> egui::style::WidgetVisuals {
|
||||||
|
egui::style::WidgetVisuals {
|
||||||
|
bg_fill,
|
||||||
|
weak_bg_fill,
|
||||||
|
bg_stroke,
|
||||||
|
corner_radius: corner_radius_4(),
|
||||||
|
fg_stroke,
|
||||||
|
expansion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_style(theme: &Theme) -> Style {
|
pub fn create_style(theme: &Theme) -> Style {
|
||||||
match theme {
|
match theme {
|
||||||
Theme::Light => Style {
|
Theme::Light => {
|
||||||
visuals: egui::Visuals::light(),
|
let mut visuals = Visuals::light();
|
||||||
|
visuals.widgets.noninteractive.bg_fill = Color32::from_rgb(250, 250, 250);
|
||||||
|
visuals.window_fill = Color32::from_rgb(250, 250, 250);
|
||||||
|
visuals.panel_fill = Color32::from_rgb(250, 250, 250);
|
||||||
|
visuals.faint_bg_color = Color32::from_rgb(245, 245, 245);
|
||||||
|
visuals.extreme_bg_color = Color32::from_rgb(224, 224, 224);
|
||||||
|
visuals.window_corner_radius = corner_radius_4();
|
||||||
|
visuals.widgets.noninteractive.corner_radius = corner_radius_4();
|
||||||
|
visuals.widgets.inactive.corner_radius = corner_radius_4();
|
||||||
|
visuals.widgets.hovered.corner_radius = corner_radius_4();
|
||||||
|
visuals.widgets.active.corner_radius = corner_radius_4();
|
||||||
|
visuals.selection.bg_fill = Color32::from_rgb(187, 222, 251);
|
||||||
|
Style {
|
||||||
|
visuals,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
}
|
||||||
Theme::Dark => Style {
|
}
|
||||||
visuals: egui::Visuals::dark(),
|
Theme::Dark => {
|
||||||
|
let mut visuals = Visuals::dark();
|
||||||
|
visuals.widgets.noninteractive.bg_fill = Color32::from_rgb(18, 18, 18);
|
||||||
|
visuals.window_fill = Color32::from_rgb(18, 18, 18);
|
||||||
|
visuals.panel_fill = Color32::from_rgb(18, 18, 18);
|
||||||
|
visuals.faint_bg_color = Color32::from_rgb(24, 24, 24);
|
||||||
|
visuals.extreme_bg_color = Color32::from_rgb(40, 40, 40);
|
||||||
|
visuals.window_corner_radius = corner_radius_4();
|
||||||
|
visuals.widgets.noninteractive.corner_radius = corner_radius_4();
|
||||||
|
visuals.widgets.inactive.corner_radius = corner_radius_4();
|
||||||
|
visuals.widgets.hovered.corner_radius = corner_radius_4();
|
||||||
|
visuals.widgets.active.corner_radius = corner_radius_4();
|
||||||
|
visuals.selection.bg_fill = Color32::from_rgb(30, 85, 135);
|
||||||
|
Style {
|
||||||
|
visuals,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Theme::Sepia => {
|
||||||
|
let visuals = Visuals {
|
||||||
|
dark_mode: false,
|
||||||
|
override_text_color: Some(Color32::from_rgb(91, 70, 54)),
|
||||||
|
window_fill: Color32::from_rgb(244, 236, 216),
|
||||||
|
panel_fill: Color32::from_rgb(244, 236, 216),
|
||||||
|
faint_bg_color: Color32::from_rgb(235, 225, 200),
|
||||||
|
extreme_bg_color: Color32::from_rgb(220, 208, 180),
|
||||||
|
code_bg_color: Color32::from_rgb(235, 225, 200),
|
||||||
|
warn_fg_color: Color32::from_rgb(180, 120, 60),
|
||||||
|
error_fg_color: Color32::from_rgb(200, 60, 40),
|
||||||
|
hyperlink_color: Color32::from_rgb(139, 69, 19),
|
||||||
|
selection: egui::style::Selection {
|
||||||
|
bg_fill: Color32::from_rgb(210, 180, 140),
|
||||||
|
stroke: Stroke::new(1.0, Color32::from_rgb(180, 150, 110)),
|
||||||
},
|
},
|
||||||
|
widgets: egui::style::Widgets {
|
||||||
|
noninteractive: widget_visuals(
|
||||||
|
Color32::from_rgb(251, 244, 226),
|
||||||
|
Color32::from_rgb(244, 236, 216),
|
||||||
|
Stroke::new(1.0, Color32::from_rgb(180, 160, 130)),
|
||||||
|
Stroke::new(1.0, Color32::from_rgb(91, 70, 54)),
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
inactive: widget_visuals(
|
||||||
|
Color32::from_rgb(210, 180, 140),
|
||||||
|
Color32::from_rgb(200, 175, 135),
|
||||||
|
Stroke::new(1.0, Color32::from_rgb(160, 130, 100)),
|
||||||
|
Stroke::new(1.0, Color32::from_rgb(91, 70, 54)),
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
hovered: widget_visuals(
|
||||||
|
Color32::from_rgb(220, 190, 150),
|
||||||
|
Color32::from_rgb(210, 185, 145),
|
||||||
|
Stroke::new(1.0, Color32::from_rgb(170, 140, 110)),
|
||||||
|
Stroke::new(1.5, Color32::from_rgb(70, 50, 35)),
|
||||||
|
1.0,
|
||||||
|
),
|
||||||
|
active: widget_visuals(
|
||||||
|
Color32::from_rgb(190, 160, 120),
|
||||||
|
Color32::from_rgb(180, 150, 110),
|
||||||
|
Stroke::new(1.0, Color32::from_rgb(140, 110, 80)),
|
||||||
|
Stroke::new(2.0, Color32::from_rgb(50, 35, 20)),
|
||||||
|
1.0,
|
||||||
|
),
|
||||||
|
open: widget_visuals(
|
||||||
|
Color32::from_rgb(200, 170, 130),
|
||||||
|
Color32::from_rgb(190, 160, 125),
|
||||||
|
Stroke::new(1.0, Color32::from_rgb(150, 120, 90)),
|
||||||
|
Stroke::new(2.0, Color32::from_rgb(50, 35, 20)),
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
window_corner_radius: corner_radius_4(),
|
||||||
|
window_shadow: egui::epaint::Shadow::NONE,
|
||||||
|
..Visuals::light()
|
||||||
|
};
|
||||||
|
Style {
|
||||||
|
visuals,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,14 +227,48 @@ mod tests {
|
|||||||
fn test_create_style_light_vs_dark() {
|
fn test_create_style_light_vs_dark() {
|
||||||
let light = create_style(&Theme::Light);
|
let light = create_style(&Theme::Light);
|
||||||
let dark = create_style(&Theme::Dark);
|
let dark = create_style(&Theme::Dark);
|
||||||
// Light and dark should have different window fills
|
|
||||||
assert_ne!(light.visuals.window_fill, dark.visuals.window_fill);
|
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.r() < light.visuals.window_fill.r());
|
||||||
assert!(dark.visuals.window_fill.g() < light.visuals.window_fill.g());
|
assert!(dark.visuals.window_fill.g() < light.visuals.window_fill.g());
|
||||||
assert!(dark.visuals.window_fill.b() < light.visuals.window_fill.b());
|
assert!(dark.visuals.window_fill.b() < light.visuals.window_fill.b());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_style_sepia() {
|
||||||
|
let sepia = create_style(&Theme::Sepia);
|
||||||
|
let light = create_style(&Theme::Light);
|
||||||
|
assert_ne!(sepia.visuals.window_fill, light.visuals.window_fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reader_colors() {
|
||||||
|
let light_colors = reader_colors(&Theme::Light);
|
||||||
|
assert!(light_colors.bg.r() > 200);
|
||||||
|
assert!(light_colors.text.r() < 50);
|
||||||
|
|
||||||
|
let dark_colors = reader_colors(&Theme::Dark);
|
||||||
|
assert!(dark_colors.bg.r() < 30);
|
||||||
|
|
||||||
|
let sepia_colors = reader_colors(&Theme::Sepia);
|
||||||
|
assert!(sepia_colors.bg.r() > 200);
|
||||||
|
assert!(sepia_colors.bg.r() < 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_theme_cycle_all() {
|
||||||
|
let themes = [Theme::Light, Theme::Dark, Theme::Sepia];
|
||||||
|
let json = serde_json::to_string(&Theme::Sepia).unwrap();
|
||||||
|
assert_eq!(json, "\"Sepia\"");
|
||||||
|
let restored: Theme = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(restored, Theme::Sepia);
|
||||||
|
let styles: Vec<_> = themes.iter().map(|t| create_style(t).visuals.window_fill).collect();
|
||||||
|
for i in 0..styles.len() {
|
||||||
|
for j in (i + 1)..styles.len() {
|
||||||
|
assert_ne!(styles[i], styles[j], "themes {i} and {j} should differ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_theme_serialize() {
|
fn test_theme_serialize() {
|
||||||
let json = serde_json::to_string(&Theme::Dark).unwrap();
|
let json = serde_json::to_string(&Theme::Dark).unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user