1use unicode_width::UnicodeWidthChar;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct WrapPoint {
11 pub char_index: usize,
13 pub byte_offset: usize,
15}
16
17#[derive(Debug, Clone)]
19pub struct VisualLineInfo {
20 pub visual_line_count: usize,
22 pub wrap_points: Vec<WrapPoint>,
24}
25
26impl VisualLineInfo {
27 pub fn new() -> Self {
29 Self {
30 visual_line_count: 1,
31 wrap_points: Vec::new(),
32 }
33 }
34
35 pub fn from_text(text: &str, viewport_width: usize) -> Self {
37 let wrap_points = calculate_wrap_points(text, viewport_width);
38 let visual_line_count = wrap_points.len() + 1;
39
40 Self {
41 visual_line_count,
42 wrap_points,
43 }
44 }
45}
46
47impl Default for VisualLineInfo {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53pub fn char_width(ch: char) -> usize {
60 UnicodeWidthChar::width(ch).unwrap_or(1)
62}
63
64pub fn str_width(s: &str) -> usize {
66 s.chars().map(char_width).sum()
67}
68
69pub fn calculate_wrap_points(text: &str, viewport_width: usize) -> Vec<WrapPoint> {
73 if viewport_width == 0 {
74 return Vec::new();
75 }
76
77 let mut wrap_points = Vec::new();
78 let mut current_width = 0;
79
80 for (char_index, (byte_offset, ch)) in text.char_indices().enumerate() {
81 let ch_width = char_width(ch);
82
83 if current_width + ch_width > viewport_width {
85 wrap_points.push(WrapPoint {
88 char_index,
89 byte_offset,
90 });
91 current_width = ch_width;
92 } else {
93 current_width += ch_width;
94 }
95
96 if current_width == viewport_width {
98 if byte_offset + ch.len_utf8() < text.len() {
100 wrap_points.push(WrapPoint {
101 char_index: char_index + 1,
102 byte_offset: byte_offset + ch.len_utf8(),
103 });
104 current_width = 0;
105 }
106 }
107 }
108
109 wrap_points
110}
111
112pub struct LayoutEngine {
114 viewport_width: usize,
116 line_layouts: Vec<VisualLineInfo>,
118 line_texts: Vec<String>,
120}
121
122impl LayoutEngine {
123 pub fn new(viewport_width: usize) -> Self {
125 Self {
126 viewport_width,
127 line_layouts: Vec::new(),
128 line_texts: Vec::new(),
129 }
130 }
131
132 pub fn set_viewport_width(&mut self, width: usize) {
134 if self.viewport_width != width {
135 self.viewport_width = width;
136 self.recalculate_all();
137 }
138 }
139
140 pub fn viewport_width(&self) -> usize {
142 self.viewport_width
143 }
144
145 pub fn from_lines(&mut self, lines: &[&str]) {
147 self.line_layouts.clear();
148 self.line_texts.clear();
149 for line in lines {
150 self.line_texts.push((*line).to_string());
151 self.line_layouts
152 .push(VisualLineInfo::from_text(line, self.viewport_width));
153 }
154 }
155
156 pub fn add_line(&mut self, text: &str) {
158 self.line_texts.push(text.to_string());
159 self.line_layouts
160 .push(VisualLineInfo::from_text(text, self.viewport_width));
161 }
162
163 pub fn update_line(&mut self, line_index: usize, text: &str) {
165 if line_index < self.line_layouts.len() {
166 self.line_texts[line_index] = text.to_string();
167 self.line_layouts[line_index] = VisualLineInfo::from_text(text, self.viewport_width);
168 }
169 }
170
171 pub fn insert_line(&mut self, line_index: usize, text: &str) {
173 let pos = line_index.min(self.line_layouts.len());
174 self.line_texts.insert(pos, text.to_string());
175 self.line_layouts
176 .insert(pos, VisualLineInfo::from_text(text, self.viewport_width));
177 }
178
179 pub fn delete_line(&mut self, line_index: usize) {
181 if line_index < self.line_layouts.len() {
182 self.line_texts.remove(line_index);
183 self.line_layouts.remove(line_index);
184 }
185 }
186
187 pub fn get_line_layout(&self, line_index: usize) -> Option<&VisualLineInfo> {
189 self.line_layouts.get(line_index)
190 }
191
192 pub fn logical_line_count(&self) -> usize {
194 self.line_layouts.len()
195 }
196
197 pub fn visual_line_count(&self) -> usize {
199 self.line_layouts.iter().map(|l| l.visual_line_count).sum()
200 }
201
202 pub fn logical_to_visual_line(&self, logical_line: usize) -> usize {
206 self.line_layouts
207 .iter()
208 .take(logical_line)
209 .map(|l| l.visual_line_count)
210 .sum()
211 }
212
213 pub fn visual_to_logical_line(&self, visual_line: usize) -> (usize, usize) {
217 let mut cumulative_visual = 0;
218
219 for (logical_idx, layout) in self.line_layouts.iter().enumerate() {
220 if cumulative_visual + layout.visual_line_count > visual_line {
221 let visual_offset = visual_line - cumulative_visual;
222 return (logical_idx, visual_offset);
223 }
224 cumulative_visual += layout.visual_line_count;
225 }
226
227 let last_line = self.line_layouts.len().saturating_sub(1);
229 let last_visual_offset = self
230 .line_layouts
231 .last()
232 .map(|l| l.visual_line_count.saturating_sub(1))
233 .unwrap_or(0);
234 (last_line, last_visual_offset)
235 }
236
237 fn recalculate_all(&mut self) {
239 if self.line_texts.len() != self.line_layouts.len() {
240 self.line_layouts.clear();
242 for line in &self.line_texts {
243 self.line_layouts
244 .push(VisualLineInfo::from_text(line, self.viewport_width));
245 }
246 return;
247 }
248
249 for (layout, line_text) in self.line_layouts.iter_mut().zip(self.line_texts.iter()) {
250 *layout = VisualLineInfo::from_text(line_text, self.viewport_width);
251 }
252 }
253
254 pub fn clear(&mut self) {
256 self.line_layouts.clear();
257 self.line_texts.clear();
258 }
259
260 pub fn logical_position_to_visual(
269 &self,
270 logical_line: usize,
271 column: usize,
272 ) -> Option<(usize, usize)> {
273 let layout = self.get_line_layout(logical_line)?;
274 let line_text = self.line_texts.get(logical_line)?;
275
276 let line_char_len = line_text.chars().count();
277 let column = column.min(line_char_len);
278
279 let mut wrapped_offset = 0usize;
281 let mut segment_start_col = 0usize;
282
283 for wrap_point in &layout.wrap_points {
285 if column >= wrap_point.char_index {
286 wrapped_offset += 1;
287 segment_start_col = wrap_point.char_index;
288 } else {
289 break;
290 }
291 }
292
293 let x_in_segment: usize = line_text
295 .chars()
296 .skip(segment_start_col)
297 .take(column.saturating_sub(segment_start_col))
298 .map(char_width)
299 .sum();
300
301 let visual_row = self.logical_to_visual_line(logical_line) + wrapped_offset;
302 Some((visual_row, x_in_segment))
303 }
304
305 pub fn logical_position_to_visual_allow_virtual(
311 &self,
312 logical_line: usize,
313 column: usize,
314 ) -> Option<(usize, usize)> {
315 let layout = self.get_line_layout(logical_line)?;
316 let line_text = self.line_texts.get(logical_line)?;
317
318 let line_char_len = line_text.chars().count();
319 let clamped_column = column.min(line_char_len);
320
321 let mut wrapped_offset = 0usize;
322 let mut segment_start_col = 0usize;
323 for wrap_point in &layout.wrap_points {
324 if clamped_column >= wrap_point.char_index {
325 wrapped_offset += 1;
326 segment_start_col = wrap_point.char_index;
327 } else {
328 break;
329 }
330 }
331
332 let x_in_segment: usize = line_text
333 .chars()
334 .skip(segment_start_col)
335 .take(clamped_column.saturating_sub(segment_start_col))
336 .map(char_width)
337 .sum();
338
339 let x_in_segment = x_in_segment + column.saturating_sub(line_char_len);
340 let visual_row = self.logical_to_visual_line(logical_line) + wrapped_offset;
341 Some((visual_row, x_in_segment))
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
350 fn test_char_width() {
351 assert_eq!(char_width('a'), 1);
353 assert_eq!(char_width('A'), 1);
354 assert_eq!(char_width(' '), 1);
355
356 assert_eq!(char_width('你'), 2);
358 assert_eq!(char_width('好'), 2);
359 assert_eq!(char_width('世'), 2);
360 assert_eq!(char_width('界'), 2);
361
362 assert_eq!(char_width('👋'), 2);
364 assert_eq!(char_width('🌍'), 2);
365 assert_eq!(char_width('🦀'), 2);
366 }
367
368 #[test]
369 fn test_str_width() {
370 assert_eq!(str_width("hello"), 5);
371 assert_eq!(str_width("你好"), 4); assert_eq!(str_width("hello你好"), 9); assert_eq!(str_width("👋🌍"), 4); }
375
376 #[test]
377 fn test_calculate_wrap_points_simple() {
378 let text = "hello world";
380 let wraps = calculate_wrap_points(text, 10);
381
382 assert!(!wraps.is_empty());
385 }
386
387 #[test]
388 fn test_calculate_wrap_points_exact_fit() {
389 let text = "1234567890";
391 let wraps = calculate_wrap_points(text, 10);
392
393 assert_eq!(wraps.len(), 0);
395 }
396
397 #[test]
398 fn test_calculate_wrap_points_one_over() {
399 let text = "12345678901";
401 let wraps = calculate_wrap_points(text, 10);
402
403 assert_eq!(wraps.len(), 1);
405 assert_eq!(wraps[0].char_index, 10);
406 }
407
408 #[test]
409 fn test_calculate_wrap_points_cjk() {
410 let text = "你好世界测";
412 let wraps = calculate_wrap_points(text, 10);
413
414 assert_eq!(wraps.len(), 0);
416 }
417
418 #[test]
419 fn test_calculate_wrap_points_cjk_overflow() {
420 let text = "你好世界测试";
422 let wraps = calculate_wrap_points(text, 10);
423
424 assert_eq!(wraps.len(), 1);
426 assert_eq!(wraps[0].char_index, 5);
427 }
428
429 #[test]
430 fn test_wrap_double_width_char() {
431 let text = "Hello你";
434 let wraps = calculate_wrap_points(text, 6);
435
436 assert_eq!(wraps.len(), 1);
439 assert_eq!(wraps[0].char_index, 5); }
441
442 #[test]
443 fn test_visual_line_info() {
444 let info = VisualLineInfo::from_text("1234567890abc", 10);
445 assert_eq!(info.visual_line_count, 2); assert_eq!(info.wrap_points.len(), 1);
447 }
448
449 #[test]
450 fn test_layout_engine_basic() {
451 let mut engine = LayoutEngine::new(10);
452 engine.add_line("hello");
453 engine.add_line("1234567890abc");
454
455 assert_eq!(engine.logical_line_count(), 2);
456 assert_eq!(engine.visual_line_count(), 3); }
458
459 #[test]
460 fn test_layout_engine_viewport_change() {
461 let mut engine = LayoutEngine::new(20);
462 engine.from_lines(&["hello world", "rust programming"]);
463
464 let initial_visual = engine.visual_line_count();
465 assert_eq!(initial_visual, 2); engine.set_viewport_width(5);
469 engine.from_lines(&["hello world", "rust programming"]);
471
472 let new_visual = engine.visual_line_count();
473 assert!(new_visual > initial_visual); }
475
476 #[test]
477 fn test_logical_to_visual() {
478 let mut engine = LayoutEngine::new(10);
479 engine.from_lines(&["12345", "1234567890abc", "hello"]);
480
481 assert_eq!(engine.logical_to_visual_line(0), 0);
483
484 assert_eq!(engine.logical_to_visual_line(1), 1);
486
487 assert_eq!(engine.logical_to_visual_line(2), 3);
489 }
490
491 #[test]
492 fn test_visual_to_logical() {
493 let mut engine = LayoutEngine::new(10);
494 engine.from_lines(&["12345", "1234567890abc", "hello"]);
495
496 assert_eq!(engine.visual_to_logical_line(0), (0, 0));
498
499 assert_eq!(engine.visual_to_logical_line(1), (1, 0));
501
502 assert_eq!(engine.visual_to_logical_line(2), (1, 1));
504
505 assert_eq!(engine.visual_to_logical_line(3), (2, 0));
507 }
508}