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