stylish_stringlike/text/
width_sliceable.rs

1use crate::text::{RawText, Sliceable};
2use std::ops::RangeBounds;
3use unicode_segmentation::UnicodeSegmentation;
4use unicode_width::UnicodeWidthStr;
5
6/// Provides a function for slicing by grapheme width rather than bytes.
7///
8/// This is useful for ensuring that a text object fits in a given terminal
9/// width.
10pub trait WidthSliceable {
11    type Output: Sized;
12    /// Slice an object by width rather than by bytes.
13    ///
14    /// # Example
15    ///
16    /// ```
17    /// use stylish_stringlike::text::WidthSliceable;
18    /// let foo = String::from("foobar");
19    /// assert_eq!(Some(String::from("oob")), foo.slice_width(1..4));
20    /// let bar = String::from("🙈🙉🙊");
21    /// // Monkeys are two columns wide, so we get nothing back
22    /// assert_eq!(None, bar.slice_width(..1));
23    /// // We get one monkey for two columns
24    /// assert_eq!(Some(String::from("🙈")), bar.slice_width(..2));
25    /// // If we aren't column-aligned, we get nothing because no one monkey fits between 1 and 3
26    /// assert_eq!(None, bar.slice_width(1..3));
27    /// ```
28    fn slice_width<R>(&self, range: R) -> Option<Self::Output>
29    where
30        R: RangeBounds<usize>;
31}
32
33impl<T> WidthSliceable for T
34where
35    T: RawText + Sliceable + Sized,
36{
37    type Output = T;
38    fn slice_width<R>(&self, range: R) -> Option<Self::Output>
39    where
40        Self: Sized,
41        R: RangeBounds<usize>,
42    {
43        let mut start_byte = None;
44        let mut end_byte = None;
45        let mut current_width = 0;
46        let mut current_byte = 0;
47        for grapheme in self.raw().graphemes(true) {
48            let grapheme_width = grapheme.width();
49            let in_range = {
50                let mut in_range = true;
51                for w in current_width..current_width + grapheme_width {
52                    if !range.contains(&w) {
53                        in_range = false;
54                        break;
55                    }
56                }
57                in_range
58            };
59            current_width += grapheme_width;
60            match (in_range, start_byte) {
61                (true, None) => start_byte = Some(current_byte),
62                (false, Some(_)) => {
63                    end_byte = Some(current_byte);
64                    break;
65                }
66                _ => {}
67            }
68            current_byte += grapheme.len();
69        }
70        match (start_byte, end_byte) {
71            (Some(s), Some(e)) => self.slice(s..e),
72            (Some(s), None) => self.slice(s..),
73            (None, Some(e)) => self.slice(..e),
74            (None, None) => None,
75        }
76    }
77}
78
79impl<T> WidthSliceable for Option<T>
80where
81    T: WidthSliceable,
82{
83    type Output = T::Output;
84    fn slice_width<R>(&self, range: R) -> Option<Self::Output>
85    where
86        R: RangeBounds<usize>,
87    {
88        match self {
89            Some(t) => t.slice_width(range),
90            None => None,
91        }
92    }
93}