1use unicode_width::UnicodeWidthChar;
7
8pub const DEFAULT_TAB_WIDTH: usize = 4;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum WrapMode {
14 None,
16 #[default]
18 Char,
19 Word,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum WrapIndent {
26 #[default]
28 None,
29 SameAsLineIndent,
32 FixedCells(usize),
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct WrapPoint {
39 pub char_index: usize,
41 pub byte_offset: usize,
43}
44
45#[derive(Debug, Clone)]
47pub struct VisualLineInfo {
48 pub visual_line_count: usize,
50 pub wrap_points: Vec<WrapPoint>,
52}
53
54impl VisualLineInfo {
55 pub fn new() -> Self {
57 Self {
58 visual_line_count: 1,
59 wrap_points: Vec::new(),
60 }
61 }
62
63 pub fn from_text(text: &str, viewport_width: usize) -> Self {
65 let wrap_points = calculate_wrap_points(text, viewport_width);
66 let visual_line_count = wrap_points.len() + 1;
67
68 Self {
69 visual_line_count,
70 wrap_points,
71 }
72 }
73
74 pub fn from_text_with_tab_width(text: &str, viewport_width: usize, tab_width: usize) -> Self {
76 let wrap_points = calculate_wrap_points_with_tab_width(text, viewport_width, tab_width);
77 let visual_line_count = wrap_points.len() + 1;
78
79 Self {
80 visual_line_count,
81 wrap_points,
82 }
83 }
84
85 pub fn from_text_with_options(
87 text: &str,
88 viewport_width: usize,
89 tab_width: usize,
90 wrap_mode: WrapMode,
91 ) -> Self {
92 Self::from_text_with_layout_options(
93 text,
94 viewport_width,
95 tab_width,
96 wrap_mode,
97 WrapIndent::None,
98 )
99 }
100
101 pub fn from_text_with_layout_options(
103 text: &str,
104 viewport_width: usize,
105 tab_width: usize,
106 wrap_mode: WrapMode,
107 wrap_indent: WrapIndent,
108 ) -> Self {
109 let wrap_points = calculate_wrap_points_with_tab_width_mode_and_indent(
110 text,
111 viewport_width,
112 tab_width,
113 wrap_mode,
114 wrap_indent,
115 );
116 let visual_line_count = wrap_points.len() + 1;
117
118 Self {
119 visual_line_count,
120 wrap_points,
121 }
122 }
123}
124
125impl Default for VisualLineInfo {
126 fn default() -> Self {
127 Self::new()
128 }
129}
130
131pub fn char_width(ch: char) -> usize {
138 UnicodeWidthChar::width(ch).unwrap_or(1)
140}
141
142pub fn cell_width_at(ch: char, cell_offset_in_line: usize, tab_width: usize) -> usize {
148 if ch == '\t' {
149 let tab_width = tab_width.max(1);
150 let rem = cell_offset_in_line % tab_width;
151 tab_width - rem
152 } else {
153 char_width(ch)
154 }
155}
156
157pub fn str_width(s: &str) -> usize {
159 s.chars().map(char_width).sum()
160}
161
162pub fn str_width_with_tab_width(s: &str, tab_width: usize) -> usize {
164 let mut x = 0usize;
165 for ch in s.chars() {
166 x = x.saturating_add(cell_width_at(ch, x, tab_width));
167 }
168 x
169}
170
171pub fn visual_x_for_column(line: &str, column: usize, tab_width: usize) -> usize {
176 let mut x = 0usize;
177 for ch in line.chars().take(column) {
178 x = x.saturating_add(cell_width_at(ch, x, tab_width));
179 }
180 x
181}
182
183fn leading_whitespace_prefix_slice(line: &str) -> &str {
184 let bytes = line.as_bytes();
185 let mut end = 0usize;
186 while end < bytes.len() {
187 match bytes[end] {
188 b' ' | b'\t' => end += 1,
189 _ => break,
190 }
191 }
192 &line[..end]
193}
194
195pub(crate) fn wrap_indent_cells_for_line_text(
196 line_text: &str,
197 wrap_indent: WrapIndent,
198 viewport_width: usize,
199 tab_width: usize,
200) -> usize {
201 if viewport_width <= 1 {
202 return 0;
203 }
204
205 let raw = match wrap_indent {
206 WrapIndent::None => 0,
207 WrapIndent::FixedCells(n) => n,
208 WrapIndent::SameAsLineIndent => {
209 let prefix = leading_whitespace_prefix_slice(line_text);
210 str_width_with_tab_width(prefix, tab_width)
211 }
212 };
213
214 raw.min(viewport_width.saturating_sub(1))
215}
216
217pub fn calculate_wrap_points(text: &str, viewport_width: usize) -> Vec<WrapPoint> {
221 calculate_wrap_points_with_tab_width(text, viewport_width, DEFAULT_TAB_WIDTH)
222}
223
224pub fn calculate_wrap_points_with_tab_width(
226 text: &str,
227 viewport_width: usize,
228 tab_width: usize,
229) -> Vec<WrapPoint> {
230 calculate_wrap_points_with_tab_width_and_mode(text, viewport_width, tab_width, WrapMode::Char)
231}
232
233pub fn calculate_wrap_points_with_tab_width_and_mode(
235 text: &str,
236 viewport_width: usize,
237 tab_width: usize,
238 wrap_mode: WrapMode,
239) -> Vec<WrapPoint> {
240 calculate_wrap_points_with_tab_width_mode_and_indent(
241 text,
242 viewport_width,
243 tab_width,
244 wrap_mode,
245 WrapIndent::None,
246 )
247}
248
249pub fn calculate_wrap_points_with_tab_width_mode_and_indent(
251 text: &str,
252 viewport_width: usize,
253 tab_width: usize,
254 wrap_mode: WrapMode,
255 wrap_indent: WrapIndent,
256) -> Vec<WrapPoint> {
257 if viewport_width == 0 {
258 return Vec::new();
259 }
260
261 match wrap_mode {
262 WrapMode::None => Vec::new(),
263 WrapMode::Char => {
264 let indent =
265 wrap_indent_cells_for_line_text(text, wrap_indent, viewport_width, tab_width);
266 calculate_wrap_points_char_with_tab_width(text, viewport_width, tab_width, indent)
267 }
268 WrapMode::Word => {
269 let indent =
270 wrap_indent_cells_for_line_text(text, wrap_indent, viewport_width, tab_width);
271 calculate_wrap_points_word_with_tab_width(text, viewport_width, tab_width, indent)
272 }
273 }
274}
275
276fn calculate_wrap_points_char_with_tab_width(
277 text: &str,
278 viewport_width: usize,
279 tab_width: usize,
280 wrap_indent_cells: usize,
281) -> Vec<WrapPoint> {
282 let mut wrap_points = Vec::new();
283 let mut x_in_segment = 0usize;
284 let mut x_in_line = 0usize;
285
286 for (char_index, (byte_offset, ch)) in text.char_indices().enumerate() {
287 let ch_width = cell_width_at(ch, x_in_line, tab_width);
288
289 if x_in_segment + ch_width > viewport_width {
291 wrap_points.push(WrapPoint {
294 char_index,
295 byte_offset,
296 });
297 x_in_segment = wrap_indent_cells;
298 } else {
299 }
301
302 x_in_segment = x_in_segment.saturating_add(ch_width);
303 x_in_line = x_in_line.saturating_add(ch_width);
304
305 if x_in_segment == viewport_width {
307 if byte_offset + ch.len_utf8() < text.len() {
309 wrap_points.push(WrapPoint {
310 char_index: char_index + 1,
311 byte_offset: byte_offset + ch.len_utf8(),
312 });
313 x_in_segment = wrap_indent_cells;
314 }
315 }
316 }
317
318 wrap_points
319}
320
321fn calculate_wrap_points_word_with_tab_width(
322 text: &str,
323 viewport_width: usize,
324 tab_width: usize,
325 wrap_indent_cells: usize,
326) -> Vec<WrapPoint> {
327 let mut wrap_points = Vec::new();
328
329 let mut segment_start_char = 0usize;
330 let mut segment_start_x_in_line = 0usize;
331 let mut last_break: Option<(usize, usize, usize)> = None; let mut x_in_line = 0usize;
334
335 for (char_index, (byte_offset, ch)) in text.char_indices().enumerate() {
336 let ch_width = cell_width_at(ch, x_in_line, tab_width);
337
338 loop {
339 let segment_indent = if segment_start_char == 0 {
340 0
341 } else {
342 wrap_indent_cells
343 };
344 let x_in_segment = x_in_line
345 .saturating_sub(segment_start_x_in_line)
346 .saturating_add(segment_indent);
347 if x_in_segment.saturating_add(ch_width) <= viewport_width {
348 break;
349 }
350
351 if let Some((break_char, break_byte, break_x)) = last_break
352 && break_char > segment_start_char
353 {
354 wrap_points.push(WrapPoint {
355 char_index: break_char,
356 byte_offset: break_byte,
357 });
358 segment_start_char = break_char;
359 segment_start_x_in_line = break_x;
360 last_break = None;
361 continue;
362 }
363
364 wrap_points.push(WrapPoint {
366 char_index,
367 byte_offset,
368 });
369 segment_start_char = char_index;
370 segment_start_x_in_line = x_in_line;
371 last_break = None;
372 break;
373 }
374
375 x_in_line = x_in_line.saturating_add(ch_width);
376
377 if ch.is_whitespace() {
378 last_break = Some((char_index + 1, byte_offset + ch.len_utf8(), x_in_line));
379 }
380 }
381
382 wrap_points
383}
384
385pub struct LayoutEngine {
387 viewport_width: usize,
389 tab_width: usize,
391 wrap_mode: WrapMode,
393 wrap_indent: WrapIndent,
395 line_layouts: Vec<VisualLineInfo>,
397 line_texts: Vec<String>,
399}
400
401impl LayoutEngine {
402 pub fn new(viewport_width: usize) -> Self {
404 Self {
405 viewport_width,
406 tab_width: DEFAULT_TAB_WIDTH,
407 wrap_mode: WrapMode::Char,
408 wrap_indent: WrapIndent::None,
409 line_layouts: Vec::new(),
410 line_texts: Vec::new(),
411 }
412 }
413
414 pub fn set_viewport_width(&mut self, width: usize) {
416 if self.viewport_width != width {
417 self.viewport_width = width;
418 self.recalculate_all();
419 }
420 }
421
422 pub fn viewport_width(&self) -> usize {
424 self.viewport_width
425 }
426
427 pub fn wrap_mode(&self) -> WrapMode {
429 self.wrap_mode
430 }
431
432 pub fn set_wrap_mode(&mut self, wrap_mode: WrapMode) {
436 if self.wrap_mode != wrap_mode {
437 self.wrap_mode = wrap_mode;
438 self.recalculate_all();
439 }
440 }
441
442 pub fn wrap_indent(&self) -> WrapIndent {
444 self.wrap_indent
445 }
446
447 pub fn set_wrap_indent(&mut self, wrap_indent: WrapIndent) {
451 if self.wrap_indent != wrap_indent {
452 self.wrap_indent = wrap_indent;
453 self.recalculate_all();
454 }
455 }
456
457 pub fn tab_width(&self) -> usize {
459 self.tab_width
460 }
461
462 pub fn set_tab_width(&mut self, tab_width: usize) {
466 let tab_width = tab_width.max(1);
467 if self.tab_width != tab_width {
468 self.tab_width = tab_width;
469 self.recalculate_all();
470 }
471 }
472
473 pub fn from_lines(&mut self, lines: &[&str]) {
475 self.line_layouts.clear();
476 self.line_texts.clear();
477 for line in lines {
478 self.line_texts.push((*line).to_string());
479 self.line_layouts
480 .push(VisualLineInfo::from_text_with_layout_options(
481 line,
482 self.viewport_width,
483 self.tab_width,
484 self.wrap_mode,
485 self.wrap_indent,
486 ));
487 }
488 }
489
490 pub fn add_line(&mut self, text: &str) {
492 self.line_texts.push(text.to_string());
493 self.line_layouts
494 .push(VisualLineInfo::from_text_with_layout_options(
495 text,
496 self.viewport_width,
497 self.tab_width,
498 self.wrap_mode,
499 self.wrap_indent,
500 ));
501 }
502
503 pub fn update_line(&mut self, line_index: usize, text: &str) {
505 if line_index < self.line_layouts.len() {
506 self.line_texts[line_index] = text.to_string();
507 self.line_layouts[line_index] = VisualLineInfo::from_text_with_layout_options(
508 text,
509 self.viewport_width,
510 self.tab_width,
511 self.wrap_mode,
512 self.wrap_indent,
513 );
514 }
515 }
516
517 pub fn insert_line(&mut self, line_index: usize, text: &str) {
519 let pos = line_index.min(self.line_layouts.len());
520 self.line_texts.insert(pos, text.to_string());
521 self.line_layouts.insert(
522 pos,
523 VisualLineInfo::from_text_with_layout_options(
524 text,
525 self.viewport_width,
526 self.tab_width,
527 self.wrap_mode,
528 self.wrap_indent,
529 ),
530 );
531 }
532
533 pub fn delete_line(&mut self, line_index: usize) {
535 if line_index < self.line_layouts.len() {
536 self.line_texts.remove(line_index);
537 self.line_layouts.remove(line_index);
538 }
539 }
540
541 pub fn get_line_layout(&self, line_index: usize) -> Option<&VisualLineInfo> {
543 self.line_layouts.get(line_index)
544 }
545
546 pub fn logical_line_count(&self) -> usize {
548 self.line_layouts.len()
549 }
550
551 pub fn visual_line_count(&self) -> usize {
553 self.line_layouts.iter().map(|l| l.visual_line_count).sum()
554 }
555
556 pub fn logical_to_visual_line(&self, logical_line: usize) -> usize {
560 self.line_layouts
561 .iter()
562 .take(logical_line)
563 .map(|l| l.visual_line_count)
564 .sum()
565 }
566
567 pub fn visual_to_logical_line(&self, visual_line: usize) -> (usize, usize) {
571 let mut cumulative_visual = 0;
572
573 for (logical_idx, layout) in self.line_layouts.iter().enumerate() {
574 if cumulative_visual + layout.visual_line_count > visual_line {
575 let visual_offset = visual_line - cumulative_visual;
576 return (logical_idx, visual_offset);
577 }
578 cumulative_visual += layout.visual_line_count;
579 }
580
581 let last_line = self.line_layouts.len().saturating_sub(1);
583 let last_visual_offset = self
584 .line_layouts
585 .last()
586 .map(|l| l.visual_line_count.saturating_sub(1))
587 .unwrap_or(0);
588 (last_line, last_visual_offset)
589 }
590
591 fn recalculate_all(&mut self) {
593 if self.line_texts.len() != self.line_layouts.len() {
594 self.line_layouts.clear();
596 for line in &self.line_texts {
597 self.line_layouts
598 .push(VisualLineInfo::from_text_with_layout_options(
599 line,
600 self.viewport_width,
601 self.tab_width,
602 self.wrap_mode,
603 self.wrap_indent,
604 ));
605 }
606 return;
607 }
608
609 for (layout, line_text) in self.line_layouts.iter_mut().zip(self.line_texts.iter()) {
610 *layout = VisualLineInfo::from_text_with_layout_options(
611 line_text,
612 self.viewport_width,
613 self.tab_width,
614 self.wrap_mode,
615 self.wrap_indent,
616 );
617 }
618 }
619
620 pub fn clear(&mut self) {
622 self.line_layouts.clear();
623 self.line_texts.clear();
624 }
625
626 pub fn logical_position_to_visual(
635 &self,
636 logical_line: usize,
637 column: usize,
638 ) -> Option<(usize, usize)> {
639 let layout = self.get_line_layout(logical_line)?;
640 let line_text = self.line_texts.get(logical_line)?;
641
642 let line_char_len = line_text.chars().count();
643 let column = column.min(line_char_len);
644
645 let mut wrapped_offset = 0usize;
647 let mut segment_start_col = 0usize;
648
649 for wrap_point in &layout.wrap_points {
651 if column >= wrap_point.char_index {
652 wrapped_offset += 1;
653 segment_start_col = wrap_point.char_index;
654 } else {
655 break;
656 }
657 }
658
659 let seg_start_x_in_line = visual_x_for_column(line_text, segment_start_col, self.tab_width);
661 let mut x_in_line = seg_start_x_in_line;
662 let mut x_in_segment = 0usize;
663 for ch in line_text
664 .chars()
665 .skip(segment_start_col)
666 .take(column.saturating_sub(segment_start_col))
667 {
668 let w = cell_width_at(ch, x_in_line, self.tab_width);
669 x_in_line = x_in_line.saturating_add(w);
670 x_in_segment = x_in_segment.saturating_add(w);
671 }
672
673 let indent = if wrapped_offset == 0 {
674 0
675 } else {
676 wrap_indent_cells_for_line_text(
677 line_text,
678 self.wrap_indent,
679 self.viewport_width,
680 self.tab_width,
681 )
682 };
683
684 let visual_row = self.logical_to_visual_line(logical_line) + wrapped_offset;
685 Some((visual_row, indent.saturating_add(x_in_segment)))
686 }
687
688 pub fn logical_position_to_visual_allow_virtual(
694 &self,
695 logical_line: usize,
696 column: usize,
697 ) -> Option<(usize, usize)> {
698 let layout = self.get_line_layout(logical_line)?;
699 let line_text = self.line_texts.get(logical_line)?;
700
701 let line_char_len = line_text.chars().count();
702 let clamped_column = column.min(line_char_len);
703
704 let mut wrapped_offset = 0usize;
705 let mut segment_start_col = 0usize;
706 for wrap_point in &layout.wrap_points {
707 if clamped_column >= wrap_point.char_index {
708 wrapped_offset += 1;
709 segment_start_col = wrap_point.char_index;
710 } else {
711 break;
712 }
713 }
714
715 let seg_start_x_in_line = visual_x_for_column(line_text, segment_start_col, self.tab_width);
716 let mut x_in_line = seg_start_x_in_line;
717 let mut x_in_segment = 0usize;
718 for ch in line_text
719 .chars()
720 .skip(segment_start_col)
721 .take(clamped_column.saturating_sub(segment_start_col))
722 {
723 let w = cell_width_at(ch, x_in_line, self.tab_width);
724 x_in_line = x_in_line.saturating_add(w);
725 x_in_segment = x_in_segment.saturating_add(w);
726 }
727
728 let indent = if wrapped_offset == 0 {
729 0
730 } else {
731 wrap_indent_cells_for_line_text(
732 line_text,
733 self.wrap_indent,
734 self.viewport_width,
735 self.tab_width,
736 )
737 };
738
739 let x_in_segment = x_in_segment + column.saturating_sub(line_char_len);
740 let visual_row = self.logical_to_visual_line(logical_line) + wrapped_offset;
741 Some((visual_row, indent.saturating_add(x_in_segment)))
742 }
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748
749 #[test]
750 fn test_char_width() {
751 assert_eq!(char_width('a'), 1);
753 assert_eq!(char_width('A'), 1);
754 assert_eq!(char_width(' '), 1);
755
756 assert_eq!(char_width('你'), 2);
758 assert_eq!(char_width('好'), 2);
759 assert_eq!(char_width('世'), 2);
760 assert_eq!(char_width('界'), 2);
761
762 assert_eq!(char_width('👋'), 2);
764 assert_eq!(char_width('🌍'), 2);
765 assert_eq!(char_width('🦀'), 2);
766 }
767
768 #[test]
769 fn test_str_width() {
770 assert_eq!(str_width("hello"), 5);
771 assert_eq!(str_width("你好"), 4); assert_eq!(str_width("hello你好"), 9); assert_eq!(str_width("👋🌍"), 4); }
775
776 #[test]
777 fn test_tab_width_expansion() {
778 assert_eq!(cell_width_at('\t', 0, 4), 4);
780 assert_eq!(cell_width_at('\t', 1, 4), 3);
781 assert_eq!(cell_width_at('\t', 2, 4), 2);
782 assert_eq!(cell_width_at('\t', 3, 4), 1);
783 assert_eq!(cell_width_at('\t', 4, 4), 4);
784
785 assert_eq!(str_width_with_tab_width("\t", 4), 4);
786 assert_eq!(str_width_with_tab_width("a\t", 4), 4); assert_eq!(str_width_with_tab_width("ab\t", 4), 4); assert_eq!(str_width_with_tab_width("abc\t", 4), 4); assert_eq!(str_width_with_tab_width("abcd\t", 4), 8); }
791
792 #[test]
793 fn test_calculate_wrap_points_simple() {
794 let text = "hello world";
796 let wraps = calculate_wrap_points(text, 10);
797
798 assert!(!wraps.is_empty());
801 }
802
803 #[test]
804 fn test_calculate_wrap_points_exact_fit() {
805 let text = "1234567890";
807 let wraps = calculate_wrap_points(text, 10);
808
809 assert_eq!(wraps.len(), 0);
811 }
812
813 #[test]
814 fn test_calculate_wrap_points_one_over() {
815 let text = "12345678901";
817 let wraps = calculate_wrap_points(text, 10);
818
819 assert_eq!(wraps.len(), 1);
821 assert_eq!(wraps[0].char_index, 10);
822 }
823
824 #[test]
825 fn test_calculate_wrap_points_cjk() {
826 let text = "你好世界测";
828 let wraps = calculate_wrap_points(text, 10);
829
830 assert_eq!(wraps.len(), 0);
832 }
833
834 #[test]
835 fn test_calculate_wrap_points_cjk_overflow() {
836 let text = "你好世界测试";
838 let wraps = calculate_wrap_points(text, 10);
839
840 assert_eq!(wraps.len(), 1);
842 assert_eq!(wraps[0].char_index, 5);
843 }
844
845 #[test]
846 fn test_wrap_mode_none_disables_wrapping() {
847 let mut engine = LayoutEngine::new(5);
848 engine.set_wrap_mode(WrapMode::None);
849 engine.from_lines(&["abcdefghij"]);
850
851 assert_eq!(engine.visual_line_count(), 1);
852 let layout = engine.get_line_layout(0).expect("layout");
853 assert_eq!(layout.visual_line_count, 1);
854 assert!(layout.wrap_points.is_empty());
855 }
856
857 #[test]
858 fn test_word_wrap_prefers_whitespace_when_possible() {
859 let text = "hello world";
862
863 let wraps = calculate_wrap_points_with_tab_width_and_mode(
864 text,
865 7,
866 DEFAULT_TAB_WIDTH,
867 WrapMode::Word,
868 );
869
870 assert_eq!(wraps.len(), 1);
871 assert_eq!(wraps[0].char_index, 6);
872 }
873
874 #[test]
875 fn test_wrap_indent_same_as_line_indent_reduces_continuation_width() {
876 let text = " abcdefgh";
877 let wraps = calculate_wrap_points_with_tab_width_mode_and_indent(
878 text,
879 6,
880 DEFAULT_TAB_WIDTH,
881 WrapMode::Char,
882 WrapIndent::SameAsLineIndent,
883 );
884
885 let indices: Vec<usize> = wraps.iter().map(|wp| wp.char_index).collect();
886 assert_eq!(indices, vec![6, 8, 10]);
887 }
888
889 #[test]
890 fn test_wrap_double_width_char() {
891 let text = "Hello你";
894 let wraps = calculate_wrap_points(text, 6);
895
896 assert_eq!(wraps.len(), 1);
899 assert_eq!(wraps[0].char_index, 5); }
901
902 #[test]
903 fn test_visual_line_info() {
904 let info = VisualLineInfo::from_text("1234567890abc", 10);
905 assert_eq!(info.visual_line_count, 2); assert_eq!(info.wrap_points.len(), 1);
907 }
908
909 #[test]
910 fn test_layout_engine_basic() {
911 let mut engine = LayoutEngine::new(10);
912 engine.add_line("hello");
913 engine.add_line("1234567890abc");
914
915 assert_eq!(engine.logical_line_count(), 2);
916 assert_eq!(engine.visual_line_count(), 3); }
918
919 #[test]
920 fn test_layout_engine_viewport_change() {
921 let mut engine = LayoutEngine::new(20);
922 engine.from_lines(&["hello world", "rust programming"]);
923
924 let initial_visual = engine.visual_line_count();
925 assert_eq!(initial_visual, 2); engine.set_viewport_width(5);
929 engine.from_lines(&["hello world", "rust programming"]);
931
932 let new_visual = engine.visual_line_count();
933 assert!(new_visual > initial_visual); }
935
936 #[test]
937 fn test_logical_to_visual() {
938 let mut engine = LayoutEngine::new(10);
939 engine.from_lines(&["12345", "1234567890abc", "hello"]);
940
941 assert_eq!(engine.logical_to_visual_line(0), 0);
943
944 assert_eq!(engine.logical_to_visual_line(1), 1);
946
947 assert_eq!(engine.logical_to_visual_line(2), 3);
949 }
950
951 #[test]
952 fn test_visual_to_logical() {
953 let mut engine = LayoutEngine::new(10);
954 engine.from_lines(&["12345", "1234567890abc", "hello"]);
955
956 assert_eq!(engine.visual_to_logical_line(0), (0, 0));
958
959 assert_eq!(engine.visual_to_logical_line(1), (1, 0));
961
962 assert_eq!(engine.visual_to_logical_line(2), (1, 1));
964
965 assert_eq!(engine.visual_to_logical_line(3), (2, 0));
967 }
968}