1use crate::controls::intersect;
6use crate::*;
7use banished_formats::ui;
8use banished_formats::{
9 crc::type_id,
10 crs,
11 error::{Error, Result},
12};
13use std::collections::HashMap;
14
15#[derive(Debug, Clone)]
16pub struct Document {
17 pub(crate) text: String,
18 pub(crate) width: i32,
19 pub(crate) size: [i32; 2],
20 pub(crate) links: Vec<(String, [i32; 4])>,
22 pub(crate) hovered_link: Option<usize>,
23 pub(crate) pressed_link: Option<usize>,
24 fonts: Vec<Option<FontId>>,
25 sheets: Vec<Option<SheetId>>,
26 colors: Vec<u32>,
27 bullets: Vec<Option<SpriteRef>>,
28 underline: Option<SpriteRef>,
29 link_colors: [u32; 3],
30 indent: i32,
31 items: Vec<Item>,
32 captures: HashMap<String, Capture>,
33}
34
35#[derive(Debug, Clone)]
36struct Item {
37 content: Content,
38 pos: [i32; 2],
39 size: [i32; 2],
40 color: u32,
41 link: Option<String>,
42}
43#[derive(Debug, Clone)]
44enum Content {
45 Text(TextData),
46 Sprite(SpriteRef),
47 Capture(Capture),
48 Space,
49}
50
51#[derive(Debug, Clone)]
52struct Capture {
53 size: [i32; 2],
54 parts: Vec<CapturePart>,
55}
56#[derive(Debug, Clone)]
57struct CapturePart {
58 sheet: i32,
59 sprite: Option<SpriteRef>,
60 rect: [i32; 4],
61 color: u32,
62}
63
64struct RawCapture {
65 sheets: Vec<Option<String>>,
66 size: [i32; 2],
67 parts: Vec<(i32, String, [i32; 4], u32)>,
68 rotated_parts: usize,
69}
70
71fn read_capture(bytes: &[u8]) -> Result<RawCapture> {
73 crs::parse(bytes, |r, h| {
74 if h.type_id != type_id("SpriteCapture") {
75 return Err(Error::Unsupported("expected SpriteCapture".into()));
76 }
77 r.block(|r, _| {
78 let sheets = (0..r.count()?)
79 .map(|_| r.external_ref())
80 .collect::<Result<Vec<_>>>()?;
81 let size = [r.i32()?, r.i32()?];
82 let mut rotated_parts = 0;
83 let parts = (0..r.count()?)
84 .map(|_| {
85 let sheet = r.i32()?;
86 let name = r.string()?;
87 let rect = [r.i32()?, r.i32()?, r.i32()?, r.i32()?];
88 let angle = r.f32()?;
89 rotated_parts += usize::from(angle != 0.0);
90 let color = r.u32()?;
91 Ok((sheet, name, rect, color))
92 })
93 .collect::<Result<_>>()?;
94 Ok(RawCapture {
95 sheets,
96 size,
97 parts,
98 rotated_parts,
99 })
100 })
101 })
102 .map(|(_, capture)| capture)
103}
104
105fn read_capture_list(bytes: &[u8]) -> Result<Vec<Option<String>>> {
106 crs::parse(bytes, |r, h| {
107 if h.type_id != type_id("ExternalList") {
108 return Err(Error::Unsupported("expected ExternalList".into()));
109 }
110 r.block(|r, _| (0..r.count()?).map(|_| r.external_ref()).collect())
111 })
112 .map(|(_, list)| list)
113}
114
115fn capture_name(reference: &str) -> String {
116 reference
117 .split(':')
118 .next()
119 .unwrap_or(reference)
120 .replace('\\', "/")
121 .to_ascii_lowercase()
122}
123
124#[derive(Debug, Clone, PartialEq)]
125enum Token {
126 Word(String),
127 Command(char, Option<char>, String),
128}
129
130fn tokenize(text: &str) -> Vec<Token> {
133 let mut chars = text.chars().peekable();
134 let mut out = Vec::new();
135 while let Some(ch) = chars.next() {
136 if ch.is_ascii_whitespace() {
137 continue;
138 }
139 if ch != '^' {
140 let mut word = String::from(ch);
141 while chars
142 .peek()
143 .is_some_and(|c| !c.is_ascii_whitespace() && *c != '^')
144 {
145 word.push(chars.next().unwrap());
146 }
147 out.push(Token::Word(word));
148 continue;
149 }
150 let Some(command) = chars.next() else {
151 break;
152 };
153 let suffix = if "fcjvmlai".contains(command) {
154 chars.next()
155 } else {
156 None
157 };
158 let argument = if command == 'd'
159 || command == 'i'
160 || (command == 'a' && suffix == Some('b'))
161 || (command == 'm' && matches!(suffix, Some('l' | 'r')))
162 {
163 let mut value = String::new();
164 while chars
165 .peek()
166 .is_some_and(|c| !c.is_ascii_whitespace() && *c != '^')
167 {
168 value.push(chars.next().unwrap());
169 }
170 value
171 } else {
172 String::new()
173 };
174 out.push(Token::Command(command, suffix, argument));
175 }
176 out
177}
178
179struct Row {
180 start: usize,
181 x: i32,
182 y: i32,
183 left: i32,
184 right: i32,
185 align: char,
186 vertical: char,
187 font: usize,
188 color: usize,
189 link: Option<String>,
190 restore_left: i32,
191}
192
193impl Ui {
194 pub fn document_link(&self, id: NodeId, index: usize) -> Option<&str> {
196 match &self.nodes.get(id)?.kind {
197 Kind::Control(Control::Document(doc)) => {
198 doc.links.get(index).map(|(target, _)| target.as_str())
199 }
200 _ => None,
201 }
202 }
203
204 pub(crate) fn load_document(&mut self, spec: &ui::DocumentText) -> Document {
205 let fonts = spec
206 .fonts
207 .iter()
208 .map(|f| f.as_deref().and_then(|f| self.res.font(f)))
209 .collect();
210 let sheets = spec
211 .sheets
212 .iter()
213 .map(|s| s.as_deref().and_then(|s| self.res.sheet(s)))
214 .collect();
215 let bullets = spec
216 .bullets
217 .iter()
218 .map(|name| self.res.sprite(spec.sprite_sheet.as_deref(), name))
219 .collect();
220 let mut captures = HashMap::new();
221 if let Some(reference) = &spec.images {
222 let refs = self
223 .res
224 .archive
225 .read_ref(reference)
226 .and_then(|d| read_capture_list(&d));
227 match refs {
228 Ok(refs) => {
229 for reference in refs.into_iter().flatten() {
230 match self
231 .res
232 .archive
233 .read_ref(&reference)
234 .and_then(|d| read_capture(&d))
235 {
236 Ok(raw) => {
237 if raw.rotated_parts > 0 {
238 log::warn!("capture {reference}: {} rotated sprites use axis-aligned quads",raw.rotated_parts);
239 }
240 let parts = raw
241 .parts
242 .into_iter()
243 .map(|(sheet, name, rect, color)| CapturePart {
244 sheet,
245 sprite: usize::try_from(sheet)
246 .ok()
247 .and_then(|i| raw.sheets.get(i))
248 .and_then(|s| self.res.sprite(s.as_deref(), &name)),
249 rect,
250 color,
251 })
252 .collect();
253 captures.insert(
254 capture_name(&reference),
255 Capture {
256 size: raw.size,
257 parts,
258 },
259 );
260 }
261 Err(e) => log::warn!("document capture {reference}: {e}"),
262 }
263 }
264 }
265 Err(e) => log::warn!("document capture list {reference}: {e}"),
266 }
267 }
268 Document {
269 text: self.res.string(spec.string_table.as_deref(), &spec.text),
270 width: spec.width.max(1),
271 size: [0, 0],
272 links: Vec::new(),
273 hovered_link: None,
274 pressed_link: None,
275 fonts,
276 sheets,
277 bullets,
278 colors: spec.colors.clone(),
279 underline: self
280 .res
281 .sprite(spec.sprite_sheet.as_deref(), &spec.underline_sprite),
282 link_colors: spec.link_colors,
283 indent: spec.indent,
284 items: Vec::new(),
285 captures,
286 }
287 }
288
289 pub(crate) fn layout_document(&self, doc: &mut Document) {
290 doc.items.clear();
291 doc.links.clear();
292 let mut row = Row {
293 start: 0,
294 x: 0,
295 y: 0,
296 left: 0,
297 right: doc.width.max(1),
298 align: 'l',
299 vertical: 'b',
300 font: 0,
301 color: 0,
302 link: None,
303 restore_left: 0,
304 };
305 let mut columns = [0, 0];
306 let tokens = tokenize(&doc.text);
307 for (token_index, token) in tokens.iter().cloned().enumerate() {
308 match token {
309 Token::Word(word) => {
310 let font = doc.fonts.get(row.font).copied().flatten();
311 let text = format!("{word} ");
312 let layout = font
313 .map(|f| TextLayout::compute(&self.res, f, &text, None, 0))
314 .unwrap_or_default();
315 let size = [layout.width, layout.height];
316 let data = TextData {
317 font,
318 text,
319 max_width: 0,
320 justify: 0,
321 wrap: false,
322 shadow: false,
323 ellipsis_width: 0,
324 layout,
325 };
326 self.document_item(doc, &mut row, Content::Text(data), size);
327 }
328 Token::Command(command, suffix, arg) => {
329 match (command, suffix) {
330 ('n', _) => self.document_end_row(doc, &mut row),
331 ('p', _) => {
332 self.document_end_row(doc, &mut row);
333 self.document_end_row(doc, &mut row);
334 }
335 ('f', Some(d)) => {
336 if let Some(index) = d.to_digit(10) {
337 if (index as usize) < doc.fonts.len() {
338 row.font = index as usize;
339 }
340 }
341 }
342 ('c', Some(d)) => {
343 if let Some(index) = d.to_digit(10) {
344 if (index as usize) < doc.colors.len() {
345 row.color = index as usize;
346 }
347 }
348 }
349 ('j', Some(j @ ('l' | 'r' | 'c'))) => {
350 if row.start < doc.items.len() {
351 self.document_end_row(doc, &mut row);
352 }
353 row.align = j;
354 }
355 ('v', Some(v @ ('t' | 'c' | 'b'))) => {
356 if row.start < doc.items.len() {
357 self.document_end_row(doc, &mut row);
358 }
359 row.vertical = v;
360 }
361 ('m', Some('l' | 'r')) => {
362 if row.start < doc.items.len() {
363 self.document_end_row(doc, &mut row);
364 }
365 let value = arg.parse::<i32>().unwrap_or(0);
366 if suffix == Some('l') {
367 row.left = value.max(0);
368 row.x = row.left;
369 } else {
370 row.right = if value < 0 { doc.width } else { value };
371 }
372 }
373 ('m', Some('b' | 'c' | 'e')) => {
374 if row.start < doc.items.len() {
375 self.document_end_row(doc, &mut row);
376 }
377 match suffix {
378 Some('b') => columns = [row.y, row.y],
379 Some('c') => {
380 columns[1] = columns[1].max(row.y);
381 row.y = columns[0];
382 }
383 _ => {
384 columns[1] = columns[1].max(row.y);
385 row.y = columns[1];
386 }
387 }
388 row.x = row.left;
389 }
390 ('l', Some('b' | 'e')) => {
391 if row.start < doc.items.len() {
392 self.document_end_row(doc, &mut row);
393 }
394 row.left = if suffix == Some('b') {
395 row.left + doc.indent
396 } else {
397 row.left - doc.indent
398 }
399 .max(0);
400 row.x = row.left;
401 }
402 ('l', Some('i')) => {
403 if row.start < doc.items.len() {
404 self.document_end_row(doc, &mut row);
405 }
406 if let Some(sprite) = doc.bullets.get(row.font).copied().flatten() {
407 row.left -= sprite.width;
408 row.x = row.left;
409 row.restore_left = sprite.width;
410 self.document_item(
411 doc,
412 &mut row,
413 Content::Sprite(sprite),
414 [sprite.width, sprite.height],
415 );
416 }
417 }
418 ('l', Some('o')) => {
419 if row.start < doc.items.len() {
420 self.document_end_row(doc, &mut row);
421 }
422 let font = doc.fonts.get(row.font).copied().flatten();
424 let marker_width = tokens[token_index + 1..]
425 .iter()
426 .find_map(|token| match token {
427 Token::Word(word) => font.map(|f| {
428 TextLayout::compute(
429 &self.res,
430 f,
431 &format!("{word} "),
432 None,
433 0,
434 )
435 .width
436 }),
437 _ => None,
438 })
439 .unwrap_or(doc.indent);
440 row.y += font.map_or(0, |f| self.res.fonts[f].font.size / 4);
441 row.left -= marker_width;
442 row.x = row.left;
443 row.restore_left = marker_width;
444 }
445 ('a', Some('b')) => row.link = (!arg.is_empty()).then_some(arg),
446 ('a', Some('e')) => row.link = None,
447 ('i', Some(d)) => {
448 if let Some(sheet) = d
449 .to_digit(10)
450 .and_then(|i| doc.sheets.get(i as usize))
451 .copied()
452 .flatten()
453 {
454 if let Some(s) = self.res.sheets[sheet].sheet.sprite(&arg) {
455 let sprite = SpriteRef {
456 sheet,
457 width: s.width,
458 height: s.height,
459 uv: s.uv,
460 };
461 self.document_item(
462 doc,
463 &mut row,
464 Content::Sprite(sprite),
465 [s.width, s.height],
466 );
467 }
468 }
469 }
470 ('t', _) => {
471 let width = doc.indent;
472 self.document_item(doc, &mut row, Content::Space, [width, 0]);
473 }
474 ('s', _) => {
475 let width = doc.fonts.get(row.font).copied().flatten().map_or(0, |f| {
476 TextLayout::compute(&self.res, f, " ", None, 0).width
477 });
478 self.document_item(doc, &mut row, Content::Space, [width, 0]);
479 }
480 ('d', _) => {
481 if let Some(capture) = doc.captures.get(&capture_name(&arg)).cloned() {
482 let size = capture.size;
483 self.document_item(doc, &mut row, Content::Capture(capture), size);
484 } else {
485 log::warn!("document capture {arg:?} was not found in its image collection");
486 }
487 }
488 _ => {}
489 }
490 }
491 }
492 }
493 if row.start < doc.items.len() {
494 self.document_end_row(doc, &mut row);
495 }
496 doc.size = [doc.width.max(1), row.y];
497 for item in &doc.items {
498 if let Some(target) = &item.link {
499 doc.links.push((
500 target.clone(),
501 [
502 item.pos[0],
503 item.pos[1],
504 item.pos[0] + item.size[0],
505 item.pos[1] + item.size[1],
506 ],
507 ));
508 }
509 }
510 }
511
512 fn document_item(&self, doc: &mut Document, row: &mut Row, content: Content, size: [i32; 2]) {
513 if row.start < doc.items.len() && row.x + size[0] > row.right {
514 self.document_end_row(doc, row);
515 }
516 let color = if row.link.is_some() {
517 doc.link_colors[0]
518 } else {
519 doc.colors.get(row.color).copied().unwrap_or(0xFFFF_FFFF)
520 };
521 doc.items.push(Item {
522 content,
523 pos: [row.x, row.y],
524 size,
525 color,
526 link: row.link.clone(),
527 });
528 row.x += size[0];
529 }
530
531 fn document_end_row(&self, doc: &mut Document, row: &mut Row) {
532 let default_height = doc
533 .fonts
534 .get(row.font)
535 .copied()
536 .flatten()
537 .map_or(0, |f| self.res.fonts[f].font.size);
538 let height = doc.items[row.start..]
539 .iter()
540 .map(|i| i.size[1])
541 .max()
542 .unwrap_or(default_height);
543 let width = row.x - row.left;
544 let shift = match row.align {
545 'r' => row.right - row.left - width,
546 'c' => (row.right - row.left - width) / 2,
547 _ => 0,
548 };
549 for item in &mut doc.items[row.start..] {
550 item.pos[0] += shift;
551 item.pos[1] = row.y
552 + match row.vertical {
553 't' => 0,
554 'c' => (height - item.size[1]) / 2,
555 _ => height - item.size[1],
556 };
557 }
558 row.y += height;
559 row.start = doc.items.len();
560 row.left += row.restore_left;
561 row.restore_left = 0;
562 row.x = row.left;
563 }
564
565 pub(crate) fn draw_document(
566 &self,
567 doc: &Document,
568 origin: [i32; 2],
569 color: u32,
570 clip: Option<[i32; 4]>,
571 out: &mut Vec<Quad>,
572 ) {
573 let hovered = doc
574 .hovered_link
575 .and_then(|i| doc.links.get(i))
576 .map(|(target, _)| target);
577 let pressed = doc
578 .pressed_link
579 .and_then(|i| doc.links.get(i))
580 .map(|(target, _)| target);
581 for item in &doc.items {
582 let pos = [origin[0] + item.pos[0], origin[1] + item.pos[1]];
583 let hot = item
584 .link
585 .as_ref()
586 .is_some_and(|target| Some(target) == hovered);
587 let ink = if hot {
588 doc.link_colors[if pressed == hovered { 2 } else { 1 }]
589 } else {
590 item.color
591 };
592 let tint = modulate(color, ink);
593 match &item.content {
594 Content::Text(text) => self.draw_text(text, pos, tint, clip, out),
595 Content::Sprite(sprite) => Self::sprite_quad(
596 out,
597 sprite,
598 [
599 pos[0],
600 pos[1],
601 pos[0] + sprite.width,
602 pos[1] + sprite.height,
603 ],
604 tint,
605 clip,
606 ),
607 Content::Capture(capture) => {
608 let mut capture_clip = clip;
609 for part in &capture.parts {
610 let rect = [
611 part.rect[0] + pos[0],
612 part.rect[1] + pos[1],
613 part.rect[2] + pos[0],
614 part.rect[3] + pos[1],
615 ];
616 if part.sheet == -1 {
617 capture_clip = intersect(clip, rect);
618 } else if part.sheet == -2 {
619 capture_clip = clip;
620 } else if let Some(sprite) = part.sprite {
621 Self::sprite_quad(
622 out,
623 &sprite,
624 rect,
625 modulate(color, part.color),
626 capture_clip,
627 );
628 }
629 }
630 }
631 Content::Space => {}
632 }
633 if hot {
634 if let Some(sprite) = doc.underline {
635 let y = pos[1] + item.size[1] - 1;
636 Self::sprite_quad(
637 out,
638 &sprite,
639 [pos[0], y, pos[0] + item.size[0], y + 1],
640 tint,
641 clip,
642 );
643 }
644 }
645 }
646 }
647}
648
649#[cfg(test)]
650mod tests;