1pub mod state;
7pub mod theme;
8pub mod font;
9pub mod svg;
10pub mod renderer;
11
12use serde::{Deserialize, Serialize};
13use yog_registry::ItemDef;
14
15pub use state::BookViewState;
16pub use theme::BookTheme;
17pub use font::BookFont;
18pub use renderer::BookRenderer;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct BookMacro(pub String, pub String);
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
30pub enum BookPage {
31 Text {
33 text: String,
34 },
35 Spotlight {
37 item: ItemDef,
38 title: Option<String>,
39 text: Option<String>,
40 },
41 Crafting {
43 recipe_id: String,
44 text: Option<String>,
45 },
46 Smelting {
48 recipe_id: String,
49 text: Option<String>,
50 },
51 Image {
53 texture: String,
54 title: Option<String>,
55 text: Option<String>,
56 border: bool,
57 },
58 Entity {
60 entity_type: String,
61 name: Option<String>,
62 text: Option<String>,
63 },
64 Relations {
66 entries: Vec<String>,
67 text: Option<String>,
68 },
69 Empty,
71 Pattern {
73 op_id: String,
74 anchor: String,
75 input: String,
76 output: String,
77 text: String,
78 },
79 Svg {
81 data: String,
83 title: Option<String>,
84 text: Option<String>,
85 },
86 CustomText {
88 text: String,
89 font: BookFont,
90 color: u32,
92 },
93}
94
95#[derive(Debug, Clone, Default, Serialize, Deserialize)]
99pub struct BookCategory {
100 pub id: String,
101 pub name: String,
102 pub description: Option<String>,
103 pub icon: Option<String>,
105 pub icon_svg: Option<String>,
107 pub sortnum: i32,
109}
110
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
115pub struct BookEntry {
116 pub id: String,
117 pub name: String,
118 pub category: String,
119 pub pages: Vec<BookPage>,
120 pub icon: Option<String>,
122 pub icon_svg: Option<String>,
124 pub secret: bool,
126 pub priority: i32,
128 pub read_by_default: bool,
130 pub advancement: Option<String>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct Book {
139 pub id: String,
140 pub name: String,
141 pub nameplate_color: String,
142 pub landing_text: String,
143 pub author: Option<String>,
144 pub book_texture: String,
145 pub filler_texture: String,
146 pub model: String,
147 pub categories: Vec<BookCategory>,
148 pub entries: Vec<BookEntry>,
149 pub macros: Vec<BookMacro>,
150 pub use_resource_pack: bool,
151 pub show_progress: bool,
152 pub i18n: bool,
153 pub creative_tab: Option<String>,
154}
155
156impl Book {
157 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
158 Self {
159 id: id.into(),
160 name: name.into(),
161 nameplate_color: "000000".into(),
162 landing_text: String::new(),
163 author: None,
164 book_texture: "yog:textures/gui/book.png".into(),
165 filler_texture: "yog:textures/gui/book_filler.png".into(),
166 model: "minecraft:book".into(),
167 categories: Vec::new(),
168 entries: Vec::new(),
169 macros: Vec::new(),
170 use_resource_pack: false,
171 show_progress: true,
172 i18n: false,
173 creative_tab: None,
174 }
175 }
176
177 pub fn author(mut self, author: impl Into<String>) -> Self {
178 self.author = Some(author.into());
179 self
180 }
181
182 pub fn book_texture(mut self, tex: impl Into<String>) -> Self {
183 self.book_texture = tex.into();
184 self
185 }
186
187 pub fn filler_texture(mut self, tex: impl Into<String>) -> Self {
188 self.filler_texture = tex.into();
189 self
190 }
191
192 pub fn nameplate(mut self, color: impl Into<String>) -> Self {
193 self.nameplate_color = color.into();
194 self
195 }
196
197 pub fn landing_text(mut self, text: impl Into<String>) -> Self {
198 self.landing_text = text.into();
199 self
200 }
201
202 pub fn model(mut self, model: impl Into<String>) -> Self {
203 self.model = model.into();
204 self
205 }
206
207 pub fn creative_tab(mut self, tab: impl Into<String>) -> Self {
208 self.creative_tab = Some(tab.into());
209 self
210 }
211
212 pub fn show_progress(mut self, show: bool) -> Self {
213 self.show_progress = show;
214 self
215 }
216
217 pub fn i18n(mut self, val: bool) -> Self {
218 self.i18n = val;
219 self
220 }
221
222 pub fn use_resource_pack(mut self, val: bool) -> Self {
223 self.use_resource_pack = val;
224 self
225 }
226
227 pub fn add_macro(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
228 self.macros.push(BookMacro(key.into(), value.into()));
229 self
230 }
231
232 pub fn add_category(mut self, category: BookCategory) -> Self {
233 self.categories.push(category);
234 self
235 }
236
237 pub fn add_entry(mut self, entry: BookEntry) -> Self {
238 self.entries.push(entry);
239 self
240 }
241}
242
243impl Default for Book {
244 fn default() -> Self {
245 Self::new("yog:default", "Unknown Book")
246 }
247}
248
249#[derive(Debug, Default)]
253pub struct BookRegistry {
254 books: std::collections::HashMap<String, Book>,
255}
256
257impl BookRegistry {
258 pub fn register(&mut self, book: Book) {
259 self.books.insert(book.id.clone(), book);
260 }
261
262 pub fn get(&self, id: &str) -> Option<&Book> {
263 self.books.get(id)
264 }
265
266 pub fn all(&self) -> impl Iterator<Item = &Book> {
267 self.books.values()
268 }
269}
270
271pub fn text_page(text: impl Into<String>) -> BookPage {
274 BookPage::Text { text: text.into() }
275}
276
277pub fn spotlight_page(item: ItemDef) -> BookPage {
278 BookPage::Spotlight { item, title: None, text: None }
279}
280
281pub fn crafting_page(recipe_id: impl Into<String>) -> BookPage {
282 BookPage::Crafting { recipe_id: recipe_id.into(), text: None }
283}
284
285pub fn crafting_page_with_text(recipe_id: impl Into<String>, text: impl Into<String>) -> BookPage {
286 BookPage::Crafting { recipe_id: recipe_id.into(), text: Some(text.into()) }
287}
288
289pub fn smelting_page(recipe_id: impl Into<String>) -> BookPage {
290 BookPage::Smelting { recipe_id: recipe_id.into(), text: None }
291}
292
293pub fn image_page(texture: impl Into<String>) -> BookPage {
294 BookPage::Image { texture: texture.into(), title: None, text: None, border: true }
295}
296
297pub fn entity_page(entity_type: impl Into<String>) -> BookPage {
298 BookPage::Entity { entity_type: entity_type.into(), name: None, text: None }
299}
300
301pub fn relations_page(entries: Vec<String>) -> BookPage {
302 BookPage::Relations { entries, text: None }
303}
304
305pub fn pattern_page(op_id: impl Into<String>, anchor: impl Into<String>, input: impl Into<String>, output: impl Into<String>, text: impl Into<String>) -> BookPage {
306 BookPage::Pattern {
307 op_id: op_id.into(),
308 anchor: anchor.into(),
309 input: input.into(),
310 output: output.into(),
311 text: text.into(),
312 }
313}
314
315pub mod book_ui {
317 use crate::{Book, BookEntry, BookPage};
318 use yog_ui::widget::{self, Widget};
319 use yog_ui::{Align, FlexDir, UiRoot};
320
321 pub fn build_book_ui(book: &Book, selected_cat: usize, selected_entry: usize, current_page: usize) -> UiRoot {
325 let mut cats: Vec<Widget> = Vec::new();
326 for (i, cat) in book.categories.iter().enumerate() {
327 let color = if i == selected_cat { 0xFF_FFFF55 } else { 0xFF_CCCCCC };
328 cats.push(widget::button(&cat.name)
329 .color(color)
330 .on_click(format!("cat:{}", i)));
331 }
332
333 let cat = book.categories.get(selected_cat);
334 let mut entries: Vec<Widget> = Vec::new();
335 if let Some(cat) = cat {
336 let cat_entries: Vec<&BookEntry> = book.entries.iter()
337 .filter(|e| e.category == cat.id).collect();
338 for (i, entry) in cat_entries.iter().enumerate() {
339 let color = if i == selected_entry { 0xFF_FFFF55 } else { 0xFF_CCCCCC };
340 let label = if entry.name.len() > 14 { &entry.name[..14] } else { &entry.name };
341 entries.push(widget::button(label)
342 .color(color)
343 .on_click(format!("entry:{}", i)));
344 }
345 }
346
347 let mut pages: Vec<Widget> = Vec::new();
348 if let Some(cat) = cat {
349 let cat_entries: Vec<&BookEntry> = book.entries.iter()
350 .filter(|e| e.category == cat.id).collect();
351 if let Some(entry) = cat_entries.get(selected_entry) {
352 if let Some(page) = entry.pages.get(current_page) {
353 pages.push(render_page(page));
354 }
355 }
356 }
357
358 let nav = widget::panel(FlexDir::Row).gap(4.0)
359 .child(widget::button("<").w(28.0).on_click("prev_page"))
360 .child(widget::label(&format!("{}/{}", current_page + 1,
361 cat.map_or(0, |c| {
362 book.entries.iter().filter(|e| e.category == c.id).nth(selected_entry)
363 .map_or(0, |e| e.pages.len())
364 }))).color(0xFF_888888).flex(1.0).align(Align::Center))
365 .child(widget::button(">").w(28.0).on_click("next_page"));
366
367 UiRoot::new(&book.id,
368 widget::panel(FlexDir::Row).gap(2.0)
369 .padding(2.0, 2.0, 2.0, 2.0).bg(0xFF_2A1A0E)
370 .child(
371 widget::panel(FlexDir::Column).w(104.0)
372 .child(widget::label("Categories").color(0xFF_888888))
373 .child(widget::panel(FlexDir::Column).gap(1.0)
374 .child_many(cats))
375 .child(widget::label("Entries").color(0xFF_888888))
376 .child(widget::panel(FlexDir::Column).gap(1.0)
377 .child_many(entries))
378 )
379 .child(
380 widget::panel(FlexDir::Column).flex(1.0).gap(2.0)
381 .child(widget::panel(FlexDir::Column).flex(1.0)
382 .child_many(pages))
383 .child(nav)
384 )
385 )
386 }
387
388 fn render_page(page: &BookPage) -> Widget {
389 match page {
390 BookPage::Text { text } =>
391 widget::label(text).color(0xFF_CCCCAA),
392 BookPage::Spotlight { item, title, text } => {
393 let mut p = widget::panel(FlexDir::Column).gap(2.0);
394 if let Some(t) = title { p = p.child(widget::label(t).color(0xFF_FFFF55)); }
395 p = p.child(widget::item_slot(&item.id));
396 if let Some(t) = text { p = p.child(widget::label(t).color(0xFF_CCCCAA)); }
397 p
398 }
399 BookPage::Crafting { recipe_id, text } => {
400 let mut p = widget::panel(FlexDir::Column).gap(2.0);
401 p = p.child(widget::label(format!("Crafting: {}", recipe_id)).color(0xFF_888888));
402 if let Some(t) = text { p = p.child(widget::label(t).color(0xFF_CCCCAA)); }
403 p
404 }
405 BookPage::Smelting { recipe_id, text } => {
406 let mut p = widget::panel(FlexDir::Column).gap(2.0);
407 p = p.child(widget::label(format!("Smelting: {}", recipe_id)).color(0xFF_888888));
408 if let Some(t) = text { p = p.child(widget::label(t).color(0xFF_CCCCAA)); }
409 p
410 }
411 BookPage::Empty => widget::spacer(),
412 _ => widget::label("(unsupported page)").color(0xFF_888888),
413 }
414 }
415
416 trait WidgetExt {
418 fn child_many(self, children: Vec<Widget>) -> Self;
419 }
420 impl WidgetExt for Widget {
421 fn child_many(mut self, children: Vec<Widget>) -> Self {
422 for c in children { self = self.child(c); }
423 self
424 }
425 }
426}
427
428fn esc(s: &str) -> String {
431 s.replace('\\', "\\\\").replace('"', "\\\"")
432}
433
434impl BookPage {
435 pub fn to_json(&self) -> String {
436 match self {
437 Self::Text { text } =>
438 format!(r#"{{"type":"text","text":"{}"}}"#, esc(text)),
439 Self::Spotlight { item, title, text } => {
440 let t = title.as_deref().map(|s| format!(r#","title":"{}""#, esc(s))).unwrap_or_default();
441 let tx = text.as_deref().map(|s| format!(r#","text":"{}""#, esc(s))).unwrap_or_default();
442 format!(r#"{{"type":"spotlight","item":"{id}"{t}{tx}}}"#, id = esc(&item.id))
443 }
444 Self::Crafting { recipe_id, text } => {
445 let tx = text.as_deref().map(|s| format!(r#","text":"{}""#, esc(s))).unwrap_or_default();
446 format!(r#"{{"type":"crafting","recipe":"{}"{}}}"#, esc(recipe_id), tx)
447 }
448 Self::Smelting { recipe_id, text } => {
449 let tx = text.as_deref().map(|s| format!(r#","text":"{}""#, esc(s))).unwrap_or_default();
450 format!(r#"{{"type":"smelting","recipe":"{}"{}}}"#, esc(recipe_id), tx)
451 }
452 Self::Image { texture, title, text, border } => {
453 let t = title.as_deref().map(|s| format!(r#","title":"{}""#, esc(s))).unwrap_or_default();
454 let tx = text.as_deref().map(|s| format!(r#","text":"{}""#, esc(s))).unwrap_or_default();
455 format!(r#"{{"type":"image","texture":"{}","border":{}{}{}}}"#,
456 esc(texture), border, t, tx)
457 }
458 Self::Entity { entity_type, name, text } => {
459 let n = name.as_deref().map(|s| format!(r#","name":"{}""#, esc(s))).unwrap_or_default();
460 let tx = text.as_deref().map(|s| format!(r#","text":"{}""#, esc(s))).unwrap_or_default();
461 format!(r#"{{"type":"entity","entity":"{}"{}{}}}"#, esc(entity_type), n, tx)
462 }
463 Self::Relations { entries, text } => {
464 let e: String = entries.iter().map(|s| format!(r#""{}""#, esc(s))).collect::<Vec<_>>().join(",");
465 let tx = text.as_deref().map(|s| format!(r#","text":"{}""#, esc(s))).unwrap_or_default();
466 format!(r#"{{"type":"relations","entries":[{}]{}}}"#, e, tx)
467 }
468 Self::Empty => r#"{"type":"empty"}"#.to_string(),
469 Self::Pattern { op_id, anchor, input, output, text } =>
470 format!(r#"{{"type":"pattern","op_id":"{}","anchor":"{}","input":"{}","output":"{}","text":"{}"}}"#,
471 esc(op_id), esc(anchor), esc(input), esc(output), esc(text)),
472 Self::Svg { data, title, text } => {
473 let t = title.as_deref().map(|s| format!(r#","title":"{}""#, esc(s))).unwrap_or_default();
474 let tx = text.as_deref().map(|s| format!(r#","text":"{}""#, esc(s))).unwrap_or_default();
475 format!(r#"{{"type":"svg","data":"{}"{}{}}}"#, esc(data), t, tx)
476 }
477 Self::CustomText { text, font, color } =>
478 format!(r#"{{"type":"custom_text","text":"{}","font_id":"{}","size_px":{},"color":{}}}"#,
479 esc(text), esc(&font.font_id), font.size_px, color),
480 }
481 }
482}
483
484impl BookEntry {
485 pub fn to_json(&self) -> String {
486 let pages: String = self.pages.iter().map(|p| p.to_json()).collect::<Vec<_>>().join(",");
487 let icon = self.icon.as_deref().map(|s| format!(r#","icon":"{}""#, esc(s))).unwrap_or_default();
488 let adv = self.advancement.as_deref().map(|s| format!(r#","advancement":"{}""#, esc(s))).unwrap_or_default();
489 format!(
490 r#"{{"id":"{}","name":"{}","category":"{}","pages":[{}],"secret":{},"priority":{},"read_by_default":{}{}{}}}"#,
491 esc(&self.id), esc(&self.name), esc(&self.category), pages,
492 self.secret, self.priority, self.read_by_default, icon, adv
493 )
494 }
495}
496
497impl BookCategory {
498 pub fn to_json(&self) -> String {
499 let desc = self.description.as_deref().map(|s| format!(r#","description":"{}""#, esc(s))).unwrap_or_default();
500 let icon = self.icon.as_deref().map(|s| format!(r#","icon":"{}""#, esc(s))).unwrap_or_default();
501 format!(
502 r#"{{"id":"{}","name":"{}","sortnum":{}{}{}}}"#,
503 esc(&self.id), esc(&self.name), self.sortnum, desc, icon
504 )
505 }
506}
507
508impl Book {
509 pub fn to_json(&self) -> String {
510 let cats: String = self.categories.iter().map(|c| c.to_json()).collect::<Vec<_>>().join(",");
511 let entries: String = self.entries.iter().map(|e| e.to_json()).collect::<Vec<_>>().join(",");
512 let author = self.author.as_deref().map(|s| format!(r#","author":"{}""#, esc(s))).unwrap_or_default();
513 let tab = self.creative_tab.as_deref().map(|s| format!(r#","creative_tab":"{}""#, esc(s))).unwrap_or_default();
514 format!(
515 r#"{{"id":"{}","name":"{}","nameplate_color":"{}","landing_text":"{}","book_texture":"{}","filler_texture":"{}","model":"{}","show_progress":{},"i18n":{},"use_resource_pack":{},"categories":[{}],"entries":[{}]{}{}}}"#,
516 esc(&self.id), esc(&self.name), esc(&self.nameplate_color), esc(&self.landing_text),
517 esc(&self.book_texture), esc(&self.filler_texture), esc(&self.model),
518 self.show_progress, self.i18n, self.use_resource_pack,
519 cats, entries, author, tab
520 )
521 }
522}