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