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

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
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)]

View File

@@ -10,22 +10,54 @@ fn parse_blocks(raw_text: &str) -> Vec<ContentBlock> {
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
@@ -414,7 +452,7 @@ egui::ComboBox::from_id_salt("bg_type_selector")
}
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()
} else {
format!("{}{}", indent_str, block.text)
@@ -424,13 +462,31 @@ egui::ComboBox::from_id_salt("bg_type_selector")
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)
.size(style.font_size)
.size(block_font_size)
.color(colors.text);
if block.is_heading {
if block.heading_level >= 1 && block.heading_level <= 4 {
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");
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段二");