diff --git a/main.png b/main.png deleted file mode 100644 index 012e1a2..0000000 Binary files a/main.png and /dev/null differ diff --git a/src/book.rs b/src/book.rs index bdf6589..6f4d328 100644 --- a/src/book.rs +++ b/src/book.rs @@ -65,10 +65,11 @@ pub fn strip_html(input: &str) -> String { // Emit text before the tag if tag_start > pos { let text = decode_entities(&input[pos..tag_start]); - if heading_level.is_some() { - out.push_str("\x01"); + if let Some(level) = heading_level { + out.push('\x01'); + out.push(char::from_digit(level, 10).unwrap_or('1')); out.push_str(&text); - out.push_str("\x02"); + out.push('\x02'); } else { out.push_str(&text); } @@ -182,7 +183,7 @@ pub struct TocEntry { #[derive(Debug, Clone)] pub struct ContentBlock { pub text: String, - pub is_heading: bool, + pub heading_level: u8, // 0 = body, 1-6 = h1-h6 } #[derive(Debug, Clone)] diff --git a/src/reader.rs b/src/reader.rs index e5af8ed..4e72b13 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -10,22 +10,54 @@ fn parse_blocks(raw_text: &str) -> Vec { if trimmed.is_empty() { continue; } - let is_heading = trimmed.contains('\x01'); - let text = trimmed.replace('\x01', "").replace('\x02', ""); + let mut heading_level = 0u8; + let mut text = trimmed.to_string(); + if text.contains('\x01') { + if let Some(pos) = text.find('\x01') { + let rest = &text[pos + 1..]; + heading_level = rest + .chars() + .next() + .and_then(|c| c.to_digit(10)) + .unwrap_or(1) as u8; + text.drain(pos..pos + 2); + } + text = text.replace('\x02', ""); + } let text = text.trim().to_string(); if !text.is_empty() { - blocks.push(ContentBlock { text, is_heading }); + blocks.push(ContentBlock { text, heading_level }); } } if blocks.is_empty() { blocks.push(ContentBlock { text: String::new(), - is_heading: false, + heading_level: 0, }); } blocks } +fn heading_font_size(base_size: f32, level: u8) -> f32 { + match level { + 1 => base_size * 1.6, + 2 => base_size * 1.35, + 3 => base_size * 1.15, + 4 => base_size * 1.05, + _ => base_size, + } +} + +fn heading_top_spacing(para_spacing: f32, level: u8) -> f32 { + match level { + 1 => para_spacing * 3.5, + 2 => para_spacing * 3.0, + 3 => para_spacing * 2.5, + 4 => para_spacing * 2.0, + _ => 0.0, + } +} + fn measure_block_height( ctx: &egui::Context, text: &str, @@ -104,13 +136,19 @@ pub fn recalculate_pages( let mut current_height: f32 = 0.0; for (i, block) in section.blocks.iter().enumerate() { - let display_text = if block.is_heading || indent_str.is_empty() { + let block_font_size = if block.heading_level > 0 { + heading_font_size(font_size, block.heading_level) + } else { + font_size + }; + + let display_text = if block.heading_level > 0 || indent_str.is_empty() { block.text.clone() } else { format!("{}{}", indent_str, block.text) }; - let block_height = measure_block_height(ctx, &display_text, font_size, available_width); + let block_height = measure_block_height(ctx, &display_text, block_font_size, available_width); let spacing = if i > page_start_block && i > 0 { para_spacing @@ -413,24 +451,42 @@ egui::ComboBox::from_id_salt("bg_type_selector") ui.add_space(para_spacing); } - let block = §ion.blocks[i]; - let display_text = if block.is_heading || indent_str.is_empty() { - block.text.clone() - } else { - format!("{}{}", indent_str, block.text) - }; + let block = §ion.blocks[i]; + let display_text = if block.heading_level > 0 || indent_str.is_empty() { + block.text.clone() + } else { + format!("{}{}", indent_str, block.text) + }; - if display_text.is_empty() { - continue; - } + if display_text.is_empty() { + continue; + } - let mut rt = egui::RichText::new(&display_text) - .size(style.font_size) - .color(colors.text); - if block.is_heading { - rt = rt.strong(); - } - ui.add(egui::Label::new(rt).wrap()); + if block.heading_level > 0 && i > block_start { + ui.add_space(heading_top_spacing(para_spacing, block.heading_level)); + } + + let block_font_size = if block.heading_level > 0 { + heading_font_size(style.font_size, block.heading_level) + } else { + style.font_size + }; + + let mut rt = egui::RichText::new(&display_text) + .size(block_font_size) + .color(colors.text); + if block.heading_level >= 1 && block.heading_level <= 4 { + rt = rt.strong(); + } + + let label = egui::Label::new(rt).wrap(); + if block.heading_level == 1 { + ui.centered_and_justified(|ui| { + ui.add(label); + }); + } else { + ui.add(label); + } } }, ); @@ -544,7 +600,7 @@ mod tests { let blocks = parse_blocks("Hello World"); assert_eq!(blocks.len(), 1); assert_eq!(blocks[0].text, "Hello World"); - assert!(!blocks[0].is_heading); + assert_eq!(blocks[0].heading_level, 0); } #[test] @@ -558,14 +614,28 @@ mod tests { #[test] fn test_parse_blocks_heading() { - let blocks = parse_blocks("\x01标题\x02\n\n正文内容"); + let blocks = parse_blocks("\x011标题\x02\n\n正文内容"); assert_eq!(blocks.len(), 2); - assert!(blocks[0].is_heading); + assert_eq!(blocks[0].heading_level, 1); assert_eq!(blocks[0].text, "标题"); - assert!(!blocks[1].is_heading); + assert_eq!(blocks[1].heading_level, 0); assert_eq!(blocks[1].text, "正文内容"); } + #[test] + fn test_parse_blocks_heading_levels() { + let blocks = parse_blocks("\x011一级\x02\n\n\x012二级\x02\n\n\x013三级\x02\n\n正文"); + assert_eq!(blocks.len(), 4); + assert_eq!(blocks[0].heading_level, 1); + assert_eq!(blocks[0].text, "一级"); + assert_eq!(blocks[1].heading_level, 2); + assert_eq!(blocks[1].text, "二级"); + assert_eq!(blocks[2].heading_level, 3); + assert_eq!(blocks[2].text, "三级"); + assert_eq!(blocks[3].heading_level, 0); + assert_eq!(blocks[3].text, "正文"); + } + #[test] fn test_parse_blocks_extra_newlines() { let blocks = parse_blocks("段一\n\n\n\n段二");