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 {
161 wanted.min(room.floor())
162 } else {
163 room.max(MIN_SCALE)
164 };
165 let (bw, bh) = (BK_W * scale, BK_H * scale);
166 let book_rect = Rect::new((sw - bw) / 2.0, (sh - bh) / 2.0, bw, bh);
167 self.localize();
168 if self.dirty || (sw, sh) != self.last_screen {
169 let sheet = names::book_sheet(&self.book.book_texture);
170 let spread = Spread {
171 book: &self.book,
172 state: &self.state,
173 theme: &self.theme,
174 recipes: &self.recipes,
175 sheet: &sheet,
176 };
177 let (root, bg_sprites, overlays) = spread.build(book_rect);
178 (self.ui, self.bg_sprites, self.overlays) = (Some(root), bg_sprites, overlays);
179 self.dirty = false;
180 self.last_screen = (sw, sh);
181 }
182 if let Some(ui) = &mut self.ui
183 && ui.needs_layout
184 {
185 ui.layout(sw, sh);
186 }
187 book_rect
188 }
189
190 pub fn render(&mut self, ctx: &GfxContext, sw: f32, sh: f32) {
195 self.render_with(ctx, sw, sh, &mut |_, _, _, _| {});
196 }
197
198 pub fn render_with(&mut self, ctx: &GfxContext, sw: f32, sh: f32, draw: PageDrawer<'_>) {
201 yog_ui::text::use_host_metrics(ctx);
203 if !self.measured {
204 self.measured = true;
205 self.reflow();
206 }
207 let book_rect = self.prepare(sw, sh);
208 let sheet = names::book_sheet(&self.book.book_texture);
209 ctx.draw2d().image_region(
210 &sheet,
211 book_rect,
212 Some(Rect::new(0.0, 0.0, BK_W, BK_H)),
213 Color::WHITE,
214 );
215 overlay::draw_sprites(ctx, &self.bg_sprites, &sheet);
216 if let Some(ui) = &mut self.ui {
217 ui.render(ctx);
218 }
219 let elapsed = self.start.elapsed();
222 overlay::draw_scenes(ctx, &self.overlays, elapsed.as_secs_f32(), draw);
223 overlay::draw_items(ctx, &self.overlays, elapsed.as_secs() as usize);
224 }
225
226 pub fn animated(&self) -> bool {
229 self.overlays.iter().any(OverlayCmd::animated)
230 }
231
232 pub fn click(&mut self, x: f32, y: f32) -> Option<String> {
236 let ui = self.ui.as_ref()?;
237 let hit = yog_ui::layout::hit_test(&ui.layout_root, x, y)?;
238 let action = hit.on_click.clone()?;
239 self.handle_event(&action);
240 Some(action)
241 }
242
243 pub fn show(&mut self, entry: &str, page: usize) -> bool {
247 let Some(e) = self.book.entries.iter().find(|e| e.id == entry) else {
248 return false;
249 };
250 let Some(cat) = self.book.categories.iter().position(|c| c.id == e.category) else {
251 return false;
252 };
253 self.state.cat = cat;
254 self.state.at_home = false;
255 let Some(i) = self
256 .state
257 .entries_in_cat(&self.book)
258 .iter()
259 .position(|x| x.id == entry)
260 else {
261 return false;
262 };
263 let shown = self
264 .origins
265 .get(entry)
266 .and_then(|o| o.iter().position(|&w| w >= page))
267 .unwrap_or(page);
268 self.state.entry = i;
269 self.state.page = shown.min(e.pages.len().saturating_sub(1));
270 self.dirty = true;
271 true
272 }
273
274 pub fn shown(&self) -> Option<(String, usize)> {
277 if self.state.at_home {
278 return None;
279 }
280 let e = self.state.current_entry(&self.book)?;
281 let page = self
282 .origins
283 .get(&e.id)
284 .and_then(|o| o.get(self.state.page))
285 .copied()
286 .unwrap_or(self.state.page);
287 Some((e.id.clone(), page))
288 }
289
290 fn reflow(&mut self) {
292 let at = self.shown();
293 (self.book, self.origins) = flow_traced(self.written.clone());
294 self.dirty = true;
295 if let Some((entry, page)) = at {
296 self.show(&entry, page);
297 }
298 }
299
300 pub fn set_book(&mut self, book: Book) {
302 let at = self.shown();
303 self.written = styled(&book);
304 self.probe = None;
305 self.source = book.i18n.then_some(book);
306 (self.book, self.origins) = flow_traced(self.written.clone());
307 self.localize();
308 self.theme = BookTheme::default().with_nameplate(&self.book.nameplate_color);
309 self.dirty = true;
310 match at {
311 Some((entry, page)) if self.show(&entry, page) => {}
312 _ => {
313 if self.state.cat >= self.book.categories.len() {
314 self.state = BookViewState::default();
315 }
316 }
317 }
318 }
319}
320
321#[cfg(test)]
322mod tests;