Skip to main content

yog_book/
state.rs

1//! Per-book navigation state.
2
3use yog_formats::book::Book;
4
5pub mod action;
6
7/// Rows of entries on the first list spread (right page only).
8pub const ENTRIES_FIRST: usize = 10;
9/// Rows of entries on each subsequent list spread (right page only).
10pub const ENTRIES_PER: usize = 12;
11
12/// The rows an entry's name takes in the list: its lines, wrapped to the row.
13pub fn list_rows(name: &str) -> usize {
14    yog_ui::text::line_count(name, crate::renderer::geometry::LIST_NAME_W, 1.0).max(1)
15}
16
17#[derive(Debug, Clone)]
18pub struct BookViewState {
19    pub cat: usize,
20    /// The entry's index among its category's (all of them, not just the spread's).
21    pub entry: usize,
22    pub page: usize,
23    pub at_home: bool,
24    /// Which spread of the entry list is shown.
25    pub list_spread: usize,
26}
27
28impl Default for BookViewState {
29    fn default() -> Self {
30        Self {
31            cat: 0,
32            entry: 0,
33            page: 0,
34            at_home: true,
35            list_spread: 0,
36        }
37    }
38}
39
40impl BookViewState {
41    /// Handle a navigation event string. Returns true if state changed.
42    pub fn handle(&mut self, ev: &str, book: &Book) -> bool {
43        if ev == action::HOME {
44            if !self.at_home {
45                self.at_home = true;
46                return true;
47            }
48        } else if let Some(n) = ev.strip_prefix(action::CATEGORY) {
49            if let Ok(i) = n.parse::<usize>()
50                && i < book.categories.len()
51            {
52                let changed = self.at_home || i != self.cat;
53                self.cat = i;
54                self.entry = 0;
55                self.page = 0;
56                self.list_spread = 0;
57                self.at_home = false;
58                return changed;
59            }
60        } else if let Some(n) = ev.strip_prefix(action::ENTRY) {
61            // N is the absolute index within entries_in_cat.
62            if let Ok(i) = n.parse::<usize>() {
63                let total = self.entries_in_cat(book).len();
64                if i < total && (i != self.entry || self.page != 0) {
65                    self.entry = i;
66                    self.page = 0;
67                    return true;
68                }
69            }
70        } else if ev == action::PREV_PAGE {
71            if self.page > 0 {
72                self.page -= 1;
73                return true;
74            }
75        } else if ev == action::NEXT_PAGE {
76            if self.page + 1 < self.page_count(book) {
77                self.page += 1;
78                return true;
79            }
80        } else if ev == action::PREV_LIST {
81            if self.list_spread > 0 {
82                self.list_spread -= 1;
83                return true;
84            }
85        } else if ev == action::NEXT_LIST && self.list_spread + 1 < self.list_spread_count(book) {
86            self.list_spread += 1;
87            return true;
88        }
89        false
90    }
91
92    /// All entries in the current category, unfiltered.
93    pub fn entries_in_cat<'b>(&self, book: &'b Book) -> Vec<&'b yog_formats::book::BookEntry> {
94        let cat_id = book
95            .categories
96            .get(self.cat)
97            .map(|c| c.id.as_str())
98            .unwrap_or("");
99        book.entries
100            .iter()
101            .filter(|e| e.category == cat_id && crate::shown(e))
102            .collect()
103    }
104
105    /// The entries each list spread of the current category shows (their
106    /// indices), filled row by row: a long name wraps onto more rows.
107    fn list_spreads(&self, book: &Book) -> Vec<std::ops::Range<usize>> {
108        let all = self.entries_in_cat(book);
109        let mut spreads = Vec::new();
110        let (mut start, mut used) = (0, 0);
111        for (i, e) in all.iter().enumerate() {
112            let rows = list_rows(&e.name);
113            let room = if spreads.is_empty() { ENTRIES_FIRST } else { ENTRIES_PER };
114            if used + rows > room && i > start {
115                spreads.push(start..i);
116                (start, used) = (i, 0);
117            }
118            used += rows;
119        }
120        spreads.push(start..all.len());
121        spreads
122    }
123
124    /// How many list spreads the current category needs.
125    pub fn list_spread_count(&self, book: &Book) -> usize {
126        self.list_spreads(book).len()
127    }
128
129    /// Absolute index of the first entry on the current list spread.
130    pub fn list_spread_start(&self, book: &Book) -> usize {
131        self.list_spreads(book).get(self.list_spread).map_or(0, |r| r.start)
132    }
133
134    /// The slice of entries visible on the current list spread.
135    pub fn entries_visible<'b>(&self, book: &'b Book) -> Vec<&'b yog_formats::book::BookEntry> {
136        let range = self.list_spreads(book).get(self.list_spread).cloned().unwrap_or_default();
137        let all = self.entries_in_cat(book);
138        all.into_iter().skip(range.start).take(range.len()).collect()
139    }
140
141    pub fn current_entry<'b>(&self, book: &'b Book) -> Option<&'b yog_formats::book::BookEntry> {
142        self.entries_in_cat(book).into_iter().nth(self.entry)
143    }
144
145    pub fn page_count(&self, book: &Book) -> usize {
146        self.current_entry(book)
147            .map(|e| e.pages.len().max(1))
148            .unwrap_or(1)
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    #[test]
155    fn a_long_entry_name_takes_more_rows() {
156        assert_eq!(super::list_rows("Типы"), 1);
157        assert!(super::list_rows(&"Продвинутое Экранирование ".repeat(3)) > 1);
158    }
159}