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}
398
399impl LayoutEngine {
400 pub fn new(viewport_width: usize) -> Self {
402 Self {
403 viewport_width,
404 tab_width: DEFAULT_TAB_WIDTH,
405 wrap_mode: WrapMode::Char,
406 wrap_indent: WrapIndent::None,
407 line_layouts: Vec::new(),
408 }
409 }
410
411 pub fn set_viewport_width(&mut self, width: usize) {
415 if self.viewport_width != width {
416 self.viewport_width = width;
417 }
418 }
419
420 pub fn viewport_width(&self) -> usize {
422 self.viewport_width
423 }
424
425 pub fn wrap_mode(&self) -> WrapMode {
427 self.wrap_mode
428 }
429
430 pub fn set_wrap_mode(&mut self, wrap_mode: WrapMode) {
434 if self.wrap_mode != wrap_mode {
435 self.wrap_mode = wrap_mode;
436 }
437 }
438
439 pub fn wrap_indent(&self) -> WrapIndent {
441 self.wrap_indent
442 }
443
444 pub fn set_wrap_indent(&mut self, wrap_indent: WrapIndent) {
448 if self.wrap_indent != wrap_indent {
449 self.wrap_indent = wrap_indent;
450 }
451 }
452
453 pub fn tab_width(&self) -> usize {
455 self.tab_width
456 }
457
458 pub fn set_tab_width(&mut self, tab_width: usize) {
462 let tab_width = tab_width.max(1);
463 if self.tab_width != tab_width {
464 self.tab_width = tab_width;
465 }
466 }
467
468 pub fn from_lines(&mut self, lines: &[&str]) {
470 self.recalculate_all_from_lines(lines.iter().copied());
471 }
472
473 pub fn recalculate_all_from_lines<I, S>(&mut self, lines: I)
475 where
476 I: IntoIterator<Item = S>,
477 S: AsRef<str>,
478 {
479 self.line_layouts.clear();
480 for line in lines {
481 let line = line.as_ref();
482 self.line_layouts
483 .push(VisualLineInfo::from_text_with_layout_options(
484 line,
485 self.viewport_width,
486 self.tab_width,
487 self.wrap_mode,
488 self.wrap_indent,
489 ));
490 }
491 }
492
493 pub fn add_line(&mut self, text: &str) {
495 self.line_layouts
496 .push(VisualLineInfo::from_text_with_layout_options(
497 text,
498 self.viewport_width,
499 self.tab_width,
500 self.wrap_mode,
501 self.wrap_indent,
502 ));
503 }
504
505 pub fn update_line(&mut self, line_index: usize, text: &str) {
507 if line_index < self.line_layouts.len() {
508 self.line_layouts[line_index] = VisualLineInfo::from_text_with_layout_options(
509 text,
510 self.viewport_width,
511 self.tab_width,
512 self.wrap_mode,
513 self.wrap_indent,
514 );
515 }
516 }
517
518 pub fn insert_line(&mut self, line_index: usize, text: &str) {
520 let pos = line_index.min(self.line_layouts.len());
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_layouts.remove(line_index);
537 }
538 }
539
540 pub fn get_line_layout(&self, line_index: usize) -> Option<&VisualLineInfo> {
542 self.line_layouts.get(line_index)
543 }
544
545 pub fn logical_line_count(&self) -> usize {
547 self.line_layouts.len()
548 }
549
550 pub fn visual_line_count(&self) -> usize {
552 self.line_layouts.iter().map(|l| l.visual_line_count).sum()
553 }
554
555 pub fn logical_to_visual_line(&self, logical_line: usize) -> usize {
559 self.line_layouts
560 .iter()
561 .take(logical_line)
562 .map(|l| l.visual_line_count)
563 .sum()
564 }
565
566 pub fn visual_to_logical_line(&self, visual_line: usize) -> (usize, usize) {
570 let mut cumulative_visual = 0;
571
572 for (logical_idx, layout) in self.line_layouts.iter().enumerate() {
573 if cumulative_visual + layout.visual_line_count > visual_line {
574 let visual_offset = visual_line - cumulative_visual;
575 return (logical_idx, visual_offset);
576 }
577 cumulative_visual += layout.visual_line_count;
578 }
579
580 let last_line = self.line_layouts.len().saturating_sub(1);
582 let last_visual_offset = self
583 .line_layouts
584 .last()
585 .map(|l| l.visual_line_count.saturating_sub(1))
586 .unwrap_or(0);
587 (last_line, last_visual_offset)
588 }
589
590 pub fn clear(&mut self) {
592 self.line_layouts.clear();
593 }
594
595 pub fn logical_position_to_visual(
605 &self,
606 logical_line: usize,
607 column: usize,
608 line_text: &str,
609 ) -> Option<(usize, usize)> {
610 let layout = self.get_line_layout(logical_line)?;
611
612 let line_char_len = line_text.chars().count();
613 let column = column.min(line_char_len);
614
615 let mut wrapped_offset = 0usize;
617 let mut segment_start_col = 0usize;
618
619 for wrap_point in &layout.wrap_points {
621 if column >= wrap_point.char_index {
622 wrapped_offset += 1;
623 segment_start_col = wrap_point.char_index;
624 } else {
625 break;
626 }
627 }
628
629 let seg_start_x_in_line = visual_x_for_column(line_text, segment_start_col, self.tab_width);
631 let mut x_in_line = seg_start_x_in_line;
632 let mut x_in_segment = 0usize;
633 for ch in line_text
634 .chars()
635 .skip(segment_start_col)
636 .take(column.saturating_sub(segment_start_col))
637 {
638 let w = cell_width_at(ch, x_in_line, self.tab_width);
639 x_in_line = x_in_line.saturating_add(w);
640 x_in_segment = x_in_segment.saturating_add(w);
641 }
642
643 let indent = if wrapped_offset == 0 {
644 0
645 } else {
646 wrap_indent_cells_for_line_text(
647 line_text,
648 self.wrap_indent,
649 self.viewport_width,
650 self.tab_width,
651 )
652 };
653
654 let visual_row = self.logical_to_visual_line(logical_line) + wrapped_offset;
655 Some((visual_row, indent.saturating_add(x_in_segment)))
656 }
657
658 pub fn logical_position_to_visual_allow_virtual(
665 &self,
666 logical_line: usize,
667 column: usize,
668 line_text: &str,
669 ) -> Option<(usize, usize)> {
670 let layout = self.get_line_layout(logical_line)?;
671
672 let line_char_len = line_text.chars().count();
673 let clamped_column = column.min(line_char_len);
674
675 let mut wrapped_offset = 0usize;
676 let mut segment_start_col = 0usize;
677 for wrap_point in &layout.wrap_points {
678 if clamped_column >= wrap_point.char_index {
679 wrapped_offset += 1;
680 segment_start_col = wrap_point.char_index;
681 } else {
682 break;
683 }
684 }
685
686 let seg_start_x_in_line = visual_x_for_column(line_text, segment_start_col, self.tab_width);
687 let mut x_in_line = seg_start_x_in_line;
688 let mut x_in_segment = 0usize;
689 for ch in line_text
690 .chars()
691 .skip(segment_start_col)
692 .take(clamped_column.saturating_sub(segment_start_col))
693 {
694 let w = cell_width_at(ch, x_in_line, self.tab_width);
695 x_in_line = x_in_line.saturating_add(w);
696 x_in_segment = x_in_segment.saturating_add(w);
697 }
698
699 let indent = if wrapped_offset == 0 {
700 0
701 } else {
702 wrap_indent_cells_for_line_text(
703 line_text,
704 self.wrap_indent,
705 self.viewport_width,
706 self.tab_width,
707 )
708 };
709
710 let x_in_segment = x_in_segment + column.saturating_sub(line_char_len);
711 let visual_row = self.logical_to_visual_line(logical_line) + wrapped_offset;
712 Some((visual_row, indent.saturating_add(x_in_segment)))
713 }
714}
715
716#[cfg(test)]
717mod tests {
718 use super::*;
719
720 #[test]
721 fn test_char_width() {
722 assert_eq!(char_width('a'), 1);
724 assert_eq!(char_width('A'), 1);
725 assert_eq!(char_width(' '), 1);
726
727 assert_eq!(char_width('你'), 2);
729 assert_eq!(char_width('好'), 2);
730 assert_eq!(char_width('世'), 2);
731 assert_eq!(char_width('界'), 2);
732
733 assert_eq!(char_width('👋'), 2);
735 assert_eq!(char_width('🌍'), 2);
736 assert_eq!(char_width('🦀'), 2);
737 }
738
739 #[test]
740 fn test_str_width() {
741 assert_eq!(str_width("hello"), 5);
742 assert_eq!(str_width("你好"), 4); assert_eq!(str_width("hello你好"), 9); assert_eq!(str_width("👋🌍"), 4); }
746
747 #[test]
748 fn test_tab_width_expansion() {
749 assert_eq!(cell_width_at('\t', 0, 4), 4);
751 assert_eq!(cell_width_at('\t', 1, 4), 3);
752 assert_eq!(cell_width_at('\t', 2, 4), 2);
753 assert_eq!(cell_width_at('\t', 3, 4), 1);
754 assert_eq!(cell_width_at('\t', 4, 4), 4);
755
756 assert_eq!(str_width_with_tab_width("\t", 4), 4);
757 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); }
762
763 #[test]
764 fn test_calculate_wrap_points_simple() {
765 let text = "hello world";
767 let wraps = calculate_wrap_points(text, 10);
768
769 assert!(!wraps.is_empty());
772 }
773
774 #[test]
775 fn test_calculate_wrap_points_exact_fit() {
776 let text = "1234567890";
778 let wraps = calculate_wrap_points(text, 10);
779
780 assert_eq!(wraps.len(), 0);
782 }
783
784 #[test]
785 fn test_calculate_wrap_points_one_over() {
786 let text = "12345678901";
788 let wraps = calculate_wrap_points(text, 10);
789
790 assert_eq!(wraps.len(), 1);
792 assert_eq!(wraps[0].char_index, 10);
793 }
794
795 #[test]
796 fn test_calculate_wrap_points_cjk() {
797 let text = "你好世界测";
799 let wraps = calculate_wrap_points(text, 10);
800
801 assert_eq!(wraps.len(), 0);
803 }
804
805 #[test]
806 fn test_calculate_wrap_points_cjk_overflow() {
807 let text = "你好世界测试";
809 let wraps = calculate_wrap_points(text, 10);
810
811 assert_eq!(wraps.len(), 1);
813 assert_eq!(wraps[0].char_index, 5);
814 }
815
816 #[test]
817 fn test_wrap_mode_none_disables_wrapping() {
818 let mut engine = LayoutEngine::new(5);
819 engine.set_wrap_mode(WrapMode::None);
820 engine.from_lines(&["abcdefghij"]);
821
822 assert_eq!(engine.visual_line_count(), 1);
823 let layout = engine.get_line_layout(0).expect("layout");
824 assert_eq!(layout.visual_line_count, 1);
825 assert!(layout.wrap_points.is_empty());
826 }
827
828 #[test]
829 fn test_word_wrap_prefers_whitespace_when_possible() {
830 let text = "hello world";
833
834 let wraps = calculate_wrap_points_with_tab_width_and_mode(
835 text,
836 7,
837 DEFAULT_TAB_WIDTH,
838 WrapMode::Word,
839 );
840
841 assert_eq!(wraps.len(), 1);
842 assert_eq!(wraps[0].char_index, 6);
843 }
844
845 #[test]
846 fn test_wrap_indent_same_as_line_indent_reduces_continuation_width() {
847 let text = " abcdefgh";
848 let wraps = calculate_wrap_points_with_tab_width_mode_and_indent(
849 text,
850 6,
851 DEFAULT_TAB_WIDTH,
852 WrapMode::Char,
853 WrapIndent::SameAsLineIndent,
854 );
855
856 let indices: Vec<usize> = wraps.iter().map(|wp| wp.char_index).collect();
857 assert_eq!(indices, vec![6, 8, 10]);
858 }
859
860 #[test]
861 fn test_wrap_double_width_char() {
862 let text = "Hello你";
865 let wraps = calculate_wrap_points(text, 6);
866
867 assert_eq!(wraps.len(), 1);
870 assert_eq!(wraps[0].char_index, 5); }
872
873 #[test]
874 fn test_visual_line_info() {
875 let info = VisualLineInfo::from_text("1234567890abc", 10);
876 assert_eq!(info.visual_line_count, 2); assert_eq!(info.wrap_points.len(), 1);
878 }
879
880 #[test]
881 fn test_layout_engine_basic() {
882 let mut engine = LayoutEngine::new(10);
883 engine.add_line("hello");
884 engine.add_line("1234567890abc");
885
886 assert_eq!(engine.logical_line_count(), 2);
887 assert_eq!(engine.visual_line_count(), 3); }
889
890 #[test]
891 fn test_layout_engine_viewport_change() {
892 let mut engine = LayoutEngine::new(20);
893 engine.from_lines(&["hello world", "rust programming"]);
894
895 let initial_visual = engine.visual_line_count();
896 assert_eq!(initial_visual, 2); engine.set_viewport_width(5);
900 engine.from_lines(&["hello world", "rust programming"]);
902
903 let new_visual = engine.visual_line_count();
904 assert!(new_visual > initial_visual); }
906
907 #[test]
908 fn test_logical_to_visual() {
909 let mut engine = LayoutEngine::new(10);
910 engine.from_lines(&["12345", "1234567890abc", "hello"]);
911
912 assert_eq!(engine.logical_to_visual_line(0), 0);
914
915 assert_eq!(engine.logical_to_visual_line(1), 1);
917
918 assert_eq!(engine.logical_to_visual_line(2), 3);
920 }
921
922 #[test]
923 fn test_visual_to_logical() {
924 let mut engine = LayoutEngine::new(10);
925 engine.from_lines(&["12345", "1234567890abc", "hello"]);
926
927 assert_eq!(engine.visual_to_logical_line(0), (0, 0));
929
930 assert_eq!(engine.visual_to_logical_line(1), (1, 0));
932
933 assert_eq!(engine.visual_to_logical_line(2), (1, 1));
935
936 assert_eq!(engine.visual_to_logical_line(3), (2, 0));
938 }
939}