1use std::{ops::Sub, str::Chars};
4
5use bevy::{color::LinearRgba, math::IVec2, reflect::Reflect};
6use sark_grids::{GridPoint, GridRect, GridSize, Pivot, PivotedPoint};
7
8#[derive(Default, Debug, Clone)]
20pub struct TerminalString<T> {
21 pub string: T,
22 pub decoration: StringDecoration,
23 pub formatting: StringFormatting,
24}
25
26impl<T: AsRef<str>> TerminalString<T> {
27 pub fn fg(mut self, color: impl Into<LinearRgba>) -> Self {
28 self.decoration.fg_color = Some(color.into());
29 self
30 }
31
32 pub fn bg(mut self, color: impl Into<LinearRgba>) -> Self {
33 self.decoration.bg_color = Some(color.into());
34 self
35 }
36
37 pub fn delimiters(mut self, delimiters: impl AsRef<str>) -> Self {
38 let mut chars = delimiters.as_ref().chars();
39 self.decoration.delimiters = (chars.next(), chars.next());
40 self
41 }
42
43 pub fn clear_colors(mut self) -> Self {
44 self.decoration.clear_colors = true;
45 self
46 }
47
48 pub fn ignore_spaces(mut self) -> Self {
49 self.formatting.ignore_spaces = true;
50 self
51 }
52
53 pub fn dont_word_wrap(mut self) -> Self {
54 self.formatting.word_wrap = false;
55 self
56 }
57}
58
59#[derive(Default, Debug, Clone, Copy, Reflect)]
61pub struct StringDecoration {
62 pub fg_color: Option<LinearRgba>,
65 pub bg_color: Option<LinearRgba>,
68 pub delimiters: (Option<char>, Option<char>),
70 pub clear_colors: bool,
74}
75
76#[derive(Default)]
78pub struct DecoratedString<T: AsRef<str>> {
79 pub string: T,
80 pub decoration: StringDecoration,
81}
82
83pub trait StringDecorator<T: AsRef<str>> {
85 fn fg(self, color: impl Into<LinearRgba>) -> DecoratedString<T>;
87 fn bg(self, color: impl Into<LinearRgba>) -> DecoratedString<T>;
89 fn delimiters(self, delimiters: impl AsRef<str>) -> DecoratedString<T>;
92 fn clear_colors(self) -> DecoratedString<T>;
95}
96
97impl<T: AsRef<str>> StringDecorator<T> for T {
98 fn fg(self, color: impl Into<LinearRgba>) -> DecoratedString<T> {
99 DecoratedString {
100 string: self,
101 decoration: StringDecoration {
102 fg_color: Some(color.into()),
103 ..Default::default()
104 },
105 }
106 }
107
108 fn bg(self, color: impl Into<LinearRgba>) -> DecoratedString<T> {
109 DecoratedString {
110 string: self,
111 decoration: StringDecoration {
112 bg_color: Some(color.into()),
113 ..Default::default()
114 },
115 }
116 }
117
118 fn clear_colors(self) -> DecoratedString<T> {
119 DecoratedString {
120 string: self,
121 decoration: StringDecoration {
122 clear_colors: true,
123 ..Default::default()
124 },
125 }
126 }
127
128 fn delimiters(self, delimiters: impl AsRef<str>) -> DecoratedString<T> {
129 let mut chars = delimiters.as_ref().chars();
130 DecoratedString {
131 string: self,
132 decoration: StringDecoration {
133 delimiters: (chars.next(), chars.next()),
134 ..Default::default()
135 },
136 }
137 }
138}
139
140impl<T: AsRef<str>> StringDecorator<T> for DecoratedString<T> {
141 fn fg(mut self, color: impl Into<LinearRgba>) -> DecoratedString<T> {
142 self.decoration.fg_color = Some(color.into());
143 self
144 }
145
146 fn bg(mut self, color: impl Into<LinearRgba>) -> DecoratedString<T> {
147 self.decoration.bg_color = Some(color.into());
148 self
149 }
150
151 fn clear_colors(mut self) -> DecoratedString<T> {
152 self.decoration.clear_colors = true;
153 self
154 }
155
156 fn delimiters(self, delimiters: impl AsRef<str>) -> DecoratedString<T> {
157 let mut chars = delimiters.as_ref().chars();
158 DecoratedString {
159 string: self.string,
160 decoration: StringDecoration {
161 delimiters: (chars.next(), chars.next()),
162 ..self.decoration
163 },
164 }
165 }
166}
167
168impl<T: AsRef<str>> DecoratedString<T> {
169 pub fn ignore_spaces(self) -> TerminalString<T> {
170 TerminalString {
171 string: self.string,
172 decoration: self.decoration,
173 formatting: StringFormatting {
174 ignore_spaces: true,
175 ..Default::default()
176 },
177 }
178 }
179}
180
181impl<T: AsRef<str>> From<T> for DecoratedString<T> {
182 fn from(value: T) -> Self {
183 DecoratedString {
184 string: value,
185 decoration: Default::default(),
186 }
187 }
188}
189
190#[derive(Debug, Clone, Reflect, Copy)]
192pub struct StringFormatting {
193 pub ignore_spaces: bool,
200 pub word_wrap: bool,
204}
205
206impl StringFormatting {
207 pub fn without_word_wrap() -> Self {
208 Self {
209 word_wrap: false,
210 ..Self::default()
211 }
212 }
213}
214
215impl Default for StringFormatting {
216 fn default() -> Self {
217 Self {
218 ignore_spaces: Default::default(),
219 word_wrap: true,
220 }
221 }
222}
223
224#[derive(Default)]
225pub struct FormattedString<T: AsRef<str>> {
226 pub string: T,
227 pub formatting: StringFormatting,
228}
229
230pub trait StringFormatter<T: AsRef<str>> {
231 fn ignore_spaces(self) -> FormattedString<T>;
232 fn dont_word_wrap(self) -> FormattedString<T>;
233}
234
235impl<T: AsRef<str>> StringFormatter<T> for T {
236 fn ignore_spaces(self) -> FormattedString<T> {
237 FormattedString {
238 string: self,
239 formatting: StringFormatting {
240 ignore_spaces: true,
241 ..Default::default()
242 },
243 }
244 }
245
246 fn dont_word_wrap(self) -> FormattedString<T> {
247 FormattedString {
248 string: self,
249 formatting: StringFormatting {
250 word_wrap: false,
251 ..Default::default()
252 },
253 }
254 }
255}
256
257impl<T: AsRef<str>> StringFormatter<T> for FormattedString<T> {
258 fn ignore_spaces(mut self) -> FormattedString<T> {
259 self.formatting.ignore_spaces = true;
260 self
261 }
262
263 fn dont_word_wrap(mut self) -> FormattedString<T> {
264 self.formatting.word_wrap = false;
265 self
266 }
267}
268
269impl<T: AsRef<str>> From<DecoratedString<T>> for TerminalString<T> {
270 fn from(value: DecoratedString<T>) -> Self {
271 TerminalString {
272 string: value.string,
273 decoration: value.decoration,
274 formatting: Default::default(),
275 }
276 }
277}
278
279impl<T: AsRef<str>> From<FormattedString<T>> for TerminalString<T> {
280 fn from(value: FormattedString<T>) -> Self {
281 TerminalString {
282 string: value.string,
283 formatting: value.formatting,
284 decoration: Default::default(),
285 }
286 }
287}
288
289impl<T: AsRef<str>> FormattedString<T> {
290 pub fn fg(self, color: impl Into<LinearRgba>) -> TerminalString<T> {
291 TerminalString {
292 string: self.string,
293 decoration: StringDecoration {
294 fg_color: Some(color.into()),
295 ..Default::default()
296 },
297 formatting: self.formatting,
298 }
299 }
300 pub fn bg(self, color: impl Into<LinearRgba>) -> TerminalString<T> {
301 TerminalString {
302 string: self.string,
303 decoration: StringDecoration {
304 bg_color: Some(color.into()),
305 ..Default::default()
306 },
307 formatting: self.formatting,
308 }
309 }
310
311 pub fn delimiters(self, delimiters: impl AsRef<str>) -> TerminalString<T> {
312 let mut chars = delimiters.as_ref().chars();
313 TerminalString {
314 string: self.string,
315 decoration: StringDecoration {
316 delimiters: (chars.next(), chars.next()),
317 ..Default::default()
318 },
319 formatting: self.formatting,
320 }
321 }
322
323 }
334
335impl<T: AsRef<str> + Default> From<T> for TerminalString<T> {
336 fn from(value: T) -> Self {
337 Self {
338 string: value,
339 ..Default::default()
340 }
341 }
342}
343
344fn line_count(mut input: &str, max_len: usize, wrap: bool) -> usize {
347 let mut line_count = 0;
348 while let Some((_, rem)) = wrap_string(input, max_len, wrap) {
349 line_count += 1;
350 input = rem;
351 }
352 line_count
353}
354
355fn hor_pivot_offset(pivot: Pivot, line_len: usize) -> i32 {
358 match pivot {
359 Pivot::TopLeft | Pivot::LeftCenter | Pivot::BottomLeft => 0,
360 _ => -(line_len.saturating_sub(1) as f32 * pivot.normalized().x).round() as i32,
361 }
362}
363
364fn ver_pivot_offset(string: &str, pivot: Pivot, max_width: usize, wrap: bool) -> i32 {
366 match pivot {
367 Pivot::TopLeft | Pivot::TopCenter | Pivot::TopRight => 0,
368 _ => {
369 let line_count = line_count(string, max_width, wrap);
370 (line_count.saturating_sub(1) as f32 * (1.0 - pivot.normalized().y)).round() as i32
371 }
372 }
373}
374
375fn wrap_string(string: &str, max_len: usize, word_wrap: bool) -> Option<(&str, &str)> {
380 debug_assert!(
381 max_len > 0,
382 "max_len for wrap_string must be greater than 0"
383 );
384 if string.trim_end().is_empty() {
385 return None;
386 }
387
388 if let Some(newline_index) = string
390 .char_indices()
392 .take(max_len)
393 .find(|(_, c)| *c == '\n')
394 .map(|(i, _)| i)
395 {
396 let (a, b) = string.split_at(newline_index);
397 return Some((a.trim_end(), b.trim_start()));
398 };
399
400 let len = string.chars().count();
401 if len <= max_len {
402 return Some((string.trim_end(), ""));
403 };
404
405 let mut move_back = if word_wrap {
406 string
407 .chars()
408 .rev()
409 .skip(len - max_len - 1)
410 .position(|c| c.is_whitespace())
411 .unwrap_or_default()
412 } else {
413 0
414 };
415 while !string.is_char_boundary(max_len.sub(move_back)) {
416 move_back += 1;
417 }
418
419 let (a, b) = string.split_at(max_len.sub(move_back));
420 Some((a.trim_end(), b.trim_start()))
421}
422
423pub struct StringIter<'a> {
430 remaining: &'a str,
431 rect: GridRect,
432 xy: IVec2,
433 pivot: Pivot,
434 current: Chars<'a>,
435 formatting: StringFormatting,
436 decoration: StringDecoration,
437}
438
439impl<'a> StringIter<'a> {
440 pub fn new(
441 string: &'a str,
442 rect: GridRect,
443 local_xy: impl Into<PivotedPoint>,
444 formatting: Option<StringFormatting>,
445 decoration: Option<StringDecoration>,
446 ) -> Self {
447 let pivoted_point: PivotedPoint = local_xy.into().with_default_pivot(Pivot::TopLeft);
448 let pivot = pivoted_point.pivot.unwrap();
449 let local_xy = pivoted_point.point;
450
451 let formatting = formatting.unwrap_or_default();
452 let decoration = decoration.unwrap_or_default();
453
454 debug_assert!(
455 rect.size
456 .contains_point(local_xy.pivot(pivot).calculate(rect.size)),
457 "Local position {} passed to StringIter must be within the bounds of the given rect size {}",
458 local_xy,
459 rect.size
460 );
461
462 let first_max_len = rect
463 .width()
464 .saturating_sub(local_xy.x.unsigned_abs() as usize);
465 let (first, remaining) =
466 wrap_string(string, first_max_len, formatting.word_wrap).unwrap_or_default();
467
468 let horizontal_offset = hor_pivot_offset(pivot, first.len());
469 let vertical_offset = ver_pivot_offset(string, pivot, rect.width(), formatting.word_wrap);
470
471 let mut xy = rect.pivoted_point(pivoted_point);
472
473 xy.x += horizontal_offset;
474 xy.y += vertical_offset;
475
476 Self {
477 remaining,
478 rect,
479 xy,
480 pivot,
481 current: first.chars(),
482 formatting,
483 decoration,
484 }
485 }
486
487 fn line_feed(&mut self, line_len: usize) {
488 let x = self.rect.pivot_point(self.pivot).x;
489 let hor_offset = hor_pivot_offset(self.pivot, line_len);
490 self.xy.x = x + hor_offset;
491 self.xy.y -= 1;
492 }
493}
494
495impl Iterator for StringIter<'_> {
496 type Item = (IVec2, (char, Option<LinearRgba>, Option<LinearRgba>));
497
498 fn next(&mut self) -> Option<Self::Item> {
499 let ch = self
500 .decoration
501 .delimiters
502 .0
503 .take()
504 .or_else(|| self.current.next())
505 .or_else(|| {
506 let (next_line, remaining) =
507 wrap_string(self.remaining, self.rect.width(), self.formatting.word_wrap)?;
508
509 self.line_feed(next_line.len());
510 if self.xy.y < 0 {
511 return None;
512 }
513 self.remaining = remaining;
514 self.current = next_line.chars();
515 self.current.next()
516 })
517 .or_else(|| self.decoration.delimiters.1.take())?;
518 let p = self.xy;
519 self.xy.x += 1;
520 if ch == ' ' && self.formatting.ignore_spaces {
521 return self.next();
522 }
523 let fg = self.decoration.fg_color;
524 let bg = self.decoration.bg_color;
525 Some((p, (ch, fg, bg)))
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use bevy_platform::collections::HashMap;
532
533 use crate::{GridPoint, GridRect, ascii};
534
535 use super::*;
536
537 fn make_map(string: StringIter<'_>) -> HashMap<[i32; 2], char> {
539 string.map(|(p, (ch, _, _))| (p.to_array(), ch)).collect()
540 }
541
542 fn get_char(map: &HashMap<[i32; 2], char>, xy: [i32; 2]) -> char {
543 *map.get(&xy).unwrap_or(&' ')
544 }
545
546 fn read_string(map: &HashMap<[i32; 2], char>, xy: [i32; 2], len: usize) -> String {
547 (0..len)
548 .map(|i| get_char(map, [xy[0] + i as i32, xy[1]]))
549 .collect()
550 }
551
552 #[test]
553 fn word_wrap() {
554 let rem = "Use wasd to resize terminal";
555 let (split, rem) = wrap_string(rem, 8, true).unwrap();
556 assert_eq!("Use wasd", split);
557 assert_eq!("to resize terminal", rem);
558 let (split, rem) = wrap_string(rem, 8, true).unwrap();
559 assert_eq!("to", split);
560 assert_eq!("resize terminal", rem);
561 let (split, rem) = wrap_string(rem, 8, true).unwrap();
562 assert_eq!("resize", split);
563 assert_eq!("terminal", rem);
564 let (split, rem) = wrap_string(rem, 8, true).unwrap();
565 assert_eq!("terminal", split);
566 assert_eq!("", rem);
567 }
568
569 #[test]
570 fn iter_newline() {
571 let area = GridRect::new([0, 0], [40, 40]);
572 let iter = StringIter::new(
573 "A simple string\nWith a newline",
574 area,
575 [0, 0],
576 Some(StringFormatting {
577 word_wrap: true,
578 ..Default::default()
579 }),
580 None,
581 );
582 let map = make_map(iter);
583 assert_eq!('g', get_char(&map, [14, 39]));
584 assert_eq!('W', get_char(&map, [0, 38]))
585 }
586
587 #[test]
588 fn newline_line_wrap() {
589 let (split, remaining) = wrap_string("A simple string\nWith a newline", 12, false).unwrap();
590 assert_eq!("A simple str", split);
591 assert_eq!("ing\nWith a newline", remaining);
592 let (split, remaining) = wrap_string(remaining, 12, false).unwrap();
593 assert_eq!("ing", split);
594 assert_eq!("With a newline", remaining);
595 let (split, remaining) = wrap_string(remaining, 12, false).unwrap();
596 assert_eq!("With a newli", split);
597 assert_eq!("ne", remaining);
598 let (split, remaining) = wrap_string(remaining, 12, false).unwrap();
599 assert_eq!("ne", split);
600 assert_eq!("", remaining);
601 }
602
603 #[test]
604 fn newline_word_wrap() {
605 let (wrapped, remaining) =
606 wrap_string("A simple string\nWith a newline", 12, true).unwrap();
607 assert_eq!("A simple", wrapped);
608 assert_eq!("string\nWith a newline", remaining);
609 let (wrapped, remaining) = wrap_string(remaining, 12, true).unwrap();
610 assert_eq!("string", wrapped);
611 assert_eq!("With a newline", remaining);
612 let (wrapped, remaining) = wrap_string(remaining, 12, true).unwrap();
613 assert_eq!("With a", wrapped);
614 assert_eq!("newline", remaining);
615 let (wrapped, remaining) = wrap_string(remaining, 12, true).unwrap();
616 assert_eq!("newline", wrapped);
617 assert_eq!("", remaining);
618 }
619
620 #[test]
621 fn iter_no_word_wrap() {
622 let area = GridRect::new([0, 0], [12, 20]);
623 let iter = StringIter::new(
624 "A simple string\nWith a newline",
625 area,
626 [0, 0],
627 Some(StringFormatting::without_word_wrap()),
628 None,
629 );
630 let map = make_map(iter);
631 assert_eq!("A simple str", read_string(&map, [0, 19], 12));
632 assert_eq!("ing", read_string(&map, [0, 18], 3));
633 assert_eq!("With a newli", read_string(&map, [0, 17], 12));
634 assert_eq!("ne", read_string(&map, [0, 16], 2));
635 }
636
637 #[test]
638 fn iter_word_wrap() {
639 let area = GridRect::new([0, 0], [12, 20]);
640 let iter = StringIter::new(
641 "A simple string\nWith a newline",
642 area,
643 [0, 0],
644 Some(StringFormatting {
645 word_wrap: true,
646 ..Default::default()
647 }),
648 None,
649 );
650 let map = make_map(iter);
651 assert_eq!("A simple", read_string(&map, [0, 19], 8));
652 assert_eq!("string", read_string(&map, [0, 18], 6));
653 assert_eq!("With a", read_string(&map, [0, 17], 6));
654 assert_eq!("newline", read_string(&map, [0, 16], 7));
655 }
656
657 #[test]
658 fn wrap_line_count() {
659 let string = "A somewhat longer line\nWith a newline or two\nOkay? WHEEEEEE.";
660 assert_eq!(7, line_count(string, 12, true));
661 assert_eq!(6, line_count(string, 12, false));
662 }
663
664 #[test]
665 fn y_offset_wrap() {
666 let string = "A somewhat longer line\nWith a newline or two\nOkay? WHEEEEEE.";
667 let line_len = 12;
668 let wrap = true;
669 let offset = ver_pivot_offset(string, Pivot::TopLeft, line_len, wrap);
670 assert_eq!(0, offset);
671 assert_eq!(7, line_count(string, 12, wrap));
672 assert_eq!(6, ver_pivot_offset(string, Pivot::BottomLeft, 12, wrap));
673 }
674
675 #[test]
676 fn y_offset_no_wrap() {
677 let string = "A somewhat longer line\nWith a newline or two\nOkay? WHEEEEEE.";
678 let line_len = 12;
679 let wrap = false;
680 let offset = ver_pivot_offset(string, Pivot::TopLeft, line_len, wrap);
681 assert_eq!(0, offset);
682 let offset = ver_pivot_offset(string, Pivot::BottomLeft, 12, wrap);
683 assert_eq!(6, line_count(string, 12, false));
684 assert_eq!(5, offset);
685 }
686
687 #[test]
688 fn right_pivot() {
689 let string = "A somewhat longer line\nWith a newline";
690 let area = GridRect::new([0, 0], [12, 20]);
691 let iter = StringIter::new(
692 string,
693 area,
694 [0, 0].pivot(Pivot::TopRight),
695 Some(StringFormatting {
696 word_wrap: true,
697 ..Default::default()
698 }),
699 None,
700 );
701 let map = make_map(iter);
702 let assert_string_location = |string: &str, xy: [i32; 2]| {
703 assert_eq!(string, read_string(&map, xy, string.len()));
704 };
705 assert_string_location("A somewhat", [2, 19]);
706 assert_string_location("longer line", [1, 18]);
707 assert_string_location("With a", [6, 17]);
708 assert_string_location("newline", [5, 16]);
709 }
710
711 #[test]
712 fn delimiters() {
713 let string = "A simple string";
714 let area = GridRect::new([0, 0], [20, 5]);
715 let iter = StringIter::new(
716 string,
717 area,
718 [0, 0],
719 None,
720 Some(StringDecoration {
721 delimiters: (Some('['), Some(']')),
722 ..Default::default()
723 }),
724 );
725 let map = make_map(iter);
726 assert_eq!("[A simple string]", read_string(&map, [0, 4], 17));
727 }
728
729 #[test]
730 fn one_wide() {
731 let string = "Abcdefg";
732 let area = GridRect::new([0, 0], [1, 7]);
733 let iter = StringIter::new(string, area, [0, 0], None, None);
734 let map = make_map(iter);
735 assert_eq!('A', get_char(&map, [0, 6]));
736 assert_eq!('b', get_char(&map, [0, 5]));
737 assert_eq!('c', get_char(&map, [0, 4]));
738 assert_eq!('d', get_char(&map, [0, 3]));
739 assert_eq!('e', get_char(&map, [0, 2]));
740 assert_eq!('f', get_char(&map, [0, 1]));
741 assert_eq!('g', get_char(&map, [0, 0]));
742 }
743
744 #[test]
745 fn leftbot() {
746 let string = "LeftBot";
747 let p = [0, 0].pivot(Pivot::BottomLeft);
748 let rect = GridRect::new([-1, 6], [1, 40]);
749 let iter = StringIter::new(string, rect, p, None, None);
750 let map = make_map(iter);
751 assert_eq!('L', get_char(&map, [-1, 12]));
752 assert_eq!('e', get_char(&map, [-1, 11]));
753 assert_eq!('f', get_char(&map, [-1, 10]));
754 assert_eq!('t', get_char(&map, [-1, 9]));
755 assert_eq!('B', get_char(&map, [-1, 8]));
756 assert_eq!('o', get_char(&map, [-1, 7]));
757 assert_eq!('t', get_char(&map, [-1, 6]));
758 }
759
760 #[test]
761 fn centered() {
762 let string = "Hello\nThere";
763 let p = [0, 0].pivot(Pivot::Center);
764 let rect = GridRect::new([0, 0], [11, 11]);
765 let iter = StringIter::new(string, rect, p, None, None);
766 let map = make_map(iter);
767 assert_eq!('H', get_char(&map, [3, 6]));
768 assert_eq!('e', get_char(&map, [4, 6]));
769 assert_eq!('l', get_char(&map, [5, 6]));
770 assert_eq!('l', get_char(&map, [6, 6]));
771 assert_eq!('o', get_char(&map, [7, 6]));
772 }
773
774 #[test]
775 fn wrap_after_unicode() {
776 let mut string = String::with_capacity(ascii::CP_437_ARRAY.len() * 2);
777 for ch in ascii::CP_437_ARRAY.iter() {
778 string.push(*ch);
779 string.push('\n');
780 }
781 let iter = StringIter::new(
782 &string,
783 GridRect::new([0, 0], [10, 500]),
784 [0, 0],
785 None,
786 None,
787 );
788 iter.count();
789 }
790}