Skip to main content

view/
line_segment.rs

1use std::cell::RefCell;
2
3use display::DisplayColor;
4use unicode_segmentation::UnicodeSegmentation;
5use unicode_width::UnicodeWidthStr;
6use xi_unicode::EmojiExt;
7
8fn unicode_column_width(s: &str) -> usize {
9	s.graphemes(true).map(grapheme_column_width).sum()
10}
11
12fn grapheme_column_width(s: &str) -> usize {
13	for c in s.chars() {
14		if c.is_emoji_modifier_base() || c.is_emoji_modifier() {
15			return 2;
16		}
17	}
18	UnicodeWidthStr::width(s)
19}
20
21pub(crate) struct SegmentPartial {
22	content: String,
23	length: usize,
24}
25
26impl SegmentPartial {
27	fn new(content: &str, length: usize) -> Self {
28		Self {
29			content: String::from(content),
30			length,
31		}
32	}
33
34	pub(crate) fn get_content(&self) -> &str {
35		self.content.as_str()
36	}
37
38	pub(crate) const fn get_length(&self) -> usize {
39		self.length
40	}
41}
42
43/// Represents a segment in a larger line.
44#[derive(Clone, Debug)]
45pub struct LineSegment {
46	color: DisplayColor,
47	dim: bool,
48	reverse: bool,
49	text: String,
50	length: usize,
51	underline: bool,
52}
53
54impl LineSegment {
55	/// Create a new instance with just the content.
56	#[must_use]
57	#[inline]
58	pub fn new(text: &str) -> Self {
59		Self::new_with_color_and_style(text, DisplayColor::Normal, false, false, false)
60	}
61
62	/// Create a new instance with added color.
63	#[must_use]
64	#[inline]
65	pub fn new_with_color(text: &str, color: DisplayColor) -> Self {
66		Self::new_with_color_and_style(text, color, false, false, false)
67	}
68
69	/// Create a new instance with added color and style.
70	#[must_use]
71	#[inline]
72	pub fn new_with_color_and_style(
73		text: &str,
74		color: DisplayColor,
75		dim: bool,
76		underline: bool,
77		reverse: bool,
78	) -> Self {
79		Self {
80			text: String::from(text),
81			color,
82			reverse,
83			dim,
84			length: unicode_column_width(text),
85			underline,
86		}
87	}
88
89	pub(crate) fn get_content(&self) -> &str {
90		self.text.as_str()
91	}
92
93	pub(crate) const fn get_color(&self) -> DisplayColor {
94		self.color
95	}
96
97	pub(crate) const fn is_dimmed(&self) -> bool {
98		self.dim
99	}
100
101	pub(crate) const fn is_underlined(&self) -> bool {
102		self.underline
103	}
104
105	pub(crate) const fn is_reversed(&self) -> bool {
106		self.reverse
107	}
108
109	pub(crate) const fn get_length(&self) -> usize {
110		self.length
111	}
112
113	pub(crate) fn get_partial_segment(&self, left: usize, max_width: usize) -> SegmentPartial {
114		let segment_length = unicode_column_width(self.text.as_str());
115
116		// segment is hidden to the left of the line/scroll
117		if segment_length <= left {
118			SegmentPartial::new("", 0)
119		}
120		else {
121			let graphemes = UnicodeSegmentation::graphemes(self.text.as_str(), true);
122
123			let skip_length = RefCell::new(0);
124			let graphemes_itr = graphemes.skip_while(|v| {
125				let len = grapheme_column_width(v);
126				let value = *skip_length.borrow();
127				if value + len > left {
128					false
129				}
130				else {
131					_ = skip_length.replace(value + len);
132					true
133				}
134			});
135
136			if segment_length - *skip_length.borrow() >= max_width {
137				let take_length = RefCell::new(0);
138				let partial_line = graphemes_itr
139					.take_while(|v| {
140						let len = grapheme_column_width(v);
141						let value = *take_length.borrow();
142						if value + len > max_width {
143							false
144						}
145						else {
146							_ = take_length.replace(value + len);
147							true
148						}
149					})
150					.collect::<String>();
151				SegmentPartial::new(partial_line.as_str(), take_length.into_inner())
152			}
153			else {
154				let partial_line = graphemes_itr.collect::<String>();
155				SegmentPartial::new(partial_line.as_str(), segment_length - skip_length.into_inner())
156			}
157		}
158	}
159}
160
161#[cfg(test)]
162mod tests {
163	use super::*;
164
165	#[test]
166	fn line_segment_case_new() {
167		let line_segment = LineSegment::new("D'fhuascail Íosa, Úrmhac na hÓighe Beannaithe, pór Éava agus Ádhaimh");
168
169		assert_eq!(line_segment.get_color(), DisplayColor::Normal);
170		assert!(!line_segment.is_dimmed());
171		assert!(!line_segment.is_underlined());
172		assert!(!line_segment.is_reversed());
173		assert_eq!(line_segment.get_length(), 68);
174	}
175
176	#[test]
177	fn line_segment_case_new_with_color() {
178		let line_segment = LineSegment::new_with_color("Árvíztűrő tükörfúrógép", DisplayColor::IndicatorColor);
179
180		assert_eq!(line_segment.get_color(), DisplayColor::IndicatorColor);
181		assert!(!line_segment.is_dimmed());
182		assert!(!line_segment.is_underlined());
183		assert!(!line_segment.is_reversed());
184		assert_eq!(line_segment.get_length(), 22);
185	}
186
187	#[test]
188	fn line_segment_case_new_with_color_and_style_all_styles_enabled() {
189		let line_segment = LineSegment::new_with_color_and_style(
190			"Sævör grét áðan því úlpan var ónýt",
191			DisplayColor::IndicatorColor,
192			true,
193			true,
194			true,
195		);
196
197		assert_eq!(line_segment.get_color(), DisplayColor::IndicatorColor);
198		assert!(line_segment.is_dimmed());
199		assert!(line_segment.is_underlined());
200		assert!(line_segment.is_reversed());
201		assert_eq!(line_segment.get_length(), 34);
202	}
203
204	#[test]
205	fn line_segment_case_new_with_color_and_style_all_styles_disabled() {
206		let line_segment = LineSegment::new_with_color_and_style(
207			"? דג סקרן שט בים מאוכזב ולפתע מצא לו חברה איך הקליטה",
208			DisplayColor::IndicatorColor,
209			false,
210			false,
211			false,
212		);
213
214		assert_eq!(line_segment.get_color(), DisplayColor::IndicatorColor);
215		assert!(!line_segment.is_dimmed());
216		assert!(!line_segment.is_underlined());
217		assert!(!line_segment.is_reversed());
218		assert_eq!(line_segment.get_length(), 52);
219	}
220
221	#[test]
222	fn line_segment_case_new_with_color_and_style_all_styles_dimmed() {
223		let line_segment =
224			LineSegment::new_with_color_and_style("Test String", DisplayColor::IndicatorColor, true, false, false);
225
226		assert!(line_segment.is_dimmed());
227		assert!(!line_segment.is_underlined());
228		assert!(!line_segment.is_reversed());
229	}
230
231	#[test]
232	fn line_segment_case_new_with_color_and_style_all_styles_underlined() {
233		let line_segment =
234			LineSegment::new_with_color_and_style("Test String", DisplayColor::IndicatorColor, false, true, false);
235
236		assert!(!line_segment.is_dimmed());
237		assert!(line_segment.is_underlined());
238		assert!(!line_segment.is_reversed());
239	}
240
241	#[test]
242	fn line_segment_case_new_with_color_and_style_all_styles_reversed() {
243		let line_segment =
244			LineSegment::new_with_color_and_style("Test String", DisplayColor::IndicatorColor, false, false, true);
245
246		assert!(!line_segment.is_dimmed());
247		assert!(!line_segment.is_underlined());
248		assert!(line_segment.is_reversed());
249	}
250
251	#[test]
252	fn line_segment_case_get_content() {
253		let line_segment = LineSegment::new("1234567890");
254
255		assert_eq!(line_segment.get_content(), "1234567890");
256	}
257
258	#[test]
259	fn line_segment_case_get_partial_segment_full_segment_exact_fit() {
260		let line_segment = LineSegment::new("1234567890");
261
262		let partial = line_segment.get_partial_segment(0, 10);
263
264		assert_eq!(partial.get_content(), "1234567890");
265		assert_eq!(partial.get_length(), 10);
266	}
267
268	#[test]
269	fn line_segment_case_get_partial_segment_full_segment_width_one_over() {
270		let line_segment = LineSegment::new("1234567890");
271
272		let partial = line_segment.get_partial_segment(0, 11);
273
274		assert_eq!(partial.get_content(), "1234567890");
275		assert_eq!(partial.get_length(), 10);
276	}
277
278	#[test]
279	fn line_segment_case_get_partial_segment_partial_segment_width_one_less() {
280		let line_segment = LineSegment::new("1234567890");
281
282		let partial = line_segment.get_partial_segment(0, 9);
283
284		assert_eq!(partial.get_content(), "123456789");
285		assert_eq!(partial.get_length(), 9);
286	}
287
288	#[test]
289	fn line_segment_case_get_partial_segment_partial_segment_width_left_one() {
290		let line_segment = LineSegment::new("1234567890");
291
292		let partial = line_segment.get_partial_segment(1, 9);
293
294		assert_eq!(partial.get_content(), "234567890");
295		assert_eq!(partial.get_length(), 9);
296	}
297
298	#[test]
299	fn line_segment_case_get_partial_segment_partial_segment_width_left_middle() {
300		let line_segment = LineSegment::new("1234567890");
301
302		let partial = line_segment.get_partial_segment(5, 5);
303
304		assert_eq!(partial.get_content(), "67890");
305		assert_eq!(partial.get_length(), 5);
306	}
307
308	#[test]
309	fn line_segment_case_get_partial_segment_partial_segment_width_left_middle_and_extra_max_width() {
310		let line_segment = LineSegment::new("1234567890");
311
312		let partial = line_segment.get_partial_segment(5, 6);
313
314		assert_eq!(partial.get_content(), "67890");
315		assert_eq!(partial.get_length(), 5);
316	}
317
318	#[test]
319	fn line_segment_case_get_partial_segment_partial_segment_width_one_left_in_segment() {
320		let line_segment = LineSegment::new("1234567890");
321
322		let partial = line_segment.get_partial_segment(9, 1);
323
324		assert_eq!(partial.get_content(), "0");
325		assert_eq!(partial.get_length(), 1);
326	}
327
328	#[test]
329	fn line_segment_case_get_partial_segment_partial_segment_width_one_left_in_segment_with_extra_max_width() {
330		let line_segment = LineSegment::new("1234567890");
331
332		let partial = line_segment.get_partial_segment(9, 2);
333
334		assert_eq!(partial.get_content(), "0");
335		assert_eq!(partial.get_length(), 1);
336	}
337
338	#[test]
339	fn line_segment_case_get_partial_segment_partial_segment_one_max_width() {
340		let line_segment = LineSegment::new("1234567890");
341
342		let partial = line_segment.get_partial_segment(0, 1);
343
344		assert_eq!(partial.get_content(), "1");
345		assert_eq!(partial.get_length(), 1);
346	}
347
348	#[test]
349	fn line_segment_case_get_partial_segment_partial_segment_two_max_width() {
350		let line_segment = LineSegment::new("1234567890");
351
352		let partial = line_segment.get_partial_segment(0, 2);
353
354		assert_eq!(partial.get_content(), "12");
355		assert_eq!(partial.get_length(), 2);
356	}
357
358	#[test]
359	fn line_segment_case_get_partial_segment_partial_segment_no_max_width() {
360		let line_segment = LineSegment::new("1234567890");
361
362		let partial = line_segment.get_partial_segment(0, 0);
363
364		assert_eq!(partial.get_content(), "");
365		assert_eq!(partial.get_length(), 0);
366	}
367
368	#[test]
369	fn line_segment_case_get_partial_segment_partial_segment_left_at_segment_length() {
370		let line_segment = LineSegment::new("1234567890");
371
372		let partial = line_segment.get_partial_segment(10, 1);
373
374		assert_eq!(partial.get_content(), "");
375		assert_eq!(partial.get_length(), 0);
376	}
377
378	#[test]
379	fn line_segment_case_get_partial_segment_partial_segment_left_after_segment_length() {
380		let line_segment = LineSegment::new("1234567890");
381
382		let partial = line_segment.get_partial_segment(11, 1);
383
384		assert_eq!(partial.get_content(), "");
385		assert_eq!(partial.get_length(), 0);
386	}
387
388	#[test]
389	fn line_segment_case_get_partial_segment_partial_segment_left_with_emoji() {
390		let line_segment = LineSegment::new("1😊2😊3😊4567890");
391
392		let partial = line_segment.get_partial_segment(0, 10);
393
394		assert_eq!(partial.get_content(), "1😊2😊3😊4");
395		assert_eq!(partial.get_length(), 10);
396	}
397
398	#[test]
399	fn line_segment_case_get_partial_segment_partial_segment_left_with_emoji_split_at_length() {
400		let line_segment = LineSegment::new("123456789😊");
401
402		let partial = line_segment.get_partial_segment(0, 10);
403
404		assert_eq!(partial.get_content(), "123456789");
405		assert_eq!(partial.get_length(), 9);
406	}
407}