glyph_brush_layout/
linebreak.rs

1use std::{
2    fmt,
3    hash::Hash,
4    iter::FusedIterator,
5    str::{self, CharIndices},
6};
7
8/// Indicator that a character is a line break, soft or hard. Includes the offset (byte-index)
9/// position.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum LineBreak {
12    /// Soft line break (offset).
13    Soft(usize),
14    /// Hard line break (offset).
15    Hard(usize),
16}
17
18impl LineBreak {
19    /// Returns the offset of the line break, the index after the breaking character.
20    #[inline]
21    pub fn offset(&self) -> usize {
22        match *self {
23            LineBreak::Soft(offset) | LineBreak::Hard(offset) => offset,
24        }
25    }
26}
27
28/// Producer of a [`LineBreak`](enum.LineBreak.html) iterator. Used to allow to the
29/// [`Layout`](enum.Layout.html) to be line break aware in a generic way.
30pub trait LineBreaker: fmt::Debug + Copy + Hash {
31    fn line_breaks<'a>(&self, glyph_info: &'a str) -> Box<dyn Iterator<Item = LineBreak> + 'a>;
32}
33
34/// Built-in linebreaking logic.
35#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
36pub enum BuiltInLineBreaker {
37    /// LineBreaker that follows Unicode Standard Annex #14. That effectively means it
38    /// wraps words in a way that should work for most cases.
39    #[default]
40    UnicodeLineBreaker,
41    /// LineBreaker that soft breaks on any character, and hard breaks similarly to
42    /// UnicodeLineBreaker.
43    AnyCharLineBreaker,
44}
45
46// Iterator that indicates all characters are soft line breaks, except hard ones which are hard.
47struct AnyCharLineBreakerIter<'a> {
48    chars: CharIndices<'a>,
49    breaks: xi_unicode::LineBreakIterator<'a>,
50    current_break: Option<(usize, bool)>,
51}
52
53impl Iterator for AnyCharLineBreakerIter<'_> {
54    type Item = LineBreak;
55
56    #[inline]
57    fn next(&mut self) -> Option<LineBreak> {
58        let (b_index, c) = self.chars.next()?;
59        let c_len = c.len_utf8();
60        while self.current_break.is_some() {
61            if self.current_break.as_ref().unwrap().0 < b_index + c_len {
62                self.current_break = self.breaks.next();
63            } else {
64                break;
65            }
66        }
67        if let Some((break_index, true)) = self.current_break {
68            if break_index == b_index + c_len {
69                return Some(LineBreak::Hard(break_index));
70            }
71        }
72        Some(LineBreak::Soft(b_index + c_len))
73    }
74}
75
76impl FusedIterator for AnyCharLineBreakerIter<'_> {}
77
78impl LineBreaker for BuiltInLineBreaker {
79    #[inline]
80    fn line_breaks<'a>(&self, text: &'a str) -> Box<dyn Iterator<Item = LineBreak> + 'a> {
81        match *self {
82            BuiltInLineBreaker::UnicodeLineBreaker => Box::new(
83                xi_unicode::LineBreakIterator::new(text).map(|(offset, hard)| {
84                    if hard {
85                        LineBreak::Hard(offset)
86                    } else {
87                        LineBreak::Soft(offset)
88                    }
89                }),
90            ),
91            BuiltInLineBreaker::AnyCharLineBreaker => {
92                let mut unicode_breaker = xi_unicode::LineBreakIterator::new(text);
93                let current_break = unicode_breaker.next();
94
95                Box::new(AnyCharLineBreakerIter {
96                    chars: text.char_indices(),
97                    breaks: unicode_breaker,
98                    current_break,
99                })
100            }
101        }
102    }
103}
104
105/// Line breakers can't easily tell the difference between the end of a slice being a hard
106/// break and the last character being itself a hard or soft break. This trait allows testing
107/// of eol characters being "true" eol line breakers.
108pub(crate) trait EolLineBreak<B: LineBreaker> {
109    fn eol_line_break(&self, line_breaker: &B) -> Option<LineBreak>;
110}
111
112impl<B: LineBreaker> EolLineBreak<B> for char {
113    #[inline]
114    fn eol_line_break(&self, line_breaker: &B) -> Option<LineBreak> {
115        // to check if the previous end char (say '$') should hard break construct
116        // a str "$ " an check if the line break logic flags a hard break at index 1
117        let mut last_end_bytes: [u8; 5] = [b' '; 5];
118        self.encode_utf8(&mut last_end_bytes);
119        let len_utf8 = self.len_utf8();
120        if let Ok(last_end_padded) = str::from_utf8(&last_end_bytes[0..=len_utf8]) {
121            match line_breaker.line_breaks(last_end_padded).next() {
122                l @ Some(LineBreak::Soft(1)) | l @ Some(LineBreak::Hard(1)) => return l,
123                _ => {}
124            }
125        }
126
127        // check for soft breaks using str "$a"
128        last_end_bytes[len_utf8] = b'a';
129        if let Ok(last_end_padded) = str::from_utf8(&last_end_bytes[0..=len_utf8]) {
130            match line_breaker.line_breaks(last_end_padded).next() {
131                l @ Some(LineBreak::Soft(1)) | l @ Some(LineBreak::Hard(1)) => return l,
132                _ => {}
133            }
134        }
135
136        None
137    }
138}
139
140#[cfg(test)]
141mod eol_line_break {
142    use super::*;
143
144    #[test]
145    fn hard_break_char() {
146        assert_eq!(
147            '\n'.eol_line_break(&BuiltInLineBreaker::default()),
148            Some(LineBreak::Hard(1))
149        );
150    }
151
152    #[test]
153    fn soft_break_char() {
154        assert_eq!(
155            ' '.eol_line_break(&BuiltInLineBreaker::default()),
156            Some(LineBreak::Soft(1))
157        );
158    }
159}