Skip to main content

anathema_widgets/layout/
text.rs

1use std::ops::{AddAssign, Deref};
2
3use anathema_geometry::Size;
4use anathema_store::tree::ValueId;
5use anathema_value_resolver::ValueKind;
6use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
7
8use crate::WidgetId;
9
10/// Word wrapping strategy
11#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
12pub enum Wrap {
13    /// Normal word wrapping. This will break text on hyphen and whitespace.
14    /// Trailing whitespace is consumed if it would cause a line break.
15    #[default]
16    Normal,
17    /// Insert a newline in the middle of any text
18    WordBreak,
19}
20
21impl Wrap {
22    /// Returns true if word wrapping is enabled (self == Self::Normal)
23    pub fn is_word_wrap(&self) -> bool {
24        matches!(self, Self::Normal)
25    }
26}
27
28impl TryFrom<&ValueKind<'_>> for Wrap {
29    type Error = ();
30
31    fn try_from(value: &ValueKind<'_>) -> Result<Self, Self::Error> {
32        let s = value.as_str().ok_or(())?;
33        match s {
34            "normal" => Ok(Wrap::Normal),
35            "break" => Ok(Wrap::WordBreak),
36            _ => Err(()),
37        }
38    }
39}
40
41impl From<Wrap> for ValueKind<'_> {
42    fn from(value: Wrap) -> Self {
43        let value = match value {
44            Wrap::Normal => "normal",
45            Wrap::WordBreak => "break",
46        };
47
48        ValueKind::Str(value.into())
49    }
50}
51
52#[derive(Debug)]
53pub(crate) struct LineWidth(usize);
54
55impl LineWidth {
56    pub(crate) const ZERO: Self = Self(0);
57
58    // update the current value and return the old value
59    pub(crate) fn swap(&mut self, mut new_value: usize) -> u16 {
60        std::mem::swap(&mut self.0, &mut new_value);
61        new_value as u16
62    }
63}
64
65impl Default for LineWidth {
66    fn default() -> Self {
67        LineWidth::ZERO
68    }
69}
70
71impl Deref for LineWidth {
72    type Target = usize;
73
74    fn deref(&self) -> &Self::Target {
75        &self.0
76    }
77}
78
79impl AddAssign<usize> for LineWidth {
80    fn add_assign(&mut self, rhs: usize) {
81        self.0 += rhs;
82    }
83}
84
85/// The process result dictates whether it's possible to
86/// fit more text or not.
87#[derive(Debug, Copy, Clone, PartialEq)]
88pub enum ProcessResult {
89    /// Continue means it's possible to process more text
90    Continue,
91    /// Break means that there is no more room for text and
92    /// further processing should be avoided
93    Break,
94}
95
96#[derive(Debug, Copy, Clone)]
97pub(crate) enum Entry {
98    Newline,
99    LineWidth(u16),
100    Style(ValueId),
101}
102
103/// Represents a line containing the width and the segments
104#[derive(Debug)]
105pub struct Line<I> {
106    pub width: u16,
107    pub entries: I,
108}
109
110/// A line segment.
111#[derive(Debug)]
112pub enum Segment<'a> {
113    /// Set the style
114    SetStyle(ValueId),
115    /// String slice
116    Str(&'a str),
117}
118
119#[derive(Debug, Copy, Clone, PartialEq)]
120pub(crate) enum LineEntry {
121    Width(u16),
122    Str(u32, u32),
123    SetStyle(ValueId),
124    Newline,
125}
126
127/// Perform text layout
128/// ```
129/// # use anathema_widgets::layout::text::*;
130/// # use anathema_geometry::Size;
131///
132/// let mut text = Strings::new(Size::new(10, 10), Wrap::Normal);
133/// text.add_str("he");
134/// text.add_str("ll");
135/// text.add_str("o world");
136/// let size = text.finish();
137///
138/// let lines = text.lines();
139/// ```
140#[derive(Debug)]
141pub struct Strings {
142    layout: Vec<(u32, Entry)>,
143    lines: Vec<LineEntry>,
144    chomper: Chomper,
145    frozen: bool,
146    bytes: Vec<u8>,
147    max: Size,
148    size: Size,
149    wrap: Wrap,
150    // Byte index where the current line starts
151    line: usize,
152    current_width: LineWidth,
153}
154
155impl Strings {
156    pub fn new(max: Size, wrap: Wrap) -> Self {
157        Self {
158            layout: vec![],
159            lines: vec![],
160            chomper: Chomper::Continuous(0),
161            frozen: false,
162            bytes: vec![],
163            max,
164            wrap,
165            size: Size::new(0, 1),
166            line: 0,
167            current_width: LineWidth::ZERO,
168        }
169    }
170
171    /// Layout another string slice.
172    pub fn add_str(&mut self, s: &str) -> ProcessResult {
173        if self.max.height == 0 || self.max.width == 0 {
174            return ProcessResult::Break;
175        }
176
177        if self.frozen {
178            return ProcessResult::Break;
179        }
180
181        for word in s.split_inclusive(char::is_whitespace) {
182            self.bytes.extend(word.bytes());
183            for c in word.chars() {
184                if let res @ ProcessResult::Break = self.chomp(c) {
185                    self.bytes.truncate(self.chomper.index());
186                    self.freeze();
187                    return res;
188                }
189            }
190        }
191
192        ProcessResult::Continue
193    }
194
195    /// Access the laid out strings.
196    /// See [`Strings`] for example.
197    pub fn lines(&self) -> impl Iterator<Item = Line<impl Iterator<Item = Segment<'_>>>> {
198        let lines = self.lines.split(|e| *e == LineEntry::Newline);
199
200        lines.map(|entries| {
201            let LineEntry::Width(width) = entries[0] else { unreachable!() };
202
203            Line {
204                width,
205                entries: entries[1..].iter().map(|e| match e {
206                    LineEntry::Str(from, to) => Segment::Str(
207                        std::str::from_utf8(&self.bytes[*from as usize..*to as usize])
208                            .expect("only strings written to the byte store"),
209                    ),
210                    LineEntry::SetStyle(style) => Segment::SetStyle(*style),
211                    LineEntry::Width(_) | LineEntry::Newline => unreachable!("consumed already"),
212                }),
213            }
214        })
215    }
216
217    pub fn set_style(&mut self, style: WidgetId) {
218        let index = self.bytes.len();
219        self.layout.push((index as u32, Entry::Style(style)));
220    }
221
222    /// Finalize the layout, converting entries to lines
223    pub fn finish(&mut self) -> Size {
224        self.frozen = true;
225        self.layout.sort_by(|a, b| a.0.cmp(&b.0));
226
227        let last_line = self.line(self.bytes.len());
228        let last_line_width = last_line.width();
229        self.layout
230            .push((self.bytes.len() as u32, Entry::LineWidth(last_line_width as u16)));
231
232        // Write the entries as lines
233        let mut from = 0;
234        for line in self.layout.split(|e| matches!(e.1, Entry::Newline)) {
235            // Find the line width (always the last entry)
236            let width = match line.last() {
237                Some((_, Entry::LineWidth(w))) => *w,
238                _ => unreachable!("the last entry is always the line width"),
239            };
240
241            self.lines.push(LineEntry::Width(width));
242
243            for (i, entry) in line {
244                // Don't bother adding a string entry for an empty string
245                if from != *i {
246                    self.lines.push(LineEntry::Str(from, *i));
247                }
248
249                from = *i;
250
251                match entry {
252                    Entry::Style(style) => self.lines.push(LineEntry::SetStyle(*style)),
253                    Entry::LineWidth(_) => {}
254                    Entry::Newline => unreachable!("consumed by the split"),
255                }
256            }
257            self.lines.push(LineEntry::Newline);
258        }
259
260        self.lines.pop();
261
262        self.update_width();
263        if self.size.width == 0 {
264            self.size = Size::ZERO;
265        }
266        self.size
267    }
268
269    fn freeze(&mut self) {
270        self.frozen = true;
271    }
272
273    fn newline(&mut self) {
274        self.size.height += 1;
275        self.update_width();
276        self.line = match self.chomper {
277            Chomper::Continuous(idx) => {
278                self.layout
279                    .push((idx as u32, Entry::LineWidth(self.current_width.swap(0))));
280                self.layout.push((idx as u32, Entry::Newline));
281                idx
282            }
283            Chomper::WordBoundary {
284                word_boundary,
285                current_index,
286            } => {
287                let diff = self.line(current_index).width() - self.line(word_boundary).width();
288                let width = *self.current_width - diff;
289                self.layout.push((word_boundary as u32, Entry::LineWidth(width as u16)));
290                self.layout.push((word_boundary as u32, Entry::Newline));
291                let _ = self.current_width.swap(diff);
292                self.chomper = Chomper::Continuous(current_index);
293                word_boundary
294            }
295        };
296    }
297
298    fn line(&self, index: usize) -> &str {
299        self.str(self.line, index)
300    }
301
302    fn str(&self, offset: usize, index: usize) -> &str {
303        std::str::from_utf8(&self.bytes[offset..index]).expect("only valid strings here")
304    }
305
306    fn update_width(&mut self) {
307        self.size.width = self.size.width.max(*self.current_width as u16);
308    }
309
310    fn chomp(&mut self, c: char) -> ProcessResult {
311        let width = c.width().unwrap_or(0) as u16;
312
313        // NOTE
314        // Special case: the character is too wide to ever fit so it's removed,
315        // e.g a character width of two with a max width of one.
316        if width > self.max.width {
317            for _ in 0..c.len_utf8() {
318                self.bytes.pop();
319            }
320            return ProcessResult::Continue;
321        }
322
323        // NOTE
324        // If newline characters are handled then pop the bytes and insert a newline
325        if c == '\n' {
326            self.bytes.pop();
327
328            if self.size.height >= self.max.height {
329                return ProcessResult::Break;
330            }
331
332            self.chomper.force_word_boundary();
333            self.newline();
334            return ProcessResult::Continue;
335        }
336
337        // NOTE
338        // If the trailing whitespace should be removed, do so here
339        while width + *self.current_width as u16 > self.max.width {
340            if c.is_whitespace() {
341                // 1. Make this the next word boundary
342                // 2. Insert a newline here
343                // 3. Remove the bytes representing this whitespace
344
345                for _ in 0..c.len_utf8() {
346                    self.bytes.pop();
347                }
348
349                self.chomper.force_word_boundary();
350                self.newline();
351
352                return ProcessResult::Continue;
353            }
354
355            if self.size.height >= self.max.height {
356                return ProcessResult::Break;
357            }
358
359            self.newline();
360        }
361
362        self.chomper.chomp(c, self.wrap);
363        self.current_width += width as usize;
364
365        ProcessResult::Continue
366    }
367}
368
369impl Default for Strings {
370    fn default() -> Self {
371        Self::new(Size::ZERO, Wrap::default())
372    }
373}
374
375// TODO: move this into string2
376#[derive(Debug)]
377pub(crate) enum Chomper {
378    Continuous(usize),
379    WordBoundary { word_boundary: usize, current_index: usize },
380}
381
382impl Chomper {
383    pub(crate) fn index(&self) -> usize {
384        match self {
385            Chomper::Continuous(current_index) | Chomper::WordBoundary { current_index, .. } => *current_index,
386        }
387    }
388
389    pub(crate) fn force_word_boundary(&mut self) {
390        if let Chomper::WordBoundary {
391            word_boundary,
392            current_index,
393        } = self
394        {
395            *word_boundary = *current_index;
396        }
397    }
398
399    pub(crate) fn chomp(&mut self, c: char, wrap: Wrap) {
400        let c_len = c.len_utf8();
401
402        if c.is_whitespace() && wrap.is_word_wrap() {
403            match self {
404                Chomper::Continuous(idx) | Chomper::WordBoundary { current_index: idx, .. } => {
405                    let new_index = *idx + c_len;
406                    *self = Self::WordBoundary {
407                        word_boundary: new_index,
408                        current_index: new_index,
409                    };
410                    return;
411                }
412            }
413        }
414
415        match self {
416            Self::Continuous(idx) => *idx += c_len,
417            Self::WordBoundary { current_index, .. } => *current_index += c_len,
418        }
419    }
420}
421
422impl Default for Chomper {
423    fn default() -> Self {
424        Chomper::Continuous(0)
425    }
426}
427
428#[cfg(test)]
429mod test {
430    use super::*;
431
432    fn test_layout(max: Size, input: &[&str], expected: &str, wrap: Wrap) {
433        let mut strings = Strings::new(max, wrap);
434
435        for i in input {
436            if let ProcessResult::Break = strings.add_str(i) {
437                break;
438            }
439        }
440
441        let _size = strings.finish();
442
443        let lines = strings.lines();
444
445        let mut output = String::new();
446        for line in lines {
447            for e in line.entries {
448                match e {
449                    Segment::SetStyle(_) => todo!(),
450                    Segment::Str(s) => output.push_str(s),
451                }
452            }
453            output.push('\n');
454        }
455        output.pop();
456
457        assert_eq!(&output, expected);
458    }
459
460    #[test]
461    fn word_wrapping_layout() {
462        let inputs: &[(&[&str], &str)] = &[
463            (&["a\nb\nc"], "a\nb\nc"),
464            (&[" 12", "345 12", "345 "], " \n12345\n12345\n"),
465            (&[" 12", "345怀12", "345 "], " \n12345\n12345\n"),
466            (&[" šŸ‡šŸ‡šŸ‡", "šŸ‡šŸ‡ 12", "345 "], " \nšŸ‡šŸ‡\nšŸ‡šŸ‡\nšŸ‡ \n12345\n"),
467            (&["1", "23", "45 12", "345 "], "12345\n12345\n"),
468            (&["12345 abcde "], "12345\nabcde\n"),
469            (&["onereallylongword"], "onere\nallyl\nongwo\nrd"),
470            (&["ahello do the"], "ahell\no do \nthe"),
471            (&["hello do the"], "hello\ndo \nthe"),
472        ];
473
474        for (input, expected) in inputs {
475            test_layout(Size::new(5, 10), input, expected, Wrap::Normal);
476        }
477    }
478
479    #[test]
480    fn outliers() {
481        let inputs: &[(&[&str], &str)] = &[
482            (&["šŸ‡"], ""),
483            (&["\n"], "\n"),
484            (&["\n\n\n"], "\n\n\n"),
485            (&["abc"], "a\nb\nc"),
486        ];
487
488        for (input, expected) in inputs {
489            test_layout(Size::new(1, 10), input, expected, Wrap::Normal);
490        }
491    }
492
493    #[test]
494    fn layout_size() {
495        let inputs: &[(&[&str], &str)] = &[(&["123456789"], "123\n456")];
496
497        for (input, expected) in inputs {
498            test_layout(Size::new(3, 2), input, expected, Wrap::Normal);
499        }
500    }
501
502    #[test]
503    fn word_breaking_layout() {
504        let inputs: &[(&[&str], &str)] = &[(&["123 4567"], "123 4\n567")];
505
506        for (input, expected) in inputs {
507            test_layout(Size::new(5, 3), input, expected, Wrap::WordBreak);
508        }
509    }
510
511    #[test]
512    fn freeze_layout() {
513        let mut strings = Strings::new(Size::new(100, 10), Wrap::Normal);
514
515        assert_eq!(strings.add_str("abc"), ProcessResult::Continue);
516        strings.freeze();
517        assert_eq!(strings.add_str("abc"), ProcessResult::Break);
518    }
519
520    #[test]
521    fn limited_space() {
522        test_layout(Size::new(58, 0), &["meh"], "", Wrap::Normal);
523    }
524
525    #[test]
526    fn wrap_from_attribute() {
527        let mut attributes = anathema_value_resolver::Attributes::empty();
528
529        attributes.set("break", Wrap::WordBreak);
530        let word_break = attributes.get_as::<Wrap>("break").unwrap();
531        assert_eq!(word_break, Wrap::WordBreak);
532
533        attributes.set("normal", Wrap::Normal);
534        let word_break = attributes.get_as::<Wrap>("normal").unwrap();
535        assert_eq!(word_break, Wrap::Normal);
536    }
537}