1use std::borrow::Cow;
4
5use tuirealm::props::{Borders, Title};
6use tuirealm::ratatui::style::Style;
7use tuirealm::ratatui::text::{Line, Span, Text};
8use tuirealm::ratatui::widgets::{Block, TitlePosition};
9use unicode_width::UnicodeWidthStr;
10
11#[must_use]
14pub fn wrap_spans<'a, 'b: 'a>(spans: &[&'b Span<'a>], width: usize) -> Vec<Line<'a>> {
15 let mut res: Vec<Line> = Vec::with_capacity(spans.len());
17 let mut line_width: usize = 0; let mut line_spans: Vec<Span> = Vec::new(); for span in spans {
21 if line_width + span.content.width() > width {
23 if span.content.width() > width {
25 let span_lines = textwrap::wrap(&span.content, width);
27 for span_line in span_lines {
29 if line_width + span_line.width() > width {
31 res.push(Line::from(line_spans));
33 line_width = 0;
34 line_spans = Vec::new();
35 }
36 line_width += span_line.width();
38 line_spans.push(Span::styled(span_line, span.style));
40 }
41 continue;
43 }
44 res.push(Line::from(line_spans));
46 line_width = 0;
47 line_spans = Vec::new();
48 }
49 line_width += span.content.width();
51 line_spans.push(Span::styled(span.content.to_string(), span.style));
52 }
53 if !line_spans.is_empty() {
55 res.push(Line::from(line_spans));
56 }
57
58 res
59}
60
61#[inline]
63fn make_new_line<'a>(orig: &Line<'a>) -> Line<'a> {
64 Line::default().style(orig.style)
65}
66
67#[inline]
69fn commit_line<'a>(newline: &mut Line<'a>, newlines: &mut Vec<Line<'a>>, orig: &Line<'a>) {
70 let mut final_line = make_new_line(orig);
71 std::mem::swap(newline, &mut final_line);
72 newlines.push(final_line);
73}
74
75fn wrap_single_span<'a>(
79 span: &'a Span<'a>,
80 newlines: &mut Vec<Line<'a>>,
81 newline: &mut Line<'a>,
82 orig_line: &Line<'a>,
83 width: usize,
84 consumed_width: &mut usize,
85) -> usize {
86 let mut remainder_width = width - *consumed_width;
87
88 if remainder_width == 0 && newline.width() != 0 {
91 commit_line(newline, newlines, orig_line);
92 remainder_width = width;
93 }
94
95 let words = textwrap::WordSeparator::AsciiSpace.find_words(&span.content);
98 let split_words =
99 textwrap::word_splitters::split_words(words, &textwrap::WordSplitter::HyphenSplitter);
100 let broken_words = textwrap::core::break_words(split_words, remainder_width);
101
102 let line_widths = [remainder_width, width];
103 let wrapped_words = textwrap::WrapAlgorithm::FirstFit.wrap(&broken_words, &line_widths);
104
105 let mut consumed_idx = 0;
107 let last_idx = wrapped_words.len().saturating_sub(1);
108 let mut final_consumed_width = 0;
109 for (idx, words) in wrapped_words.iter().enumerate() {
111 if words.is_empty() {
112 continue;
113 }
114
115 let minus_whitespace = 0;
126
127 let len = words
129 .iter()
130 .map(|word| word.len() + word.whitespace.len())
131 .sum::<usize>()
132 - minus_whitespace;
133
134 let split_text = &span.content[consumed_idx..consumed_idx + len];
135 consumed_idx += len + minus_whitespace;
136
137 let newspan = Span::styled(split_text, span.style);
138 newline.push_span(newspan);
139
140 if idx != last_idx {
142 commit_line(newline, newlines, orig_line);
143 } else {
144 final_consumed_width = newline.width();
145 }
146 }
147
148 final_consumed_width
149}
150
151pub fn wrap_lines<'a, 'b: 'a>(lines: &[&'b Line<'a>], width: usize) -> Vec<Line<'a>> {
153 let mut new_lines: Vec<Line> = Vec::with_capacity(lines.len());
155
156 for line in lines {
157 if line.width() <= width {
159 new_lines.push(borrow_clone_line(line));
160 continue;
161 }
162
163 let mut consumed_width: usize = 0;
165 let mut newline = make_new_line(line);
166
167 for span in line.iter() {
168 let span_width = span.content.width();
170 if span_width <= width - consumed_width {
171 newline.push_span(borrow_clone_span(span));
172 consumed_width += span_width;
173 continue;
174 }
175
176 let new_consumed = wrap_single_span(
177 span,
178 &mut new_lines,
179 &mut newline,
180 line,
181 width,
182 &mut consumed_width,
183 );
184 consumed_width = new_consumed;
185 }
186
187 if !newline.spans.is_empty() {
189 new_lines.push(newline);
190 }
191 }
192
193 new_lines
194}
195
196#[must_use]
200pub fn get_block(
201 props: Borders,
202 title: Option<&Title>,
203 focus: bool,
204 inactive_style: Option<Style>,
205) -> Block<'_> {
206 let mut block = Block::default()
207 .borders(props.sides)
208 .border_style(if focus {
209 props.style()
210 } else {
211 inactive_style.unwrap_or_default()
212 })
213 .border_type(props.modifiers);
214
215 if let Some(title) = title {
216 block = match title.position {
217 TitlePosition::Top => block.title_top(borrow_clone_line(&title.content)),
218 TitlePosition::Bottom => block.title_bottom(borrow_clone_line(&title.content)),
219 };
220 }
221
222 block
223}
224
225#[must_use]
229pub fn calc_utf8_cursor_position(chars: &[char]) -> u16 {
230 chars.iter().collect::<String>().width() as u16
231}
232
233pub fn borrow_clone_span<'a, 'b: 'a>(span: &'b Span<'a>) -> Span<'a> {
237 Span {
238 content: Cow::Borrowed(&*span.content),
239 ..*span
240 }
241}
242
243pub fn borrow_clone_line<'a, 'b: 'a>(line: &'b Line<'a>) -> Line<'a> {
245 Line {
246 spans: line.spans.iter().map(borrow_clone_span).collect(),
247 ..*line
248 }
249}
250
251pub fn borrow_clone_text<'a, 'b: 'a>(text: &'b Text<'a>) -> Text<'a> {
253 Text {
254 lines: text.lines.iter().map(borrow_clone_line).collect(),
255 ..*text
256 }
257}
258
259#[cfg(test)]
260mod test {
261
262 use pretty_assertions::assert_eq;
263 use tuirealm::props::{BorderSides, BorderType, Color, HorizontalAlignment};
264
265 use super::*;
266
267 #[test]
268 fn test_components_utils_wrap_spans() {
269 let spans: Vec<Span> = vec![Span::from("hello, "), Span::from("world!")];
271 let spans: Vec<&Span> = spans.iter().collect();
272 assert_eq!(wrap_spans(&spans, 64).len(), 1);
273 let spans: Vec<Span> = vec![
275 Span::from("Hello, everybody, I'm Uncle Camel!"),
276 Span::from("How's it going today?"),
277 ];
278 let spans: Vec<&Span> = spans.iter().collect();
279 assert_eq!(wrap_spans(&spans, 32).len(), 2);
280 let spans: Vec<Span> = vec![Span::from(
282 "Hello everybody! My name is Uncle Camel. How's it going today?",
283 )];
284 let spans: Vec<&Span> = spans.iter().collect();
285 assert_eq!(wrap_spans(&spans, 16).len(), 4);
287 let spans: Vec<Span> = vec![
289 Span::from("Lorem ipsum dolor sit amet, consectetur adipiscing elit."),
290 Span::from("Canem!"),
291 Span::from("In posuere sollicitudin vulputate"),
292 Span::from("Sed vitae rutrum quam."),
293 ];
294 let spans: Vec<&Span> = spans.iter().collect();
295 assert_eq!(wrap_spans(&spans, 36).len(), 4);
297 }
298
299 #[test]
300 fn wrap_spans_should_preserve_style_if_wrapped() {
301 let input = [
302 Span::styled("hello there", Style::new().fg(Color::Black)),
303 Span::raw("test"),
304 ];
305 let input = input.iter().collect::<Vec<_>>();
306 let res = wrap_spans(&input, 5);
307 assert_eq!(res.len(), 3);
308 assert_eq!(
309 res[0],
310 Line::from(Span::styled("hello", Style::new().fg(Color::Black)))
311 );
312 assert_eq!(
313 res[1],
314 Line::from(Span::styled("there", Style::new().fg(Color::Black)))
315 );
316 assert_eq!(res[2], Line::from(Span::raw("test")));
317 }
318
319 #[test]
320 fn test_components_utils_get_block() {
321 let borders = Borders::default()
322 .sides(BorderSides::ALL)
323 .color(Color::Red)
324 .modifiers(BorderType::Rounded);
325 let _ = get_block(
326 borders,
327 Some(&Title::from("title").alignment(HorizontalAlignment::Center)),
328 true,
329 None,
330 );
331 let _ = get_block(borders, None, false, None);
332 }
333
334 #[test]
335 fn test_components_utils_calc_utf8_cursor_position() {
336 let chars: Vec<char> = vec!['v', 'e', 'e', 's', 'o'];
337 assert_eq!(calc_utf8_cursor_position(chars.as_slice()), 5);
339 assert_eq!(calc_utf8_cursor_position(&chars[0..3]), 3);
340 let chars: Vec<char> = vec!['я', ' ', 'х', 'о', 'ч', 'у', ' ', 'с', 'п', 'а', 'т', 'ь'];
342 assert_eq!(calc_utf8_cursor_position(&chars[0..6]), 6);
343 let chars: Vec<char> = vec!['H', 'i', '😄'];
344 assert_eq!(calc_utf8_cursor_position(chars.as_slice()), 4);
345 let chars: Vec<char> = vec!['我', '之', '😄'];
346 assert_eq!(calc_utf8_cursor_position(chars.as_slice()), 6);
347 }
348
349 mod lines_wrap {
350 use pretty_assertions::assert_eq;
351 use tuirealm::props::{LineStatic, SpanStatic, Style};
352 use tuirealm::ratatui::text::Span;
353
354 use crate::utils::wrap_lines;
355
356 #[test]
357 fn should_not_do_any_wrapping() {
358 assert_eq!(
360 wrap_lines(&[&LineStatic::default()], 10),
361 [LineStatic::default()]
362 );
363
364 assert_eq!(
366 wrap_lines(&[&LineStatic::raw("test")], 10),
367 [LineStatic::raw("test")]
368 );
369
370 assert_eq!(
372 wrap_lines(
373 &[&LineStatic::from_iter([
374 SpanStatic::raw("hello"),
375 SpanStatic::raw("there")
376 ])],
377 10
378 ),
379 [LineStatic::from_iter([
380 SpanStatic::raw("hello"),
381 SpanStatic::raw("there")
382 ])]
383 );
384 }
385
386 #[test]
387 fn should_wrap_single_span() {
388 assert_eq!(
389 wrap_lines(&[&LineStatic::raw("something really long")], 10),
390 [
391 LineStatic::raw("something "),
392 LineStatic::raw("really "),
393 LineStatic::raw("long")
394 ]
395 );
396
397 assert_eq!(
399 wrap_lines(
400 &[&LineStatic::from(Span::styled(
401 "something really long",
402 Style::default().crossed_out()
403 ))
404 .style(Style::default().italic())],
405 10
406 ),
407 [
408 LineStatic::from(Span::styled("something ", Style::default().crossed_out()))
409 .style(Style::default().italic()),
410 LineStatic::from(Span::styled("really ", Style::default().crossed_out()))
411 .style(Style::default().italic()),
412 LineStatic::from(Span::styled("long", Style::default().crossed_out()))
413 .style(Style::default().italic())
414 ]
415 );
416 }
417
418 #[test]
419 fn should_wrap_multi_span() {
420 assert_eq!(
421 wrap_lines(
422 &[&LineStatic::from_iter([
423 SpanStatic::raw("something "),
424 SpanStatic::raw("really "),
425 SpanStatic::raw("long")
426 ])],
427 10
428 ),
429 [
430 LineStatic::raw("something "),
431 LineStatic::from_iter([Span::raw("really "), Span::raw("lon")]),
432 LineStatic::raw("g")
433 ]
434 );
435
436 assert_eq!(
438 wrap_lines(
439 &[&LineStatic::from_iter([
440 SpanStatic::styled("something ", Style::default().crossed_out()),
441 SpanStatic::raw("really "),
442 SpanStatic::styled("long", Style::default().italic())
443 ])],
444 10
445 ),
446 [
447 LineStatic::from(Span::styled("something ", Style::default().crossed_out())),
448 LineStatic::from_iter([
449 Span::raw("really "),
450 Span::styled("lon", Style::default().italic())
451 ]),
452 LineStatic::from(Span::styled("g", Style::default().italic()))
453 ]
454 );
455 }
456 }
457}