1mod flow;
7pub(crate) mod geometry;
8mod names;
9mod overlay;
10mod page;
11mod recipe;
12mod spread;
13mod widgets;
14
15use std::collections::HashMap;
16use std::time::Instant;
17
18use yog_formats::book::Book;
19use yog_gfx::{Color, GfxContext, Rect};
20use yog_ui::UiRoot;
21
22pub use geometry::{BLOCK_PITCH, SLOT_YAW};
23pub use overlay::PageDrawer;
24pub use yog_ids::yog::{BOOK_SHEET, CRAFTING_SHEET};
28
29use crate::state::BookViewState;
30use crate::theme::BookTheme;
31use flow::{flow_traced, styled};
32use geometry::{BK_H, BK_W, MIN_SCALE, SCREEN_FILL, SCREEN_MARGIN};
33use overlay::{BgSprite, OverlayCmd};
34use recipe::{RecipeVis, parse_recipe};
35use spread::Spread;
36
37pub static BOOK_PNG: &[u8] = include_bytes!("../../assets/book_brown.png");
39pub static CRAFTING_PNG: &[u8] = include_bytes!("../../assets/crafting.png");
41pub static LANG: &[(&str, &str)] = &[
44 ("en_us", include_str!("../../assets/lang/en_us.json")),
45 ("ru_ru", include_str!("../../assets/lang/ru_ru.json")),
46];
47
48pub struct BookRenderer {
50 pub book: Book,
51 pub state: BookViewState,
52 pub theme: BookTheme,
53 recipes: HashMap<String, RecipeVis>,
54 origins: HashMap<String, Vec<usize>>,
56 written: Book,
58 measured: bool,
60 start: Instant,
62 pub ui: Option<UiRoot>,
63 bg_sprites: Vec<BgSprite>,
64 overlays: Vec<OverlayCmd>,
65 dirty: bool,
66 last_screen: (f32, f32),
68 source: Option<Book>,
71 translator: Option<names::Translator>,
72 probe: Option<Option<String>>,
73}
74
75impl BookRenderer {
76 pub fn new(book: Book) -> Self {
77 let theme = BookTheme::default().with_nameplate(&book.nameplate_color);
78 let written = styled(&book);
79 let (flowed, origins) = flow_traced(written.clone());
80 let source = book.i18n.then_some(book);
83 Self {
84 book: flowed,
85 origins,
86 written,
87 measured: false,
88 start: Instant::now(),
89 state: BookViewState::default(),
90 theme,
91 recipes: HashMap::new(),
92 ui: None,
93 bg_sprites: Vec::new(),
94 overlays: Vec::new(),
95 dirty: true,
96 last_screen: (0.0, 0.0),
97 translator: None,
98 probe: None,
99 source,
100 }
101 }
102
103 pub fn book(&self) -> &Book {
105 &self.book
106 }
107
108 pub fn set_translator(&mut self, tr: fn(&str) -> Option<String>) {
113 names::set_translator(tr);
114 if self.book.i18n {
115 self.translator = Some(tr);
116 self.probe = None;
117 }
118 }
119
120 fn localize(&mut self) {
122 let (Some(src), Some(tr)) = (&self.source, self.translator) else {
123 return;
124 };
125 let probe = Some(tr(&src.name));
126 if probe == self.probe {
127 return;
128 }
129 let accent = src.accent.clone();
130 self.written = src.localized(|k| {
131 crate::markup::resolve(&tr(k).unwrap_or_else(|| k.to_string()), &accent)
132 });
133 (self.book, self.origins) = flow_traced(self.written.clone());
134 self.probe = probe;
135 self.dirty = true;
136 }
137
138 pub fn handle_event(&mut self, ev: &str) {
139 if self.state.handle(ev, &self.book) {
140 self.dirty = true;
141 }
142 }
143
144 pub fn add_recipe(&mut self, id: impl Into<String>, recipe: &yog_data::Value) {
147 if let Some(vis) = parse_recipe(recipe) {
148 self.recipes.insert(id.into(), vis);
149 self.dirty = true;
150 }
151 }
152
153 fn prepare(&mut self, sw: f32, sh: f32) -> Rect {
158 let room = ((sw - SCREEN_MARGIN) / BK_W).min((sh - SCREEN_MARGIN) / BK_H);
159 let wanted = (sh * SCREEN_FILL / BK_H).round().max(1.0);
160 let scale = if room >= 1.0 { wanted.min(room.floor()) } else { room.max(MIN_SCALE) };
161 let (bw, bh) = (BK_W * scale, BK_H * scale);
162 let book_rect = Rect::new((sw - bw) / 2.0, (sh - bh) / 2.0, bw, bh);
163 self.localize();
164 if self.dirty || (sw, sh) != self.last_screen {
165 let sheet = names::book_sheet(&self.book.book_texture);
166 let spread = Spread {
167 book: &self.book,
168 state: &self.state,
169 theme: &self.theme,
170 recipes: &self.recipes,
171 sheet: &sheet,
172 };
173 let (root, bg_sprites, overlays) = spread.build(book_rect);
174 (self.ui, self.bg_sprites, self.overlays) = (Some(root), bg_sprites, overlays);
175 self.dirty = false;
176 self.last_screen = (sw, sh);
177 }
178 if let Some(ui) = &mut self.ui
179 && ui.needs_layout
180 {
181 ui.layout(sw, sh);
182 }
183 book_rect
184 }
185
186 pub fn render(&mut self, ctx: &GfxContext, sw: f32, sh: f32) {
191 self.render_with(ctx, sw, sh, &mut |_, _, _, _| {});
192 }
193
194 pub fn render_with(&mut self, ctx: &GfxContext, sw: f32, sh: f32, draw: PageDrawer<'_>) {
197 yog_ui::text::use_host_metrics(ctx);
199 if !self.measured {
200 self.measured = true;
201 self.reflow();
202 }
203 let book_rect = self.prepare(sw, sh);
204 let sheet = names::book_sheet(&self.book.book_texture);
205 ctx.draw2d().image_region(
206 &sheet,
207 book_rect,
208 Some(Rect::new(0.0, 0.0, BK_W, BK_H)),
209 Color::WHITE,
210 );
211 overlay::draw_sprites(ctx, &self.bg_sprites, &sheet);
212 if let Some(ui) = &mut self.ui {
213 ui.render(ctx);
214 }
215 let elapsed = self.start.elapsed();
218 overlay::draw_scenes(ctx, &self.overlays, elapsed.as_secs_f32(), draw);
219 overlay::draw_items(ctx, &self.overlays, elapsed.as_secs() as usize);
220 }
221
222 pub fn animated(&self) -> bool {
225 self.overlays.iter().any(OverlayCmd::animated)
226 }
227
228 pub fn click(&mut self, x: f32, y: f32) -> Option<String> {
232 let ui = self.ui.as_ref()?;
233 let hit = yog_ui::layout::hit_test(&ui.layout_root, x, y)?;
234 let action = hit.on_click.clone()?;
235 self.handle_event(&action);
236 Some(action)
237 }
238
239 pub fn show(&mut self, entry: &str, page: usize) -> bool {
243 let Some(e) = self.book.entries.iter().find(|e| e.id == entry) else {
244 return false;
245 };
246 let Some(cat) = self.book.categories.iter().position(|c| c.id == e.category) else {
247 return false;
248 };
249 self.state.cat = cat;
250 self.state.at_home = false;
251 let Some(i) = self
252 .state
253 .entries_in_cat(&self.book)
254 .iter()
255 .position(|x| x.id == entry)
256 else {
257 return false;
258 };
259 let shown = self
260 .origins
261 .get(entry)
262 .and_then(|o| o.iter().position(|&w| w >= page))
263 .unwrap_or(page);
264 self.state.entry = i;
265 self.state.page = shown.min(e.pages.len().saturating_sub(1));
266 self.dirty = true;
267 true
268 }
269
270 pub fn shown(&self) -> Option<(String, usize)> {
273 if self.state.at_home {
274 return None;
275 }
276 let e = self.state.current_entry(&self.book)?;
277 let page = self
278 .origins
279 .get(&e.id)
280 .and_then(|o| o.get(self.state.page))
281 .copied()
282 .unwrap_or(self.state.page);
283 Some((e.id.clone(), page))
284 }
285
286 fn reflow(&mut self) {
288 let at = self.shown();
289 (self.book, self.origins) = flow_traced(self.written.clone());
290 self.dirty = true;
291 if let Some((entry, page)) = at {
292 self.show(&entry, page);
293 }
294 }
295
296 pub fn set_book(&mut self, book: Book) {
298 let at = self.shown();
299 self.written = styled(&book);
300 self.probe = None;
301 self.source = book.i18n.then_some(book);
302 (self.book, self.origins) = flow_traced(self.written.clone());
303 self.localize();
304 self.theme = BookTheme::default().with_nameplate(&self.book.nameplate_color);
305 self.dirty = true;
306 match at {
307 Some((entry, page)) if self.show(&entry, page) => {}
308 _ => {
309 if self.state.cat >= self.book.categories.len() {
310 self.state = BookViewState::default();
311 }
312 }
313 }
314 }
315}
316
317#[cfg(test)]
318mod tests;