1use ratatui_textarea::WrapMode;
10use unicode_segmentation::UnicodeSegmentation;
11use unicode_width::UnicodeWidthChar;
12
13#[must_use]
15pub(crate) fn wrapped_line_count(line: &str, mode: WrapMode, width: usize) -> usize {
16 let width = width.max(1);
17 match mode {
18 WrapMode::None => 1,
19 WrapMode::Glyph => split_range_by_grapheme_width(line, 0, line.len(), width),
20 WrapMode::Word => wrap_word_chunks(line, width, false),
21 WrapMode::WordOrGlyph => wrap_word_chunks(line, width, true),
22 }
23 .max(1)
24}
25
26fn wrap_word_chunks(line: &str, width: usize, fallback_to_glyph: bool) -> usize {
29 let mut line_count = 0;
30 let mut seg_start = 0;
31 let mut seg_end = 0;
32 let mut seg_width = 0usize;
33
34 for (start, text) in UnicodeSegmentation::split_word_bound_indices(line) {
35 let end = start + text.len();
36
37 if seg_end == seg_start {
38 seg_start = start;
39 }
40
41 let chunk_width = display_width(text);
42 if seg_width + chunk_width <= width {
43 seg_end = end;
44 seg_width += chunk_width;
45 continue;
46 }
47
48 if seg_end > seg_start {
49 line_count += 1;
50 seg_start = start;
51 seg_width = 0;
52
53 if chunk_width <= width {
54 seg_end = end;
55 seg_width += chunk_width;
56 continue;
57 }
58 }
59
60 if fallback_to_glyph {
61 line_count += split_range_by_grapheme_width(line, start, end, width);
62 } else {
63 line_count += 1;
64 }
65
66 seg_start = end;
67 seg_end = end;
68 seg_width = 0;
69 }
70
71 if seg_end > seg_start {
72 line_count += 1;
73 }
74
75 line_count
76}
77
78fn split_range_by_grapheme_width(line: &str, start: usize, end: usize, width: usize) -> usize {
80 let mut line_count = 0;
81
82 let mut segment_start = start;
83 while segment_start < end {
84 let mut segment_end = segment_start;
85 let mut segment_width = 0usize;
86
87 for (offset, grapheme) in
88 UnicodeSegmentation::grapheme_indices(&line[segment_start..end], true)
89 {
90 let grapheme_start = segment_start + offset;
91 let grapheme_end = grapheme_start + grapheme.len();
92 let grapheme_width = display_width(grapheme);
93
94 if segment_end != segment_start && segment_width + grapheme_width > width {
95 break;
96 }
97
98 segment_end = grapheme_end;
99 segment_width += grapheme_width;
100 if segment_width > width {
101 break;
102 }
103 }
104
105 if segment_end == segment_start {
106 if let Some(ch) = line[segment_start..end].chars().next() {
107 segment_end = segment_start + ch.len_utf8();
108 } else {
109 break;
110 }
111 }
112
113 segment_start = segment_end;
114
115 line_count += 1;
116 }
117
118 line_count
119}
120
121#[inline]
123fn display_width(text: &str) -> usize {
124 text.chars().map(|c| c.width().unwrap_or(0)).sum()
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn trailing_space_rolls_onto_its_own_row() {
133 assert_eq!(wrapped_line_count("foo ", WrapMode::Word, 3), 2);
134 assert_eq!(wrapped_line_count("foo ", WrapMode::WordOrGlyph, 3), 2);
135 assert_eq!(wrapped_line_count("foo ", WrapMode::Glyph, 3), 2);
136 }
137
138 #[test]
139 fn internal_space_runs_consume_width() {
140 assert_eq!(wrapped_line_count("a b", WrapMode::Word, 2), 3);
141 }
142
143 #[test]
144 fn empty_line_is_one_row() {
145 assert_eq!(wrapped_line_count("", WrapMode::Word, 10), 1);
146 }
147
148 #[test]
149 fn word_wrap_keeps_long_word() {
150 assert_eq!(wrapped_line_count("helloworld", WrapMode::Word, 4), 1);
151 }
152
153 #[test]
154 fn word_or_glyph_wrap_splits_long_word() {
155 assert_eq!(
156 wrapped_line_count("helloworld", WrapMode::WordOrGlyph, 4),
157 3
158 );
159 }
160
161 #[test]
162 fn glyph_wrap_handles_wide_chars() {
163 assert_eq!(wrapped_line_count("ab犬猫", WrapMode::Glyph, 4), 2);
164 }
165
166 #[test]
167 fn glyph_wrap_keeps_combining_grapheme_cluster() {
168 assert_eq!(wrapped_line_count("e\u{301}x", WrapMode::Glyph, 1), 2);
169 }
170
171 #[test]
172 fn glyph_wrap_preserves_full_mixed_width_row_capacity() {
173 assert_eq!(wrapped_line_count("a中bcde", WrapMode::Glyph, 4), 2);
174 }
175}