tui_realm_stdlib/
utils.rs

1//! ## Utils
2//!
3//! `Utilities functions to work with components
4
5// deps
6extern crate textwrap;
7extern crate unicode_width;
8// local
9use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, TextModifiers, TextSpan};
10use tuirealm::Props;
11// ext
12use tuirealm::ratatui::style::{Color, Modifier, Style};
13use tuirealm::ratatui::text::Line as Spans;
14use tuirealm::ratatui::text::Span;
15use tuirealm::ratatui::widgets::Block;
16use unicode_width::UnicodeWidthStr;
17
18/// ### wrap_spans
19///
20/// Given a vector of `TextSpans`, it creates a list of `Spans` which mustn't exceed the provided width parameter.
21/// Each `Spans` in the returned `Vec` is a line in the text.
22pub fn wrap_spans<'a>(spans: &[TextSpan], width: usize, props: &Props) -> Vec<Spans<'a>> {
23    // Prepare result (capacity will be at least spans.len)
24    let mut res: Vec<Spans> = Vec::with_capacity(spans.len());
25    // Prepare environment
26    let mut line_width: usize = 0; // Incremental line width; mustn't exceed `width`.
27    let mut line_spans: Vec<Span> = Vec::new(); // Current line; when done, push to res and re-initialize
28    for span in spans.iter() {
29        // Get styles
30        let (fg, bg, tmod) = use_or_default_styles(props, span);
31        // Check if width would exceed...
32        if line_width + span.content.width() > width {
33            // Check if entire line is wider than the area
34            if span.content.width() > width {
35                // Wrap
36                let span_lines = textwrap::wrap(span.content.as_str(), width);
37                // iter lines
38                for span_line in span_lines.iter() {
39                    // Check if width would exceed...
40                    if line_width + span_line.width() > width {
41                        // New line
42                        res.push(Spans::from(line_spans));
43                        line_width = 0;
44                        line_spans = Vec::new();
45                    }
46                    // Increment line width
47                    line_width += span_line.width();
48                    // Push to line
49                    line_spans.push(Span::styled(
50                        span_line.to_string(),
51                        Style::default().fg(fg).bg(bg).add_modifier(tmod),
52                    ));
53                }
54                // Go to next iteration
55                continue;
56            } else {
57                // Just initialize a new line
58                res.push(Spans::from(line_spans));
59                line_width = 0;
60                line_spans = Vec::new();
61            }
62        }
63        // Push span to line
64        line_width += span.content.width();
65        line_spans.push(Span::styled(
66            span.content.to_string(),
67            Style::default().fg(fg).bg(bg).add_modifier(tmod),
68        ));
69    }
70    // if there are still elements in spans, push to result
71    if !line_spans.is_empty() {
72        res.push(Spans::from(line_spans));
73    }
74    // return res
75    res
76}
77
78/// ### use_or_default_styles
79///
80/// Returns the styles to be used; in case in span are default, use props'.
81/// The values returned are `(foreground, background, modifiers)`
82pub fn use_or_default_styles(props: &Props, span: &TextSpan) -> (Color, Color, Modifier) {
83    (
84        match span.fg {
85            Color::Reset => props
86                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
87                .unwrap_color(),
88            _ => span.fg,
89        },
90        match span.bg {
91            Color::Reset => props
92                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
93                .unwrap_color(),
94            _ => span.bg,
95        },
96        match span.modifiers.is_empty() {
97            true => props
98                .get_or(
99                    Attribute::TextProps,
100                    AttrValue::TextModifiers(TextModifiers::empty()),
101                )
102                .unwrap_text_modifiers(),
103            false => span.modifiers,
104        },
105    )
106}
107
108/// ### get_block
109///
110/// Construct a block for widget using block properties.
111/// If focus is true the border color is applied, otherwise inactive_style
112pub fn get_block<'a>(
113    props: Borders,
114    title: Option<(String, Alignment)>,
115    focus: bool,
116    inactive_style: Option<Style>,
117) -> Block<'a> {
118    let title = title.unwrap_or((String::default(), Alignment::Left));
119    Block::default()
120        .borders(props.sides)
121        .border_style(match focus {
122            true => props.style(),
123            false => {
124                inactive_style.unwrap_or_else(|| Style::default().fg(Color::Reset).bg(Color::Reset))
125            }
126        })
127        .border_type(props.modifiers)
128        .title(title.0)
129        .title_alignment(title.1)
130}
131
132/// ### calc_utf8_cursor_position
133///
134/// Calculate the UTF8 compliant position for the cursor given the characters preceeding the cursor position.
135/// Use this function to calculate cursor position whenever you want to handle UTF8 texts with cursors
136pub fn calc_utf8_cursor_position(chars: &[char]) -> u16 {
137    chars.iter().collect::<String>().width() as u16
138}
139
140#[cfg(test)]
141mod test {
142
143    use super::*;
144    use tuirealm::props::{Alignment, BorderSides, BorderType, Props};
145
146    use pretty_assertions::assert_eq;
147
148    #[test]
149    fn test_components_utils_wrap_spans() {
150        let mut props: Props = Props::default();
151        props.set(
152            Attribute::TextProps,
153            AttrValue::TextModifiers(TextModifiers::BOLD),
154        );
155        props.set(Attribute::Foreground, AttrValue::Color(Color::Red));
156        props.set(Attribute::Background, AttrValue::Color(Color::White));
157        // Prepare spans; let's start with two simple spans, which fits the line
158        let spans: Vec<TextSpan> = vec![TextSpan::from("hello, "), TextSpan::from("world!")];
159        assert_eq!(wrap_spans(&spans, 64, &props).len(), 1);
160        // Let's make a sentence, which would require two lines
161        let spans: Vec<TextSpan> = vec![
162            TextSpan::from("Hello, everybody, I'm Uncle Camel!"),
163            TextSpan::from("How's it going today?"),
164        ];
165        assert_eq!(wrap_spans(&spans, 32, &props).len(), 2);
166        // Let's make a sentence, which requires 3 lines, but with only one span
167        let spans: Vec<TextSpan> = vec![TextSpan::from(
168            "Hello everybody! My name is Uncle Camel. How's it going today?",
169        )];
170        // makes Hello everybody, my name is uncle, camel. how's it, goind today
171        assert_eq!(wrap_spans(&spans, 16, &props).len(), 4);
172        // Combine
173        let spans: Vec<TextSpan> = vec![
174            TextSpan::from("Lorem ipsum dolor sit amet, consectetur adipiscing elit."),
175            TextSpan::from("Canem!"),
176            TextSpan::from("In posuere sollicitudin vulputate"),
177            TextSpan::from("Sed vitae rutrum quam."),
178        ];
179        // "Lorem ipsum dolor sit amet,", "consectetur adipiscing elit. Canem!", "In posuere sollicitudin vulputate", "Sed vitae rutrum quam."
180        assert_eq!(wrap_spans(&spans, 36, &props).len(), 4);
181    }
182
183    #[test]
184    fn test_components_utils_use_or_default_styles() {
185        let mut props: Props = Props::default();
186        props.set(
187            Attribute::TextProps,
188            AttrValue::TextModifiers(TextModifiers::BOLD),
189        );
190        props.set(Attribute::Foreground, AttrValue::Color(Color::Red));
191        props.set(Attribute::Background, AttrValue::Color(Color::White));
192        let span: TextSpan = TextSpan::from("test")
193            .underlined()
194            .fg(Color::Yellow)
195            .bg(Color::Cyan);
196        // Not-default
197        let (fg, bg, modifiers) = use_or_default_styles(&props, &span);
198        assert_eq!(fg, Color::Yellow);
199        assert_eq!(bg, Color::Cyan);
200        assert!(modifiers.intersects(Modifier::UNDERLINED));
201        // Default
202        let span: TextSpan = TextSpan::from("test");
203        let (fg, bg, modifiers) = use_or_default_styles(&props, &span);
204        assert_eq!(fg, Color::Red);
205        assert_eq!(bg, Color::White);
206        assert!(modifiers.intersects(Modifier::BOLD));
207    }
208
209    #[test]
210    fn test_components_utils_get_block() {
211        let props = Borders::default()
212            .sides(BorderSides::ALL)
213            .color(Color::Red)
214            .modifiers(BorderType::Rounded);
215        get_block(
216            props.clone(),
217            Some(("title".to_string(), Alignment::Center)),
218            true,
219            None,
220        );
221        get_block(props, None, false, None);
222    }
223
224    #[test]
225    fn test_components_utils_calc_utf8_cursor_position() {
226        let chars: Vec<char> = vec!['v', 'e', 'e', 's', 'o'];
227        // Entire
228        assert_eq!(calc_utf8_cursor_position(chars.as_slice()), 5);
229        assert_eq!(calc_utf8_cursor_position(&chars[0..3]), 3);
230        // With special characters
231        let chars: Vec<char> = vec!['я', ' ', 'х', 'о', 'ч', 'у', ' ', 'с', 'п', 'а', 'т', 'ь'];
232        assert_eq!(calc_utf8_cursor_position(&chars[0..6]), 6);
233        let chars: Vec<char> = vec!['H', 'i', '😄'];
234        assert_eq!(calc_utf8_cursor_position(chars.as_slice()), 4);
235        let chars: Vec<char> = vec!['我', '之', '😄'];
236        assert_eq!(calc_utf8_cursor_position(chars.as_slice()), 6);
237    }
238}