kobo_core/rendering/layout/
pagination.rs1use super::{word_wrap_bytes, word_wrap_char_based_styled};
4use crate::rendering::text_render;
5
6#[derive(Debug, Clone, Copy)]
7pub struct ScreenLayout {
8 pub text_w: usize,
9 pub content_h: i32,
10 pub heading_h: i32,
11 pub heading_gap: i32,
12 pub para_gap: i32,
13}
14
15pub fn block_indent_for(indent_em: f32, body_px: f32, text_w: usize) -> usize {
16 if indent_em <= 0.0 {
17 return 0;
18 }
19 const MAX_BLOCK_INDENT_PX: usize = 255;
20 let max = (text_w / 3).min(MAX_BLOCK_INDENT_PX);
21 ((indent_em * body_px) as usize).min(max)
22}
23
24pub fn estimate_chapter_offsets(
25 chapters: &[crate::formats::epub::Chapter],
26 current: (usize, usize),
27 line_h: i32,
28 layout: &ScreenLayout,
29) -> Vec<usize> {
30 let chars_per_line = (layout.text_w as f32 / (line_h as f32 * 0.6)) as usize;
31 let lines_per_page = (layout.content_h / line_h) as usize;
32 let chars_per_page = chars_per_line * lines_per_page;
33 let mut offsets = vec![0usize; chapters.len() + 1];
34 for (i, ch) in chapters.iter().enumerate() {
35 let est = if i == current.0 {
36 current.1
37 } else {
38 ((ch.text.chars().count() as f64 / chars_per_page as f64).ceil() as usize).max(1)
39 };
40 offsets[i + 1] = offsets[i] + est;
41 }
42 offsets
43}
44
45pub fn count_chapter_pages(
46 chapter: &mut crate::formats::epub::Chapter,
47 body_px: f32,
48 line_h: i32,
49 layout: &ScreenLayout,
50) -> usize {
51 let chapter_images = chapter.load_images().to_vec();
52 let full = &chapter.text;
53 let segs = &chapter.segments;
54 let mut row_heights: Vec<i32> = Vec::new();
55 let mut heading_indices: Vec<usize> = Vec::new();
56 let mut prev_was_gap = false;
57 let mut img_idx = 0usize;
58 let is_heading = |t: &str| matches!(t, "h1" | "h2" | "h3" | "h4" | "h5" | "h6");
59 for seg in segs {
60 if seg.src.is_some() {
61 let cap = seg.caption.as_deref().unwrap_or("");
62 let h = if let Some(raw) = chapter_images.get(img_idx).map(|(_, b)| b.as_slice()) {
63 text_render::decode_image(raw, layout.text_w, layout.content_h as usize - 20)
64 .map(|img| img.height as i32 + if cap.is_empty() { 4 } else { line_h + 4 })
65 .unwrap_or(line_h + 4)
66 } else {
67 line_h + 4
68 };
69 row_heights.push(h);
70 prev_was_gap = false;
71 img_idx += 1;
72 continue;
73 }
74 let seg_text = full.get(seg.start..seg.end).unwrap_or("");
75 if is_heading(seg.tag.as_str()) {
76 heading_indices.push(row_heights.len());
77 row_heights.push(layout.heading_h);
78 row_heights.push(layout.heading_gap);
79 prev_was_gap = true;
80 } else {
81 if !row_heights.is_empty() && !prev_was_gap {
82 row_heights.push(layout.para_gap);
83 }
84 let block_indent = block_indent_for(seg.indent, body_px, layout.text_w);
85 let avail = layout.text_w.saturating_sub(block_indent);
86 let lines = if block_indent > 0 {
87 word_wrap_char_based_styled(
88 seg_text,
89 avail,
90 body_px,
91 text_render::TextStyle {
92 mono: true,
93 ..Default::default()
94 },
95 )
96 } else {
97 word_wrap_bytes(seg_text, avail, body_px)
98 };
99 for _ in &lines {
100 row_heights.push(line_h);
101 }
102 prev_was_gap = false;
103 }
104 }
105 super::paginate_heights(&row_heights, layout.content_h, &heading_indices).len()
106}