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