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() {
114                ENTRIES_FIRST
115            } else {
116                ENTRIES_PER
117            };
118            if used + rows > room && i > start {
119                spreads.push(start..i);
120                (start, used) = (i, 0);
121            }
122            used += rows;
123        }
124        spreads.push(start..all.len());
125        spreads
126    }
127
128    /// How many list spreads the current category needs.
129    pub fn list_spread_count(&self, book: &Book) -> usize {
130        self.list_spreads(book).len()
131    }
132
133    /// Absolute index of the first entry on the current list spread.
134    pub fn list_spread_start(&self, book: &Book) -> usize {
135        self.list_spreads(book)
136            .get(self.list_spread)
137            .map_or(0, |r| r.start)
138    }
139
140    /// The slice of entries visible on the current list spread.
141    pub fn entries_visible<'b>(&self, book: &'b Book) -> Vec<&'b yog_formats::book::BookEntry> {
142        let range = self
143            .list_spreads(book)
144            .get(self.list_spread)
145            .cloned()
146            .unwrap_or_default();
147        let all = self.entries_in_cat(book);
148        all.into_iter()
149            .skip(range.start)
150            .take(range.len())
151            .collect()
152    }
153
154    pub fn current_entry<'b>(&self, book: &'b Book) -> Option<&'b yog_formats::book::BookEntry> {
155        self.entries_in_cat(book).into_iter().nth(self.entry)
156    }
157
158    pub fn page_count(&self, book: &Book) -> usize {
159        self.current_entry(book)
160            .map(|e| e.pages.len().max(1))
161            .unwrap_or(1)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    #[test]
168    fn a_long_entry_name_takes_more_rows() {
169        assert_eq!(super::list_rows("Типы"), 1);
170        assert!(super::list_rows(&"Продвинутое Экранирование ".repeat(3)) > 1);
171    }
172}