Skip to main content

verso/reader/
page.rs

1use super::{linebreak, styled::Span};
2
3#[derive(Debug, Clone)]
4pub struct PageRow {
5    pub text: String,
6    pub spans: Vec<Span>,   // spans that intersect this row (for styling)
7    pub char_offset: usize, // offset of the first char on this row
8}
9
10#[derive(Debug, Clone)]
11pub struct Page {
12    pub rows: Vec<PageRow>,
13}
14
15/// Paginate a list of spans to pages of `height` rows at `width` columns.
16/// In v1 styled spans are rendered as plain text for line-breaking;
17/// full per-span styling on the output rows arrives in Task 24.
18pub fn paginate(spans: &[Span], width: u16, height: u16) -> Vec<Page> {
19    let height = height as usize;
20    if spans.is_empty() {
21        return vec![Page { rows: vec![] }];
22    }
23
24    // 1) Flatten spans into plain text with a parallel offset map.
25    let mut text = String::new();
26    for s in spans {
27        text.push_str(&s.text);
28    }
29    let lines = linebreak::wrap(&text, width);
30
31    // 2) Map each line to its starting char_offset (best-effort: find() from running cursor).
32    let mut rows: Vec<PageRow> = Vec::with_capacity(lines.len());
33    let mut cursor = 0usize;
34    for l in &lines {
35        if l.is_empty() {
36            rows.push(PageRow {
37                text: String::new(),
38                spans: vec![],
39                char_offset: cursor,
40            });
41            continue;
42        }
43        let off = text[cursor..]
44            .find(l.as_str())
45            .map(|b| cursor + b)
46            .unwrap_or(cursor);
47        let char_off = text[..off].chars().count();
48        rows.push(PageRow {
49            text: l.clone(),
50            spans: vec![],
51            char_offset: char_off,
52        });
53        cursor = off + l.len();
54    }
55
56    // 3) Chunk into pages.
57    rows.chunks(height)
58        .map(|c| Page { rows: c.to_vec() })
59        .collect()
60}