bevy_ascii_terminal/
string.rs

1//! Utilities  for writing formatted/decorated strings to the terminal
2//! without any extra allocations.
3use std::{ops::Sub, str::Chars};
4
5use bevy::{color::LinearRgba, math::IVec2, reflect::Reflect};
6use sark_grids::{GridPoint, GridRect, GridSize, Pivot, PivotedPoint};
7
8/// A string with optional [StringDecoration] and [StringFormatting] applied.
9///
10/// `dont_word_wrap` Can be used to disable word wrapping, which is enabled by
11/// default for terminal strings.
12///
13/// `clear_colors` can be used to set the fg and bg colors of the string
14/// tiles to match the terminal's clear tile.
15///
16/// The `bg` and `fg` methods can be used to set the background and foreground
17/// colors of the string tiles if `clear_colors`` isn't set. Otherwise the existing
18/// colors in the terminal will remain unchanged.
19#[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/// Optional decoration to be applied to a string being written to a terminal.
60#[derive(Default, Debug, Clone, Copy, Reflect)]
61pub struct StringDecoration {
62    /// An optional foreground color for the string. If set to None then the
63    /// terminal's clear tile color will be used.
64    pub fg_color: Option<LinearRgba>,
65    /// An optional background color for the string. If set to None then the
66    /// terminal's clear tile color will be used.
67    pub bg_color: Option<LinearRgba>,
68    /// An optional pair of delimiters to be placed around the string.
69    pub delimiters: (Option<char>, Option<char>),
70    /// If true, then the terminal's clear tile colors will be used for the
71    /// string. If false then the fg and bg colors will be used if they are set.
72    /// Otherwise the existing colors in the terminal will remain unchanged.
73    pub clear_colors: bool,
74}
75
76/// A string with optional [StringDecoration].
77#[derive(Default)]
78pub struct DecoratedString<T: AsRef<str>> {
79    pub string: T,
80    pub decoration: StringDecoration,
81}
82
83/// A trait for creating a [DecoratedString].
84pub trait StringDecorator<T: AsRef<str>> {
85    /// Sets the foreground color for string tiles.
86    fn fg(self, color: impl Into<LinearRgba>) -> DecoratedString<T>;
87    /// Sets the background color for string tiles.
88    fn bg(self, color: impl Into<LinearRgba>) -> DecoratedString<T>;
89    /// Add a pair of delimiters to the string. The first character will be the
90    /// opening delimiter and the second character will be the closing delimiter.
91    fn delimiters(self, delimiters: impl AsRef<str>) -> DecoratedString<T>;
92    /// Sets the string tile colors to match the terminal's clear tile. This will
93    /// override the string's fg and bg colors.
94    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/// Optional formatting to be applied to a string being written to a terminal.
191#[derive(Debug, Clone, Reflect, Copy)]
192pub struct StringFormatting {
193    /// Defines whether or not 'empty' (" ") tiles will be modified when writing
194    /// strings to the terminal. If set to false then decorations will be
195    /// applied even to empty tiles.
196    ///
197    /// Defaults to false.
198    // TODO: move to decoration?
199    pub ignore_spaces: bool,
200    /// Word wrap prevents words from being split across lines.
201    ///
202    /// Defaults to true.
203    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    // pub fn clear_colors(self) -> TerminalString<T> {
324    //     TerminalString {
325    //         string: self.string,
326    //         decoration: StringDecoration {
327    //             clear_colors: true,
328    //             ..Default::default()
329    //         },
330    //         formatting: self.formatting,
331    //     }
332    // }
333}
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
344/// Precalculate the number of vertical lines a wrapped string will occupy.
345// TODO: Integrate with `wrap_string` to avoid the duplicate work
346fn 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
355/// Calculate the number of tiles to offset a string by horizontally based
356/// on it's pivot.
357fn 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
364/// Calculate the amount of lines to offset a wrapped string by based on a pivot
365fn 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
375/// Wrap a string to fit within a given line length. It will first try to split
376/// at the first newline before max_len, then if word_wrap is true, it will
377/// split at the last whitespace character before max_len, otherwise the string
378/// will be split at max_len.
379fn 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    // Handle newlines first
389    if let Some(newline_index) = string
390        // Accounts for unicode chars
391        .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
423/// An iterator for writing wrapped strings to a rectangular grid. Will attempt
424/// to respect formatting and the size of the given area while yielding
425/// each string character and grid position.
426///
427/// The iterator will always wrap at newlines and will strip leading and trailing
428/// whitespace past the first line.
429pub 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    /// Map each character in the string to it's grid position
538    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}