Skip to main content

kobo_core/rendering/layout/
pagination.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Nayeem Bin Ahsan
3use 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}