改进标题渲染:按层级差异化显示(字号/加粗/间距/对齐)

This commit is contained in:
Developer
2026-05-17 22:06:53 +08:00
parent afa8ea6ce7
commit 6bd96cd913
3 changed files with 101 additions and 30 deletions

BIN
main.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 941 KiB

View File

@@ -65,10 +65,11 @@ pub fn strip_html(input: &str) -> String {
// Emit text before the tag // Emit text before the tag
if tag_start > pos { if tag_start > pos {
let text = decode_entities(&input[pos..tag_start]); let text = decode_entities(&input[pos..tag_start]);
if heading_level.is_some() { if let Some(level) = heading_level {
out.push_str("\x01"); out.push('\x01');
out.push(char::from_digit(level, 10).unwrap_or('1'));
out.push_str(&text); out.push_str(&text);
out.push_str("\x02"); out.push('\x02');
} else { } else {
out.push_str(&text); out.push_str(&text);
} }
@@ -182,7 +183,7 @@ pub struct TocEntry {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ContentBlock { pub struct ContentBlock {
pub text: String, pub text: String,
pub is_heading: bool, pub heading_level: u8, // 0 = body, 1-6 = h1-h6
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@@ -10,22 +10,54 @@ fn parse_blocks(raw_text: &str) -> Vec<ContentBlock> {
if trimmed.is_empty() { if trimmed.is_empty() {
continue; continue;
} }
let is_heading = trimmed.contains('\x01'); let mut heading_level = 0u8;
let text = trimmed.replace('\x01', "").replace('\x02', ""); 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(); let text = text.trim().to_string();
if !text.is_empty() { if !text.is_empty() {
blocks.push(ContentBlock { text, is_heading }); blocks.push(ContentBlock { text, heading_level });
} }
} }
if blocks.is_empty() { if blocks.is_empty() {
blocks.push(ContentBlock { blocks.push(ContentBlock {
text: String::new(), text: String::new(),
is_heading: false, heading_level: 0,
}); });
} }
blocks 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( fn measure_block_height(
ctx: &egui::Context, ctx: &egui::Context,
text: &str, text: &str,
@@ -104,13 +136,19 @@ pub fn recalculate_pages(
let mut current_height: f32 = 0.0; let mut current_height: f32 = 0.0;
for (i, block) in section.blocks.iter().enumerate() { 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() block.text.clone()
} else { } else {
format!("{}{}", indent_str, block.text) 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 { let spacing = if i > page_start_block && i > 0 {
para_spacing para_spacing
@@ -414,7 +452,7 @@ egui::ComboBox::from_id_salt("bg_type_selector")
} }
let block = &section.blocks[i]; let block = &section.blocks[i];
let display_text = if block.is_heading || indent_str.is_empty() { let display_text = if block.heading_level > 0 || indent_str.is_empty() {
block.text.clone() block.text.clone()
} else { } else {
format!("{}{}", indent_str, block.text) format!("{}{}", indent_str, block.text)
@@ -424,13 +462,31 @@ egui::ComboBox::from_id_salt("bg_type_selector")
continue; continue;
} }
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) let mut rt = egui::RichText::new(&display_text)
.size(style.font_size) .size(block_font_size)
.color(colors.text); .color(colors.text);
if block.is_heading { if block.heading_level >= 1 && block.heading_level <= 4 {
rt = rt.strong(); rt = rt.strong();
} }
ui.add(egui::Label::new(rt).wrap());
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"); let blocks = parse_blocks("Hello World");
assert_eq!(blocks.len(), 1); assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].text, "Hello World"); assert_eq!(blocks[0].text, "Hello World");
assert!(!blocks[0].is_heading); assert_eq!(blocks[0].heading_level, 0);
} }
#[test] #[test]
@@ -558,14 +614,28 @@ mod tests {
#[test] #[test]
fn test_parse_blocks_heading() { 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_eq!(blocks.len(), 2);
assert!(blocks[0].is_heading); assert_eq!(blocks[0].heading_level, 1);
assert_eq!(blocks[0].text, "标题"); assert_eq!(blocks[0].text, "标题");
assert!(!blocks[1].is_heading); assert_eq!(blocks[1].heading_level, 0);
assert_eq!(blocks[1].text, "正文内容"); 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] #[test]
fn test_parse_blocks_extra_newlines() { fn test_parse_blocks_extra_newlines() {
let blocks = parse_blocks("段一\n\n\n\n段二"); let blocks = parse_blocks("段一\n\n\n\n段二");