diff --git a/Cargo.lock b/Cargo.lock index 21f68b0..0f84d60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,6 +138,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "android-activity" version = "0.6.1" @@ -178,6 +196,12 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "arboard" version = "3.6.1" @@ -198,6 +222,17 @@ dependencies = [ "x11rb", ] +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -216,6 +251,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "ash" version = "0.38.0+1.3.281" @@ -463,6 +507,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -478,6 +565,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -493,6 +586,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + [[package]] name = "block" version = "0.1.6" @@ -539,6 +641,12 @@ dependencies = [ "piper", ] +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + [[package]] name = "bumpalo" version = "3.20.2" @@ -686,6 +794,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.7" @@ -773,6 +887,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -990,6 +1123,12 @@ dependencies = [ "winit", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "emath" version = "0.31.1" @@ -1069,11 +1208,32 @@ version = "0.1.0" dependencies = [ "eframe", "epub", + "image", "rfd", "serde", "serde_json", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1117,6 +1277,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -1325,6 +1500,16 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -1620,12 +1805,38 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", "moxcms", "num-traits", "png", + "qoi", + "ravif", + "rayon", + "rgb", "tiff", + "zune-core", + "zune-jpeg", ] +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" + [[package]] name = "immutable-chunkmap" version = "2.1.2" @@ -1647,6 +1858,26 @@ dependencies = [ "serde_core", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1756,12 +1987,28 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.8.9" @@ -1823,6 +2070,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1832,6 +2088,16 @@ dependencies = [ "libc", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.8.0" @@ -1952,6 +2218,12 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" version = "0.29.0" @@ -1965,12 +2237,77 @@ dependencies = [ "memoffset", ] +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + [[package]] name = "nohash-hasher" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2362,6 +2699,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2501,6 +2844,19 @@ name = "profiling" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn", +] [[package]] name = "pxfm" @@ -2508,6 +2864,15 @@ version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quick-error" version = "2.0.1" @@ -2613,12 +2978,82 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.4", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2705,6 +3140,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2895,6 +3336,15 @@ dependencies = [ "simdutf8", ] +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -3330,6 +3780,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.5" @@ -4239,6 +4700,12 @@ version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yoke" version = "0.8.2" @@ -4520,6 +4987,15 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + [[package]] name = "zune-jpeg" version = "0.5.15" diff --git a/Cargo.toml b/Cargo.toml index f5d1ff4..865fde3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ epub = "1.2" rfd = "0.15" serde = { version = "1", features = ["derive"] } serde_json = "1" +image = "0.25" [profile.release] opt-level = "z" diff --git a/src/app.rs b/src/app.rs index 641bec3..c665421 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,7 +1,7 @@ use crate::book::Book; use crate::persistence; use crate::style::StyleProfile; -use crate::theme::{self, Settings}; +use crate::theme::{self, BgType, Settings}; use eframe::egui; use std::path::PathBuf; @@ -10,6 +10,9 @@ pub struct App { settings: Settings, settings_dir: std::path::PathBuf, kraft_texture: Option, + manuscript_texture: Option, + composition_texture: Option, + custom_texture: Option, } pub struct AppState { @@ -53,7 +56,6 @@ impl App { let settings_dir = persistence::settings_dir(); let settings = persistence::load_settings(&settings_dir).unwrap_or_default(); cc.egui_ctx.set_style(theme::create_style(&settings.theme)); - let kraft_texture = Some(crate::texture::generate_kraft_paper(&cc.egui_ctx)); Self { state: AppState { book: None, @@ -65,7 +67,10 @@ impl App { }, settings, settings_dir, - kraft_texture, + kraft_texture: Some(crate::texture::generate_kraft_paper(&cc.egui_ctx)), + manuscript_texture: Some(crate::texture::generate_manuscript(&cc.egui_ctx)), + composition_texture: Some(crate::texture::generate_composition(&cc.egui_ctx)), + custom_texture: None, } } @@ -177,44 +182,102 @@ impl App { } impl eframe::App for App { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - let use_kraft = self.settings.use_kraft_bg; + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + let bg_type = self.settings.bg_type.clone(); + let has_bg = bg_type.has_background(); - // 牛皮纸模式:全窗口绘制纹理 + 面板透明 - if use_kraft { - if let Some(texture) = &self.kraft_texture { - ctx.style_mut(|s| { - s.visuals.panel_fill = egui::Color32::TRANSPARENT; - s.visuals.window_fill = egui::Color32::TRANSPARENT; - s.visuals.faint_bg_color = egui::Color32::TRANSPARENT; - }); - let viewport_rect = - ctx.input(|i| i.screen_rect()); - crate::texture::draw_tiled_bg( - &ctx.layer_painter(egui::LayerId::background()), - viewport_rect, - texture, - ); - } - } + // 有背景时面板设为透明 + if has_bg { + ctx.style_mut(|s| { + s.visuals.panel_fill = egui::Color32::TRANSPARENT; + s.visuals.window_fill = egui::Color32::TRANSPARENT; + s.visuals.faint_bg_color = egui::Color32::TRANSPARENT; + }); + } - if self.state.book.is_none() { - self.welcome_view(ctx); - return; - } + // 全窗口背景纹理 + let viewport_rect = ctx.input(|i| i.screen_rect()); + match bg_type { + BgType::Kraft => { + if let Some(tex) = &self.kraft_texture { + crate::texture::draw_tiled_bg( + &ctx.layer_painter(egui::LayerId::background()), + viewport_rect, + tex, + ); + } + } + BgType::Manuscript => { + if let Some(tex) = &self.manuscript_texture { + crate::texture::draw_tiled_bg( + &ctx.layer_painter(egui::LayerId::background()), + viewport_rect, + tex, + ); + } + } + BgType::Composition => { + if let Some(tex) = &self.composition_texture { + crate::texture::draw_tiled_bg( + &ctx.layer_painter(egui::LayerId::background()), + viewport_rect, + tex, + ); + } + } +BgType::Custom(ref path) if !path.is_empty() => { + if self.custom_texture.is_none() { + match image::ImageReader::open(path) { + Ok(reader) => match reader.decode() { + Ok(img) => { + let size = [img.width() as usize, img.height() as usize]; + let pixels: Vec = img + .to_rgba8() + .pixels() + .map(|p| { + egui::Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3]) + }) + .collect(); + let image = egui::ColorImage { size, pixels }; + self.custom_texture = Some( + ctx.load_texture("custom_bg", image, Default::default()), + ); + } + Err(_) => {} + }, + Err(_) => {} + } + } + if let Some(tex) = &self.custom_texture { + let painter = &ctx.layer_painter(egui::LayerId::background()); + painter.image( + tex.id(), + viewport_rect, + egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), + egui::Color32::WHITE, + ); + } + } + _ => {} + } - let file_path = self.state.file_path - .as_ref() - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_default(); + if self.state.book.is_none() { + self.welcome_view(ctx); + return; + } - let mut style = self.active_profile().clone(); - let theme_copy = self.settings.theme; - let profile_names: Vec = self.settings.profiles.iter().map(|p| p.name.clone()).collect(); + let file_path = self.state.file_path + .as_ref() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_default(); - egui::CentralPanel::default().show(ctx, |ui| { - let book = self.state.book.as_mut().unwrap(); -let action = crate::reader::reading_view( + let mut style = self.active_profile().clone(); + let theme_copy = self.settings.theme; + let profile_names: Vec = self.settings.profiles.iter().map(|p| p.name.clone()).collect(); + + egui::CentralPanel::default().show(ctx, |ui| { + let book = self.state.book.as_mut().unwrap(); + let action = crate::reader::reading_view( ui, book, &mut self.state.current_section, @@ -222,67 +285,83 @@ let action = crate::reader::reading_view( &mut self.state.sidebar_open, &mut style, &theme_copy, - use_kraft, + bg_type.clone(), &file_path, &profile_names, ); - if action.go_back { - self.save_reading_position(); - self.state.book = None; - self.state.current_section = 0; - self.state.current_page = 0; - } + if action.go_back { + self.save_reading_position(); + self.state.book = None; + self.state.current_section = 0; + self.state.current_page = 0; + } - // Switch profile immediately: copy all settings from the selected profile - if let Some(ref target) = action.switch_to_profile { - if let Some(profile) = self.settings.profiles.iter() - .find(|p| p.name == *target) - { - style.alignment = profile.alignment; - style.line_spacing = profile.line_spacing; - style.paragraph_spacing = profile.paragraph_spacing; - style.first_line_indent = profile.first_line_indent; - style.font_size = profile.font_size; - style.name = profile.name.clone(); - self.settings.active_profile = profile.name.clone(); - } - } + // Switch profile immediately + if let Some(ref target) = action.switch_to_profile { + if let Some(profile) = self.settings.profiles.iter() + .find(|p| p.name == *target) + { + style.alignment = profile.alignment; + style.line_spacing = profile.line_spacing; + style.paragraph_spacing = profile.paragraph_spacing; + style.first_line_indent = profile.first_line_indent; + style.font_size = profile.font_size; + style.name = profile.name.clone(); + self.settings.active_profile = profile.name.clone(); + } + } - if action.toggle_kraft_bg { - self.settings.use_kraft_bg = !self.settings.use_kraft_bg; - } + if let Some(new_bg) = action.switch_bg { + self.settings.bg_type = new_bg; + // 切换背景时,如果选自定义图片则弹出文件选择器 + if let BgType::Custom(_) = &self.settings.bg_type { + if let Some(p) = rfd::FileDialog::new() + .add_filter("Images", &["png", "jpg", "jpeg", "bmp", "webp"]) + .pick_file() + { + self.settings.bg_type = BgType::Custom(p.to_string_lossy().to_string()); + self.custom_texture = None; + } else { + self.settings.bg_type = BgType::None; + } + } + // 切换背景时,如果从自定义图片切走,清除纹理缓存 + if !self.settings.bg_type.has_background() || !matches!(self.settings.bg_type, BgType::Custom(_)) { + self.custom_texture = None; + } + } - if action.toggle_theme { - self.settings.theme = match self.settings.theme { - theme::Theme::Light => theme::Theme::Dark, - theme::Theme::Dark => theme::Theme::Sepia, - theme::Theme::Sepia => theme::Theme::Light, - }; - ctx.set_style(theme::create_style(&self.settings.theme)); - } + if action.toggle_theme { + self.settings.theme = match self.settings.theme { + theme::Theme::Light => theme::Theme::Dark, + theme::Theme::Dark => theme::Theme::Sepia, + theme::Theme::Sepia => theme::Theme::Light, + }; + 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(); - } - }); + if action.page_prev { + self.state.prev_page(); + } + if action.page_next { + self.state.next_page(); + } + }); - // 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; + // 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(); - } -} + self.save_reading_position(); + self.save_settings(); + } + } diff --git a/src/reader.rs b/src/reader.rs index c2082d9..87c500b 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -1,7 +1,7 @@ use eframe::egui; use crate::book::Book; use crate::style::{StyleProfile, TextAlignment}; -use crate::theme::{self, Theme}; +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) { let char_width = font_size * 0.6; @@ -25,7 +25,7 @@ pub struct ReaderAction { pub go_back: bool, pub toggle_theme: bool, pub toggle_bookmark: bool, - pub toggle_kraft_bg: bool, + pub switch_bg: Option, pub switch_to_profile: Option, pub page_next: bool, pub page_prev: bool, @@ -39,7 +39,7 @@ pub fn reading_view( sidebar_open: &mut bool, style: &mut StyleProfile, theme: &Theme, - use_kraft_bg: bool, + bg_type: BgType, _file_path: &str, profile_names: &[String], ) -> ReaderAction { @@ -47,7 +47,7 @@ pub fn reading_view( go_back: false, toggle_theme: false, toggle_bookmark: false, - toggle_kraft_bg: false, + switch_bg: None, switch_to_profile: None, page_next: false, page_prev: false, @@ -108,10 +108,17 @@ pub fn reading_view( } } }); - let kraft_icon = if use_kraft_bg { "📄" } else { "📋" }; - if ui.button(kraft_icon).on_hover_text("牛皮纸背景").clicked() { - action.toggle_kraft_bg = true; - } +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; } diff --git a/src/texture.rs b/src/texture.rs index 59f0ab8..e8449a2 100644 --- a/src/texture.rs +++ b/src/texture.rs @@ -7,7 +7,6 @@ fn hash(x: u32, y: u32) -> u32 { } fn paper_noise(x: u32, y: u32) -> u8 { - // Combine multiple hash values for more natural-looking noise let n1 = hash(x, y); let n2 = hash(x.wrapping_add(137), y.wrapping_add(251)); let n3 = hash(x.wrapping_mul(7), y.wrapping_mul(13)); @@ -17,31 +16,73 @@ fn paper_noise(x: u32, y: u32) -> u8 { pub fn generate_kraft_paper(ctx: &egui::Context) -> egui::TextureHandle { let size = 128usize; let mut pixels = Vec::with_capacity(size * size); - for y in 0..size { for x in 0..size { let n = paper_noise(x as u32, y as u32); - // Warm brown-beige base with slight variation let r = 212u8.wrapping_add(n); let g = 194u8.wrapping_add(n); let b = 168u8.wrapping_add(n); pixels.push(egui::Color32::from_rgb(r, g, b)); } } - - let image = egui::ColorImage { - size: [size, size], - pixels, - }; + let image = egui::ColorImage { size: [size, size], pixels }; ctx.load_texture("kraft_paper", image, Default::default()) } +/// 稿纸:浅黄色 + 蓝色横线 + 红色竖线 +pub fn generate_manuscript(ctx: &egui::Context) -> egui::TextureHandle { + let size = 256usize; + let mut pixels = Vec::with_capacity(size * size); + for y in 0..size { + for x in 0..size { + // 浅黄色基底 + let (r, g, b) = (245u8, 230u8, 195u8); + // 横线:每 16 像素一条蓝色虚线 + let is_hline = y % 16 < 2 && x > 8 && x < size - 8; + // 竖线:居中的红色引导线 + let is_vline = x == size / 2 && y > 16 && y < size - 16; + let pixel = if is_vline { + egui::Color32::from_rgb(220, 50, 50) + } else if is_hline { + egui::Color32::from_rgb(100, 100, 200) + } else { + egui::Color32::from_rgb(r, g, b) + }; + pixels.push(pixel); + } + } + let image = egui::ColorImage { size: [size, size], pixels }; + ctx.load_texture("manuscript", image, Default::default()) +} + +/// 作文纸:浅绿色 + 横线网格 +pub fn generate_composition(ctx: &egui::Context) -> egui::TextureHandle { + let size = 256usize; + let mut pixels = Vec::with_capacity(size * size); + for y in 0..size { + for x in 0..size { + // 浅绿色基底 + let (r, g, b) = (220u8, 235u8, 210u8); + // 横线网格:每 16 像素一条灰色线 + let is_grid = (y % 16 < 1) || (x % 64 < 1); + let pixel = if is_grid { + egui::Color32::from_rgb(180, 180, 180) + } else { + egui::Color32::from_rgb(r, g, b) + }; + pixels.push(pixel); + } + } + let image = egui::ColorImage { size: [size, size], pixels }; + ctx.load_texture("composition", image, Default::default()) +} + pub fn draw_tiled_bg( painter: &egui::Painter, rect: egui::Rect, texture: &egui::TextureHandle, ) { - let tile_size = 128.0f32; + let tile_size = 256.0f32; let mut ty = rect.min.y; while ty < rect.max.y { let mut tx = rect.min.x; diff --git a/src/theme.rs b/src/theme.rs index 5998e4c..879b721 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -2,6 +2,50 @@ use eframe::egui; use eframe::egui::{Color32, CornerRadius, Stroke, Style, Visuals}; use serde::{Deserialize, Serialize}; +/// 背景类型 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum BgType { + None, + Kraft, + Manuscript, + Composition, + Custom(String), +} + +impl Default for BgType { + fn default() -> Self { + BgType::None + } +} + +impl BgType { + pub const ALL: [&'static str; 5] = ["无", "牛皮纸", "稿纸", "作文纸", "自定义图片"]; + + pub fn label(&self) -> &str { + match self { + BgType::None => "无", + BgType::Kraft => "牛皮纸", + BgType::Manuscript => "稿纸", + BgType::Composition => "作文纸", + BgType::Custom(_) => "自定义图片", + } + } + + pub fn from_label(label: &str) -> Self { + match label { + "牛皮纸" => BgType::Kraft, + "稿纸" => BgType::Manuscript, + "作文纸" => BgType::Composition, + "自定义图片" => BgType::Custom(String::new()), + _ => BgType::None, + } + } + + pub fn has_background(&self) -> bool { + !matches!(self, BgType::None) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum Theme { Light, @@ -170,7 +214,7 @@ pub fn create_style(theme: &Theme) -> Style { pub struct Settings { pub font_size: f32, pub theme: Theme, - pub use_kraft_bg: bool, + pub bg_type: BgType, pub active_profile: String, pub profiles: Vec, pub recent_files: Vec, @@ -184,7 +228,7 @@ impl Default for Settings { Self { font_size: 20.0, theme: Theme::Light, - use_kraft_bg: false, + bg_type: BgType::None, active_profile: "Kindle 默认".into(), profiles: crate::style::StyleProfile::presets(), recent_files: Vec::new(),