1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
//! Line wrapping logic for the text editor.
//!
//! This module handles the calculation of visual lines from logical lines
//! when line wrapping is enabled. It supports both viewport-based wrapping
//! (dynamic) and fixed column wrapping.
use crate::text_buffer::TextBuffer;
use std::cmp::Ordering;
use super::compare_floats;
/// Represents a visual line segment in the editor.
///
/// When line wrapping is enabled, a single logical line may be split into
/// multiple visual line segments that are displayed sequentially.
#[derive(Debug, Clone, PartialEq)]
pub struct VisualLine {
/// Index of the logical line in the text buffer
pub logical_line: usize,
/// Segment index (0 for first segment, 1+ for wrapped segments)
pub segment_index: usize,
/// Start column in the logical line (inclusive)
pub start_col: usize,
/// End column in the logical line (exclusive)
pub end_col: usize,
}
impl VisualLine {
/// Creates a new visual line segment.
///
/// # Arguments
///
/// * `logical_line` - Index of the logical line
/// * `segment_index` - Index of the segment within the line
/// * `start_col` - Start column (inclusive)
/// * `end_col` - End column (exclusive)
pub fn new(
logical_line: usize,
segment_index: usize,
start_col: usize,
end_col: usize,
) -> Self {
Self { logical_line, segment_index, start_col, end_col }
}
/// Returns whether this is the first segment of the logical line.
pub fn is_first_segment(&self) -> bool {
self.segment_index == 0
}
/// Returns the length of this segment in characters.
pub fn len(&self) -> usize {
self.end_col - self.start_col
}
}
/// Calculator for line wrapping operations.
///
/// Handles the conversion between logical lines (as stored in the text buffer)
/// and visual lines (as displayed on screen with wrapping applied).
pub struct WrappingCalculator {
/// Whether wrapping is enabled
wrap_enabled: bool,
/// Fixed wrap column (None = wrap at viewport width)
wrap_column: Option<usize>,
/// Full chat with for wide characters
full_char_width: f32,
/// Character width for narrow characters
char_width: f32,
}
impl WrappingCalculator {
/// Creates a new wrapping calculator.
///
/// # Arguments
///
/// * `wrap_enabled` - Whether line wrapping is enabled
/// * `wrap_column` - Fixed wrap column, or None for viewport-based wrapping
/// * `full_char_width` - Full chat with in pixels
/// * `char_width` - Character width in pixels
///
/// # Example
///
/// ```ignore
/// use iced_code_editor::canvas_editor::wrapping::WrappingCalculator;
///
/// // Wrap at viewport width
/// let calc = WrappingCalculator::new(true, None, 14.0, 8.4);
///
/// // Wrap at 80 characters
/// let calc = WrappingCalculator::new(true, Some(80), 14.0, 8.4);
/// ```
pub fn new(
wrap_enabled: bool,
wrap_column: Option<usize>,
full_char_width: f32,
char_width: f32,
) -> Self {
Self { wrap_enabled, wrap_column, full_char_width, char_width }
}
/// Calculates all visual lines from the text buffer.
///
/// # Arguments
///
/// * `text_buffer` - The text buffer to wrap
/// * `viewport_width` - Width of the viewport in pixels (used if wrap_column is None)
/// * `gutter_width` - Width of the line number gutter in pixels (subtracted from available width)
///
/// # Returns
///
/// A vector of visual line segments
pub fn calculate_visual_lines(
&self,
text_buffer: &TextBuffer,
viewport_width: f32,
gutter_width: f32,
) -> Vec<VisualLine> {
if !self.wrap_enabled {
// No wrapping: one visual line per logical line
return (0..text_buffer.line_count())
.map(|line| {
VisualLine::new(line, 0, 0, text_buffer.line_len(line))
})
.collect();
}
// Calculate wrap width in pixels
// If wrap_column is set, width is columns * character width.
// Otherwise, use viewport width minus gutter width.
let wrap_width_pixels = if let Some(cols) = self.wrap_column {
cols as f32 * self.char_width
} else {
(viewport_width - gutter_width).max(self.char_width)
};
let mut visual_lines = Vec::new();
for logical_line in 0..text_buffer.line_count() {
let line_content = text_buffer.line(logical_line);
if line_content.is_empty() {
visual_lines.push(VisualLine::new(logical_line, 0, 0, 0));
continue;
}
let mut segment_index = 0;
let mut current_width = 0.0;
let mut current_segment_start_col = 0;
for (i, c) in line_content.chars().enumerate() {
// Compute pixel width for the current character
let char_width = super::measure_char_width(
c,
self.full_char_width,
self.char_width,
);
// If adding the current character exceeds wrap width, wrap at the previous char.
// Ensure at least one character per segment even if a single char exceeds wrap_width.
// Use epsilon to handle floating-point error.
if compare_floats(current_width + char_width, wrap_width_pixels)
== Ordering::Greater
&& i > current_segment_start_col
{
// Create a new visual segment
visual_lines.push(VisualLine::new(
logical_line,
segment_index,
current_segment_start_col,
i, // end_col is exclusive (current char belongs to next line)
));
segment_index += 1;
current_segment_start_col = i;
current_width = 0.0;
}
current_width += char_width;
}
// Push remaining segment
// Add the last segment of the logical line
visual_lines.push(VisualLine::new(
logical_line,
segment_index,
current_segment_start_col,
line_content.chars().count(),
));
}
visual_lines
}
/// Converts a logical position to a visual line index.
///
/// # Arguments
///
/// * `visual_lines` - Pre-calculated visual lines
/// * `line` - Logical line index
/// * `col` - Column in the logical line
///
/// # Returns
///
/// The visual line index containing this position
pub fn logical_to_visual(
visual_lines: &[VisualLine],
line: usize,
col: usize,
) -> Option<usize> {
visual_lines
.iter()
.position(|vl| {
vl.logical_line == line
&& col >= vl.start_col
&& col < vl.end_col
})
.or_else(|| {
// Handle cursor at end of line (col == end_col)
visual_lines.iter().position(|vl| {
vl.logical_line == line && col == vl.end_col && {
// Check if this is the last segment for this line
visual_lines
.iter()
.filter(|v| v.logical_line == line)
.max_by_key(|v| v.segment_index)
.map(|v| v.segment_index == vl.segment_index)
.unwrap_or(false)
}
})
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::canvas_editor::{CHAR_WIDTH, FONT_SIZE};
#[test]
fn test_no_wrap_when_disabled() {
let buffer = TextBuffer::new("line 1\nline 2\nline 3");
let calc = WrappingCalculator::new(false, None, FONT_SIZE, CHAR_WIDTH);
let visual_lines = calc.calculate_visual_lines(&buffer, 800.0, 60.0);
assert_eq!(visual_lines.len(), 3);
assert_eq!(visual_lines[0].logical_line, 0);
assert_eq!(visual_lines[1].logical_line, 1);
assert_eq!(visual_lines[2].logical_line, 2);
}
#[test]
fn test_wrap_at_fixed_column() {
let buffer =
TextBuffer::new("this is a very long line that should be wrapped");
let calc =
WrappingCalculator::new(true, Some(10), FONT_SIZE, CHAR_WIDTH);
let visual_lines = calc.calculate_visual_lines(&buffer, 800.0, 60.0);
// Line is 47 chars, should wrap into 5 segments (10+10+10+10+7)
assert_eq!(visual_lines.len(), 5);
assert_eq!(visual_lines[0].start_col, 0);
assert_eq!(visual_lines[0].end_col, 10);
assert_eq!(visual_lines[1].start_col, 10);
assert_eq!(visual_lines[1].end_col, 20);
assert_eq!(visual_lines[4].start_col, 40);
assert_eq!(visual_lines[4].end_col, 47);
}
#[test]
fn test_logical_to_visual_mapping() {
let buffer =
TextBuffer::new("short\nthis is a very long line that wraps\nend");
let calc =
WrappingCalculator::new(true, Some(15), FONT_SIZE, CHAR_WIDTH);
let visual_lines = calc.calculate_visual_lines(&buffer, 800.0, 60.0);
// First line (short) - no wrap
assert_eq!(
WrappingCalculator::logical_to_visual(&visual_lines, 0, 0),
Some(0)
);
// Second line (long) - wraps
assert_eq!(
WrappingCalculator::logical_to_visual(&visual_lines, 1, 0),
Some(1)
);
assert_eq!(
WrappingCalculator::logical_to_visual(&visual_lines, 1, 14),
Some(1)
);
assert_eq!(
WrappingCalculator::logical_to_visual(&visual_lines, 1, 15),
Some(2)
);
assert_eq!(
WrappingCalculator::logical_to_visual(&visual_lines, 1, 30),
Some(3)
);
}
#[test]
fn test_wrap_empty_lines() {
let buffer = TextBuffer::new("line1\n\nline3");
let calc =
WrappingCalculator::new(true, Some(10), FONT_SIZE, CHAR_WIDTH);
let visual_lines = calc.calculate_visual_lines(&buffer, 800.0, 60.0);
assert_eq!(visual_lines.len(), 3);
assert_eq!(visual_lines[1].logical_line, 1);
assert_eq!(visual_lines[1].len(), 0);
}
#[test]
fn test_wrap_very_long_line() {
let long_text = "a".repeat(100);
let buffer = TextBuffer::new(&long_text);
let calc =
WrappingCalculator::new(true, Some(20), FONT_SIZE, CHAR_WIDTH);
let visual_lines = calc.calculate_visual_lines(&buffer, 800.0, 60.0);
// 100 chars / 20 per line = 5 lines
assert_eq!(visual_lines.len(), 5);
assert!(visual_lines.iter().all(|vl| vl.logical_line == 0));
}
#[test]
fn test_visual_line_is_first_segment() {
let vl1 = VisualLine::new(0, 0, 0, 10);
let vl2 = VisualLine::new(0, 1, 10, 20);
assert!(vl1.is_first_segment());
assert!(!vl2.is_first_segment());
}
#[test]
fn test_wrap_cjk() {
// CJK characters are wide (FONT_SIZE = 14.0)
// Latin characters are narrow (CHAR_WIDTH = 8.4)
// Wrap width = 10 columns * 8.4 = 84.0 pixels
// 6 CJK characters = 6 * 14.0 = 84.0 pixels. Matches exactly.
let text = "你好世界你好"; // 6 chars
let buffer = TextBuffer::new(text);
let calc =
WrappingCalculator::new(true, Some(10), FONT_SIZE, CHAR_WIDTH); // 84.0 px
let visual_lines = calc.calculate_visual_lines(&buffer, 800.0, 60.0);
assert_eq!(visual_lines.len(), 1);
assert_eq!(visual_lines[0].len(), 6);
// 7 CJK characters = 7 * 14.0 = 98.0 pixels.
// Wrap width is 84.0 pixels.
// First 6 chars = 6 * 14.0 = 84.0 pixels. They fit exactly (84.0 <= 84.0).
// 7th char adds 14.0, total 98.0 > 84.0. Triggers wrap before 7th char.
let text = "你好世界你好世"; // 7 chars
let buffer = TextBuffer::new(text);
let visual_lines = calc.calculate_visual_lines(&buffer, 800.0, 60.0);
assert_eq!(visual_lines.len(), 2);
assert_eq!(visual_lines[0].len(), 6); // First 6 fit
assert_eq!(visual_lines[1].len(), 1); // 7th wraps
assert_eq!(visual_lines[1].start_col, 6); // Starts at 7th char (index 6)
}
}