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