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