• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

veeso / tui-realm-stdlib / 20434720435

22 Dec 2025 02:26PM UTC coverage: 69.86% (-0.3%) from 70.169%
20434720435

Pull #46

github

web-flow
Merge c1529bfb4 into 9528c3045
Pull Request #46: Apply changes for core `TextSpan` changes

221 of 271 new or added lines in 6 files covered. (81.55%)

3 existing lines in 2 files now uncovered.

3187 of 4562 relevant lines covered (69.86%)

2.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.4
/src/utils.rs
1
//! ## Utils
2
//!
3
//! Utilities functions to work with components
4

5
// deps
6
extern crate textwrap;
7
extern crate unicode_width;
8
use std::borrow::Cow;
9

10
// local
11
use tuirealm::Props;
12
use tuirealm::props::{Alignment, Attribute, Borders};
13
// ext
14
use tuirealm::ratatui::style::{Color, Style};
15
use tuirealm::ratatui::text::{Line, Span, Text};
16
use tuirealm::ratatui::widgets::Block;
17
use unicode_width::UnicodeWidthStr;
18

19
/// ### wrap_spans
20
///
21
/// Given a vector of [`Span`]s, it creates a list of `Spans` which mustn't exceed the provided width parameter.
22
/// Each [`Line`] in the returned `Vec` is a line in the text.
23
#[must_use]
24
pub fn wrap_spans<'a, 'b: 'a>(spans: &[&'b Span<'a>], width: usize) -> Vec<Line<'a>> {
5✔
25
    // Prepare result (capacity will be at least spans.len)
26
    let mut res: Vec<Line> = Vec::with_capacity(spans.len());
5✔
27
    // Prepare environment
28
    let mut line_width: usize = 0; // Incremental line width; mustn't exceed `width`.
5✔
29
    let mut line_spans: Vec<Span> = Vec::new(); // Current line; when done, push to res and re-initialize
5✔
30
    for span in spans {
11✔
31
        // Check if width would exceed...
32
        if line_width + span.content.width() > width {
11✔
33
            // Check if entire line is wider than the area
34
            if span.content.width() > width {
7✔
35
                // Wrap
36
                let span_lines = textwrap::wrap(&span.content, width);
4✔
37
                // iter lines
38
                for span_line in span_lines {
10✔
39
                    // Check if width would exceed...
40
                    if line_width + span_line.width() > width {
10✔
41
                        // New line
6✔
42
                        res.push(Line::from(line_spans));
6✔
43
                        line_width = 0;
6✔
44
                        line_spans = Vec::new();
6✔
45
                    }
6✔
46
                    // Increment line width
47
                    line_width += span_line.width();
10✔
48
                    // Push to line
49
                    line_spans.push(Span::styled(span_line, span.style));
10✔
50
                }
51
                // Go to next iteration
52
                continue;
4✔
53
            }
3✔
54
            // Just initialize a new line
55
            res.push(Line::from(line_spans));
3✔
56
            line_width = 0;
3✔
57
            line_spans = Vec::new();
3✔
58
        }
4✔
59
        // Push span to line
60
        line_width += span.content.width();
7✔
61
        line_spans.push(Span::styled(span.content.to_string(), span.style));
7✔
62
    }
63
    // if there are still elements in spans, push to result
64
    if !line_spans.is_empty() {
5✔
65
        res.push(Line::from(line_spans));
5✔
66
    }
5✔
67
    // return res
68
    res
5✔
69
}
5✔
70

71
/// ### get_block
72
///
73
/// Construct a block for widget using block properties.
74
/// If focus is true the border color is applied, otherwise inactive_style
75
#[must_use]
76
pub fn get_block<T: AsRef<str>>(
2✔
77
    props: Borders,
2✔
78
    title: Option<&(T, Alignment)>,
2✔
79
    focus: bool,
2✔
80
    inactive_style: Option<Style>,
2✔
81
) -> Block<'_> {
2✔
82
    let mut block = Block::default()
2✔
83
        .borders(props.sides)
2✔
84
        .border_style(if focus {
2✔
85
            props.style()
1✔
86
        } else {
87
            inactive_style.unwrap_or_else(|| Style::default().fg(Color::Reset).bg(Color::Reset))
1✔
88
        })
89
        .border_type(props.modifiers);
2✔
90

91
    if let Some((title, alignment)) = title {
2✔
92
        block = block.title(title.as_ref()).title_alignment(*alignment);
1✔
93
    }
1✔
94

95
    block
2✔
96
}
2✔
97

98
/// Get the [`Attribute::Title`] or a Centered default
99
#[must_use]
100
pub fn get_title_or_center(props: &Props) -> (&str, Alignment) {
×
101
    props
×
102
        .get_ref(Attribute::Title)
×
103
        .and_then(|v| v.as_title())
×
104
        .map_or(("", Alignment::Center), |v| (v.0.as_str(), v.1))
×
105
}
×
106

107
/// ### calc_utf8_cursor_position
108
///
109
/// Calculate the UTF8 compliant position for the cursor given the characters preceeding the cursor position.
110
/// Use this function to calculate cursor position whenever you want to handle UTF8 texts with cursors
111
#[must_use]
112
pub fn calc_utf8_cursor_position(chars: &[char]) -> u16 {
5✔
113
    chars.iter().collect::<String>().width() as u16
5✔
114
}
5✔
115

116
/// Convert a `&Span` to a `Span` by using [`Cow::Borrowed`].
117
///
118
/// Note that a normal `Span::clone` (and by extension `Cow::clone`) will preserve the `Cow` Variant.
NEW
119
pub fn borrow_clone_span<'a, 'b: 'a>(span: &'b Span<'a>) -> Span<'a> {
×
NEW
120
    Span {
×
NEW
121
        style: span.style,
×
NEW
122
        content: Cow::Borrowed(&*span.content),
×
NEW
123
    }
×
NEW
124
}
×
125

126
/// Convert a `&Line` to a `Line` by using [`Cow::Borrowed`].
NEW
127
pub fn borrow_clone_line<'a, 'b: 'a>(line: &'b Line<'a>) -> Line<'a> {
×
NEW
128
    Line {
×
NEW
129
        spans: line.spans.iter().map(borrow_clone_span).collect(),
×
NEW
130
        ..*line
×
NEW
131
    }
×
NEW
132
}
×
133

134
/// Convert a `&Text` to a `Text` by using [`Cow::Borrowed`].
NEW
135
pub fn borrow_clone_text<'a, 'b: 'a>(text: &'b Text<'a>) -> Text<'a> {
×
NEW
136
    Text {
×
NEW
137
        lines: text.lines.iter().map(borrow_clone_line).collect(),
×
NEW
138
        ..*text
×
NEW
139
    }
×
NEW
140
}
×
141

142
#[cfg(test)]
143
mod test {
144

145
    use super::*;
146
    use tuirealm::props::{Alignment, BorderSides, BorderType};
147

148
    use pretty_assertions::assert_eq;
149

150
    #[test]
151
    fn test_components_utils_wrap_spans() {
1✔
152
        // Prepare spans; let's start with two simple spans, which fits the line
153
        let spans: Vec<Span> = vec![Span::from("hello, "), Span::from("world!")];
1✔
154
        let spans: Vec<&Span> = spans.iter().collect();
1✔
155
        assert_eq!(wrap_spans(&spans, 64).len(), 1);
1✔
156
        // Let's make a sentence, which would require two lines
157
        let spans: Vec<Span> = vec![
1✔
158
            Span::from("Hello, everybody, I'm Uncle Camel!"),
1✔
159
            Span::from("How's it going today?"),
1✔
160
        ];
161
        let spans: Vec<&Span> = spans.iter().collect();
1✔
162
        assert_eq!(wrap_spans(&spans, 32).len(), 2);
1✔
163
        // Let's make a sentence, which requires 3 lines, but with only one span
164
        let spans: Vec<Span> = vec![Span::from(
1✔
165
            "Hello everybody! My name is Uncle Camel. How's it going today?",
166
        )];
167
        let spans: Vec<&Span> = spans.iter().collect();
1✔
168
        // makes Hello everybody, my name is uncle, camel. how's it, goind today
169
        assert_eq!(wrap_spans(&spans, 16).len(), 4);
1✔
170
        // Combine
171
        let spans: Vec<Span> = vec![
1✔
172
            Span::from("Lorem ipsum dolor sit amet, consectetur adipiscing elit."),
1✔
173
            Span::from("Canem!"),
1✔
174
            Span::from("In posuere sollicitudin vulputate"),
1✔
175
            Span::from("Sed vitae rutrum quam."),
1✔
176
        ];
177
        let spans: Vec<&Span> = spans.iter().collect();
1✔
178
        // "Lorem ipsum dolor sit amet,", "consectetur adipiscing elit. Canem!", "In posuere sollicitudin vulputate", "Sed vitae rutrum quam."
179
        assert_eq!(wrap_spans(&spans, 36).len(), 4);
1✔
180
    }
1✔
181

182
    #[test]
183
    fn wrap_spans_should_preserve_style_if_wrapped() {
1✔
184
        let input = [
1✔
185
            Span::styled("hello there", Style::new().fg(Color::Black)),
1✔
186
            Span::raw("test"),
1✔
187
        ];
1✔
188
        let input = input.iter().collect::<Vec<_>>();
1✔
189
        let res = wrap_spans(&input, 5);
1✔
190
        assert_eq!(res.len(), 3);
1✔
191
        assert_eq!(
1✔
192
            res[0],
1✔
193
            Line::from(Span::styled("hello", Style::new().fg(Color::Black)))
1✔
194
        );
195
        assert_eq!(
1✔
196
            res[1],
1✔
197
            Line::from(Span::styled("there", Style::new().fg(Color::Black)))
1✔
198
        );
199
        assert_eq!(res[2], Line::from(Span::raw("test")));
1✔
200
    }
1✔
201

202
    #[test]
203
    fn test_components_utils_get_block() {
1✔
204
        let borders = Borders::default()
1✔
205
            .sides(BorderSides::ALL)
1✔
206
            .color(Color::Red)
1✔
207
            .modifiers(BorderType::Rounded);
1✔
208
        let _ = get_block(borders, Some(&("title", Alignment::Center)), true, None);
1✔
209
        let _ = get_block::<&str>(borders, None, false, None);
1✔
210
    }
1✔
211

212
    #[test]
213
    fn test_components_utils_calc_utf8_cursor_position() {
1✔
214
        let chars: Vec<char> = vec!['v', 'e', 'e', 's', 'o'];
1✔
215
        // Entire
216
        assert_eq!(calc_utf8_cursor_position(chars.as_slice()), 5);
1✔
217
        assert_eq!(calc_utf8_cursor_position(&chars[0..3]), 3);
1✔
218
        // With special characters
219
        let chars: Vec<char> = vec!['я', ' ', 'х', 'о', 'ч', 'у', ' ', 'с', 'п', 'а', 'т', 'ь'];
1✔
220
        assert_eq!(calc_utf8_cursor_position(&chars[0..6]), 6);
1✔
221
        let chars: Vec<char> = vec!['H', 'i', '😄'];
1✔
222
        assert_eq!(calc_utf8_cursor_position(chars.as_slice()), 4);
1✔
223
        let chars: Vec<char> = vec!['我', '之', '😄'];
1✔
224
        assert_eq!(calc_utf8_cursor_position(chars.as_slice()), 6);
1✔
225
    }
1✔
226
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc