1use unicode_width::UnicodeWidthChar;
7
8pub const DEFAULT_TAB_WIDTH: usize = 4;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct WrapPoint {
14 pub char_index: usize,
16 pub byte_offset: usize,
18}
19
20#[derive(Debug, Clone)]
22pub struct VisualLineInfo {
23 pub visual_line_count: usize,
25 pub wrap_points: Vec<WrapPoint>,
27}
28
29impl VisualLineInfo {
30 pub fn new() -> Self {
32 Self {
33 visual_line_count: 1,
34 wrap_points: Vec::new(),
35 }
36 }
37
38 pub fn from_text(text: &str, viewport_width: usize) -> Self {
40 let wrap_points = calculate_wrap_points(text, viewport_width);
41 let visual_line_count = wrap_points.len() + 1;
42
43 Self {
44 visual_line_count,
45 wrap_points,
46 }
47 }
48
49 pub fn from_text_with_tab_width(text: &str, viewport_width: usize, tab_width: usize) -> Self {
51 let wrap_points = calculate_wrap_points_with_tab_width(text, viewport_width, tab_width);
52 let visual_line_count = wrap_points.len() + 1;
53
54 Self {
55 visual_line_count,
56 wrap_points,
57 }
58 }
59}
60
61impl Default for VisualLineInfo {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67pub fn char_width(ch: char) -> usize {
74 UnicodeWidthChar::width(ch).unwrap_or(1)
76}
77
78pub fn cell_width_at(ch: char, cell_offset_in_line: usize, tab_width: usize) -> usize {
84 if ch == '\t' {
85 let tab_width = tab_width.max(1);
86 let rem = cell_offset_in_line % tab_width;
87 tab_width - rem
88 } else {
89 char_width(ch)
90 }
91}
92
93pub fn str_width(s: &str) -> usize {
95 s.chars().map(char_width).sum()
96}
97
98pub fn str_width_with_tab_width(s: &str, tab_width: usize) -> usize {
100 let mut x = 0usize;
101 for ch in s.chars() {
102 x = x.saturating_add(cell_width_at(ch, x, tab_width));
103 }
104 x
105}
106
107pub fn visual_x_for_column(line: &str, column: usize, tab_width: usize) -> usize {
112 let mut x = 0usize;
113 for ch in line.chars().take(column) {
114 x = x.saturating_add(cell_width_at(ch, x, tab_width));
115 }
116 x
117}
118
119pub fn calculate_wrap_points(text: &str, viewport_width: usize) -> Vec<WrapPoint> {
123 calculate_wrap_points_with_tab_width(text, viewport_width, DEFAULT_TAB_WIDTH)
124}
125
126pub fn calculate_wrap_points_with_tab_width(
128 text: &str,
129 viewport_width: usize,
130 tab_width: usize,
131) -> Vec<WrapPoint> {
132 if viewport_width == 0 {
133 return Vec::new();
134 }
135
136 let mut wrap_points = Vec::new();
137 let mut x_in_segment = 0usize;
138 let mut x_in_line = 0usize;
139
140 for (char_index, (byte_offset, ch)) in text.char_indices().enumerate() {
141 let ch_width = cell_width_at(ch, x_in_line, tab_width);
142
143 if x_in_segment + ch_width > viewport_width {
145 wrap_points.push(WrapPoint {
148 char_index,
149 byte_offset,
150 });
151 x_in_segment = 0;
152 } else {
153 }
155
156 x_in_segment = x_in_segment.saturating_add(ch_width);
157 x_in_line = x_in_line.saturating_add(ch_width);
158
159 if x_in_segment == viewport_width {
161 if byte_offset + ch.len_utf8() < text.len() {
163 wrap_points.push(WrapPoint {
164 char_index: char_index + 1,
165 byte_offset: byte_offset + ch.len_utf8(),
166 });
167 x_in_segment = 0;
168 }
169 }
170 }
171
172 wrap_points
173}
174
175pub struct LayoutEngine {
177 viewport_width: usize,
179 tab_width: usize,
181 line_layouts: Vec<VisualLineInfo>,
183 line_texts: Vec<String>,
185}
186
187impl LayoutEngine {
188 pub fn new(viewport_width: usize) -> Self {
190 Self {
191 viewport_width,
192 tab_width: DEFAULT_TAB_WIDTH,
193 line_layouts: Vec::new(),
194 line_texts: Vec::new(),
195 }
196 }
197
198 pub fn set_viewport_width(&mut self, width: usize) {
200 if self.viewport_width != width {
201 self.viewport_width = width;
202 self.recalculate_all();
203 }
204 }
205
206 pub fn viewport_width(&self) -> usize {
208 self.viewport_width
209 }
210
211 pub fn tab_width(&self) -> usize {
213 self.tab_width
214 }
215
216 pub fn set_tab_width(&mut self, tab_width: usize) {
220 let tab_width = tab_width.max(1);
221 if self.tab_width != tab_width {
222 self.tab_width = tab_width;
223 self.recalculate_all();
224 }
225 }
226
227 pub fn from_lines(&mut self, lines: &[&str]) {
229 self.line_layouts.clear();
230 self.line_texts.clear();
231 for line in lines {
232 self.line_texts.push((*line).to_string());
233 self.line_layouts
234 .push(VisualLineInfo::from_text_with_tab_width(
235 line,
236 self.viewport_width,
237 self.tab_width,
238 ));
239 }
240 }
241
242 pub fn add_line(&mut self, text: &str) {
244 self.line_texts.push(text.to_string());
245 self.line_layouts
246 .push(VisualLineInfo::from_text_with_tab_width(
247 text,
248 self.viewport_width,
249 self.tab_width,
250 ));
251 }
252
253 pub fn update_line(&mut self, line_index: usize, text: &str) {
255 if line_index < self.line_layouts.len() {
256 self.line_texts[line_index] = text.to_string();
257 self.line_layouts[line_index] =
258 VisualLineInfo::from_text_with_tab_width(text, self.viewport_width, self.tab_width);
259 }
260 }
261
262 pub fn insert_line(&mut self, line_index: usize, text: &str) {
264 let pos = line_index.min(self.line_layouts.len());
265 self.line_texts.insert(pos, text.to_string());
266 self.line_layouts.insert(
267 pos,
268 VisualLineInfo::from_text_with_tab_width(text, self.viewport_width, self.tab_width),
269 );
270 }
271
272 pub fn delete_line(&mut self, line_index: usize) {
274 if line_index < self.line_layouts.len() {
275 self.line_texts.remove(line_index);
276 self.line_layouts.remove(line_index);
277 }
278 }
279
280 pub fn get_line_layout(&self, line_index: usize) -> Option<&VisualLineInfo> {
282 self.line_layouts.get(line_index)
283 }
284
285 pub fn logical_line_count(&self) -> usize {
287 self.line_layouts.len()
288 }
289
290 pub fn visual_line_count(&self) -> usize {
292 self.line_layouts.iter().map(|l| l.visual_line_count).sum()
293 }
294
295 pub fn logical_to_visual_line(&self, logical_line: usize) -> usize {
299 self.line_layouts
300 .iter()
301 .take(logical_line)
302 .map(|l| l.visual_line_count)
303 .sum()
304 }
305
306 pub fn visual_to_logical_line(&self, visual_line: usize) -> (usize, usize) {
310 let mut cumulative_visual = 0;
311
312 for (logical_idx, layout) in self.line_layouts.iter().enumerate() {
313 if cumulative_visual + layout.visual_line_count > visual_line {
314 let visual_offset = visual_line - cumulative_visual;
315 return (logical_idx, visual_offset);
316 }
317 cumulative_visual += layout.visual_line_count;
318 }
319
320 let last_line = self.line_layouts.len().saturating_sub(1);
322 let last_visual_offset = self
323 .line_layouts
324 .last()
325 .map(|l| l.visual_line_count.saturating_sub(1))
326 .unwrap_or(0);
327 (last_line, last_visual_offset)
328 }
329
330 fn recalculate_all(&mut self) {
332 if self.line_texts.len() != self.line_layouts.len() {
333 self.line_layouts.clear();
335 for line in &self.line_texts {
336 self.line_layouts
337 .push(VisualLineInfo::from_text_with_tab_width(
338 line,
339 self.viewport_width,
340 self.tab_width,
341 ));
342 }
343 return;
344 }
345
346 for (layout, line_text) in self.line_layouts.iter_mut().zip(self.line_texts.iter()) {
347 *layout = VisualLineInfo::from_text_with_tab_width(
348 line_text,
349 self.viewport_width,
350 self.tab_width,
351 );
352 }
353 }
354
355 pub fn clear(&mut self) {
357 self.line_layouts.clear();
358 self.line_texts.clear();
359 }
360
361 pub fn logical_position_to_visual(
370 &self,
371 logical_line: usize,
372 column: usize,
373 ) -> Option<(usize, usize)> {
374 let layout = self.get_line_layout(logical_line)?;
375 let line_text = self.line_texts.get(logical_line)?;
376
377 let line_char_len = line_text.chars().count();
378 let column = column.min(line_char_len);
379
380 let mut wrapped_offset = 0usize;
382 let mut segment_start_col = 0usize;
383
384 for wrap_point in &layout.wrap_points {
386 if column >= wrap_point.char_index {
387 wrapped_offset += 1;
388 segment_start_col = wrap_point.char_index;
389 } else {
390 break;
391 }
392 }
393
394 let seg_start_x_in_line = visual_x_for_column(line_text, segment_start_col, self.tab_width);
396 let mut x_in_line = seg_start_x_in_line;
397 let mut x_in_segment = 0usize;
398 for ch in line_text
399 .chars()
400 .skip(segment_start_col)
401 .take(column.saturating_sub(segment_start_col))
402 {
403 let w = cell_width_at(ch, x_in_line, self.tab_width);
404 x_in_line = x_in_line.saturating_add(w);
405 x_in_segment = x_in_segment.saturating_add(w);
406 }
407
408 let visual_row = self.logical_to_visual_line(logical_line) + wrapped_offset;
409 Some((visual_row, x_in_segment))
410 }
411
412 pub fn logical_position_to_visual_allow_virtual(
418 &self,
419 logical_line: usize,
420 column: usize,
421 ) -> Option<(usize, usize)> {
422 let layout = self.get_line_layout(logical_line)?;
423 let line_text = self.line_texts.get(logical_line)?;
424
425 let line_char_len = line_text.chars().count();
426 let clamped_column = column.min(line_char_len);
427
428 let mut wrapped_offset = 0usize;
429 let mut segment_start_col = 0usize;
430 for wrap_point in &layout.wrap_points {
431 if clamped_column >= wrap_point.char_index {
432 wrapped_offset += 1;
433 segment_start_col = wrap_point.char_index;
434 } else {
435 break;
436 }
437 }
438
439 let seg_start_x_in_line = visual_x_for_column(line_text, segment_start_col, self.tab_width);
440 let mut x_in_line = seg_start_x_in_line;
441 let mut x_in_segment = 0usize;
442 for ch in line_text
443 .chars()
444 .skip(segment_start_col)
445 .take(clamped_column.saturating_sub(segment_start_col))
446 {
447 let w = cell_width_at(ch, x_in_line, self.tab_width);
448 x_in_line = x_in_line.saturating_add(w);
449 x_in_segment = x_in_segment.saturating_add(w);
450 }
451
452 let x_in_segment = x_in_segment + column.saturating_sub(line_char_len);
453 let visual_row = self.logical_to_visual_line(logical_line) + wrapped_offset;
454 Some((visual_row, x_in_segment))
455 }
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461
462 #[test]
463 fn test_char_width() {
464 assert_eq!(char_width('a'), 1);
466 assert_eq!(char_width('A'), 1);
467 assert_eq!(char_width(' '), 1);
468
469 assert_eq!(char_width('你'), 2);
471 assert_eq!(char_width('好'), 2);
472 assert_eq!(char_width('世'), 2);
473 assert_eq!(char_width('界'), 2);
474
475 assert_eq!(char_width('👋'), 2);
477 assert_eq!(char_width('🌍'), 2);
478 assert_eq!(char_width('🦀'), 2);
479 }
480
481 #[test]
482 fn test_str_width() {
483 assert_eq!(str_width("hello"), 5);
484 assert_eq!(str_width("你好"), 4); assert_eq!(str_width("hello你好"), 9); assert_eq!(str_width("👋🌍"), 4); }
488
489 #[test]
490 fn test_tab_width_expansion() {
491 assert_eq!(cell_width_at('\t', 0, 4), 4);
493 assert_eq!(cell_width_at('\t', 1, 4), 3);
494 assert_eq!(cell_width_at('\t', 2, 4), 2);
495 assert_eq!(cell_width_at('\t', 3, 4), 1);
496 assert_eq!(cell_width_at('\t', 4, 4), 4);
497
498 assert_eq!(str_width_with_tab_width("\t", 4), 4);
499 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); }
504
505 #[test]
506 fn test_calculate_wrap_points_simple() {
507 let text = "hello world";
509 let wraps = calculate_wrap_points(text, 10);
510
511 assert!(!wraps.is_empty());
514 }
515
516 #[test]
517 fn test_calculate_wrap_points_exact_fit() {
518 let text = "1234567890";
520 let wraps = calculate_wrap_points(text, 10);
521
522 assert_eq!(wraps.len(), 0);
524 }
525
526 #[test]
527 fn test_calculate_wrap_points_one_over() {
528 let text = "12345678901";
530 let wraps = calculate_wrap_points(text, 10);
531
532 assert_eq!(wraps.len(), 1);
534 assert_eq!(wraps[0].char_index, 10);
535 }
536
537 #[test]
538 fn test_calculate_wrap_points_cjk() {
539 let text = "你好世界测";
541 let wraps = calculate_wrap_points(text, 10);
542
543 assert_eq!(wraps.len(), 0);
545 }
546
547 #[test]
548 fn test_calculate_wrap_points_cjk_overflow() {
549 let text = "你好世界测试";
551 let wraps = calculate_wrap_points(text, 10);
552
553 assert_eq!(wraps.len(), 1);
555 assert_eq!(wraps[0].char_index, 5);
556 }
557
558 #[test]
559 fn test_wrap_double_width_char() {
560 let text = "Hello你";
563 let wraps = calculate_wrap_points(text, 6);
564
565 assert_eq!(wraps.len(), 1);
568 assert_eq!(wraps[0].char_index, 5); }
570
571 #[test]
572 fn test_visual_line_info() {
573 let info = VisualLineInfo::from_text("1234567890abc", 10);
574 assert_eq!(info.visual_line_count, 2); assert_eq!(info.wrap_points.len(), 1);
576 }
577
578 #[test]
579 fn test_layout_engine_basic() {
580 let mut engine = LayoutEngine::new(10);
581 engine.add_line("hello");
582 engine.add_line("1234567890abc");
583
584 assert_eq!(engine.logical_line_count(), 2);
585 assert_eq!(engine.visual_line_count(), 3); }
587
588 #[test]
589 fn test_layout_engine_viewport_change() {
590 let mut engine = LayoutEngine::new(20);
591 engine.from_lines(&["hello world", "rust programming"]);
592
593 let initial_visual = engine.visual_line_count();
594 assert_eq!(initial_visual, 2); engine.set_viewport_width(5);
598 engine.from_lines(&["hello world", "rust programming"]);
600
601 let new_visual = engine.visual_line_count();
602 assert!(new_visual > initial_visual); }
604
605 #[test]
606 fn test_logical_to_visual() {
607 let mut engine = LayoutEngine::new(10);
608 engine.from_lines(&["12345", "1234567890abc", "hello"]);
609
610 assert_eq!(engine.logical_to_visual_line(0), 0);
612
613 assert_eq!(engine.logical_to_visual_line(1), 1);
615
616 assert_eq!(engine.logical_to_visual_line(2), 3);
618 }
619
620 #[test]
621 fn test_visual_to_logical() {
622 let mut engine = LayoutEngine::new(10);
623 engine.from_lines(&["12345", "1234567890abc", "hello"]);
624
625 assert_eq!(engine.visual_to_logical_line(0), (0, 0));
627
628 assert_eq!(engine.visual_to_logical_line(1), (1, 0));
630
631 assert_eq!(engine.visual_to_logical_line(2), (1, 1));
633
634 assert_eq!(engine.visual_to_logical_line(3), (2, 0));
636 }
637}