iced_code_editor/canvas_editor/
cursor.rs1use iced::widget::operation::scroll_to;
4use iced::widget::scrollable;
5use iced::{Point, Task};
6#[cfg(not(target_arch = "wasm32"))]
7use std::time::Instant;
8
9#[cfg(target_arch = "wasm32")]
10use web_time::Instant;
11
12use super::measure_text_width;
13
14use super::wrapping::{VisualLine, WrappingCalculator};
15use super::{ArrowDirection, CodeEditor, Message};
16use crate::text_buffer::TextBuffer;
17
18fn compute_next_position(
22 pos: (usize, usize),
23 direction: ArrowDirection,
24 buffer: &TextBuffer,
25 visual_lines: &[VisualLine],
26) -> Option<(usize, usize)> {
27 let (line, col) = pos;
28 match direction {
29 ArrowDirection::Up | ArrowDirection::Down => {
30 let current_visual =
31 WrappingCalculator::logical_to_visual(visual_lines, line, col)?;
32
33 let target_visual = match direction {
34 ArrowDirection::Up => current_visual.checked_sub(1)?,
35 ArrowDirection::Down => {
36 let next = current_visual + 1;
37 if next < visual_lines.len() {
38 next
39 } else {
40 return None;
41 }
42 }
43 _ => return None,
44 };
45
46 let target_vl = &visual_lines[target_visual];
47 let current_vl = &visual_lines[current_visual];
48
49 let new_col = if target_vl.logical_line == line {
50 let offset_in_current =
51 col.saturating_sub(current_vl.start_col);
52 let target_col = target_vl.start_col + offset_in_current;
53 if target_col >= target_vl.end_col {
54 target_vl.end_col.saturating_sub(1).max(target_vl.start_col)
55 } else {
56 target_col
57 }
58 } else {
59 let target_line_len = buffer.line_len(target_vl.logical_line);
60 (target_vl.start_col + col.min(target_vl.len()))
61 .min(target_line_len)
62 };
63
64 Some((target_vl.logical_line, new_col))
65 }
66 ArrowDirection::Left => {
67 if col > 0 {
68 Some((line, col - 1))
69 } else if line > 0 {
70 Some((line - 1, buffer.line_len(line - 1)))
71 } else {
72 None
73 }
74 }
75 ArrowDirection::Right => {
76 let line_len = buffer.line_len(line);
77 if col < line_len {
78 Some((line, col + 1))
79 } else if line + 1 < buffer.line_count() {
80 Some((line + 1, 0))
81 } else {
82 None
83 }
84 }
85 }
86}
87
88impl CodeEditor {
89 pub fn set_cursor(&mut self, line: usize, col: usize) -> Task<Message> {
104 let line = line.min(self.buffer.line_count().saturating_sub(1));
105 let line_len = self.buffer.line(line).chars().count();
106 let col = col.min(line_len);
107
108 self.cursors.set_single((line, col));
109 self.is_dragging = false;
112
113 self.last_blink = Instant::now();
115
116 self.overlay_cache.clear();
117 self.scroll_to_cursor()
118 }
119
120 pub(crate) fn move_cursor(&mut self, direction: ArrowDirection) {
125 let wrapping_calc = WrappingCalculator::new(
127 self.wrap_enabled,
128 self.wrap_column,
129 self.full_char_width,
130 self.char_width,
131 );
132 let visual_lines = wrapping_calc.calculate_visual_lines(
133 &self.buffer,
134 self.viewport_width,
135 self.gutter_width(),
136 );
137
138 for cursor in self.cursors.as_mut_slice() {
139 if let Some(new_pos) = compute_next_position(
140 cursor.position,
141 direction,
142 &self.buffer,
143 &visual_lines,
144 ) {
145 cursor.position = new_pos;
146 }
147 }
148
149 self.cursors.sort_and_merge();
151
152 self.overlay_cache.clear();
155 }
156
157 pub(crate) fn calculate_cursor_from_point(
164 &self,
165 point: Point,
166 ) -> Option<(usize, usize)> {
167 if point.x < self.gutter_width() {
169 return None; }
171
172 let visual_line_idx = (point.y / self.line_height) as usize;
174
175 let visual_lines = self.visual_lines_cached(self.viewport_width);
178
179 if visual_line_idx >= visual_lines.len() {
180 let last_line = self.buffer.line_count().saturating_sub(1);
182 let last_col = self.buffer.line_len(last_line);
183 return Some((last_line, last_col));
184 }
185
186 let visual_line = &visual_lines[visual_line_idx];
187
188 let x_in_text =
190 point.x - self.gutter_width() - 5.0 + self.horizontal_scroll_offset;
191
192 let line_content = self.buffer.line(visual_line.logical_line);
194
195 let mut current_width = 0.0;
196 let mut col_offset = 0;
197
198 for c in line_content
200 .chars()
201 .skip(visual_line.start_col)
202 .take(visual_line.end_col - visual_line.start_col)
203 {
204 let char_width = super::measure_char_width(
205 c,
206 self.full_char_width,
207 self.char_width,
208 );
209
210 if current_width + char_width / 2.0 > x_in_text {
211 break;
212 }
213 current_width += char_width;
214 col_offset += 1;
215 }
216
217 let col = visual_line.start_col + col_offset;
218 Some((visual_line.logical_line, col))
219 }
220
221 pub(crate) fn handle_mouse_click(&mut self, point: Point) {
225 let before = self.cursors.primary_position();
226 if let Some(pos) = self.calculate_cursor_from_point(point) {
227 self.cursors.primary_mut().position = pos;
228 if self.cursors.primary_position() != before {
229 self.overlay_cache.clear();
231 }
232 }
233 }
234
235 pub(crate) fn scroll_to_cursor(&self) -> Task<Message> {
237 let visual_lines = self.visual_lines_cached(self.viewport_width);
240
241 let pos = self.cursors.primary_position();
242 let cursor_visual =
243 WrappingCalculator::logical_to_visual(&visual_lines, pos.0, pos.1);
244
245 let cursor_y = if let Some(visual_idx) = cursor_visual {
246 visual_idx as f32 * self.line_height
247 } else {
248 pos.0 as f32 * self.line_height
250 };
251
252 let viewport_top = self.viewport_scroll;
253 let viewport_bottom = self.viewport_scroll + self.viewport_height;
254
255 let top_margin = self.line_height * 2.0;
257 let bottom_margin = self.line_height * 2.0;
258
259 let new_v_scroll = if cursor_y < viewport_top + top_margin {
261 Some((cursor_y - top_margin).max(0.0))
263 } else if cursor_y + self.line_height > viewport_bottom - bottom_margin
264 {
265 Some(
267 cursor_y + self.line_height + bottom_margin
268 - self.viewport_height,
269 )
270 } else {
271 None
272 };
273
274 let vertical_task = if let Some(new_scroll) = new_v_scroll {
275 scroll_to(
276 self.scrollable_id.clone(),
277 scrollable::AbsoluteOffset { x: 0.0, y: new_scroll },
278 )
279 } else {
280 Task::none()
281 };
282
283 let h_task = if !self.wrap_enabled {
285 let cursor_content_x = if let Some(visual_idx) = cursor_visual {
287 let vl = &visual_lines[visual_idx];
288 let line_content = self.buffer.line(vl.logical_line);
289 let prefix: String = line_content
290 .chars()
291 .skip(vl.start_col)
292 .take(pos.1.saturating_sub(vl.start_col))
293 .collect();
294 self.gutter_width()
295 + 5.0
296 + measure_text_width(
297 &prefix,
298 self.full_char_width,
299 self.char_width,
300 )
301 } else {
302 self.gutter_width() + 5.0
303 };
304
305 let left_boundary = self.gutter_width() + self.char_width;
306 let right_boundary = self.viewport_width - self.char_width * 2.0;
307 let cursor_viewport_x =
308 cursor_content_x - self.horizontal_scroll_offset;
309
310 let new_h_offset = if cursor_viewport_x < left_boundary {
311 (cursor_content_x - left_boundary).max(0.0)
312 } else if cursor_viewport_x > right_boundary {
313 cursor_content_x - right_boundary
314 } else {
315 self.horizontal_scroll_offset };
317
318 if (new_h_offset - self.horizontal_scroll_offset).abs() > 0.5 {
319 scroll_to(
320 self.horizontal_scrollable_id.clone(),
321 scrollable::AbsoluteOffset { x: new_h_offset, y: 0.0 },
322 )
323 } else {
324 Task::none()
325 }
326 } else {
327 Task::none()
328 };
329
330 Task::batch([vertical_task, h_task])
331 }
332
333 pub(crate) fn page_up(&mut self) {
335 let lines_per_page = (self.viewport_height / self.line_height) as usize;
336 for cursor in self.cursors.as_mut_slice() {
337 let new_line = cursor.position.0.saturating_sub(lines_per_page);
338 let line_len = self.buffer.line_len(new_line);
339 cursor.position = (new_line, cursor.position.1.min(line_len));
340 }
341 self.cursors.sort_and_merge();
342 self.overlay_cache.clear();
343 }
344
345 pub(crate) fn page_down(&mut self) {
347 let lines_per_page = (self.viewport_height / self.line_height) as usize;
348 let max_line = self.buffer.line_count().saturating_sub(1);
349 for cursor in self.cursors.as_mut_slice() {
350 let new_line = (cursor.position.0 + lines_per_page).min(max_line);
351 let line_len = self.buffer.line_len(new_line);
352 cursor.position = (new_line, cursor.position.1.min(line_len));
353 }
354 self.cursors.sort_and_merge();
355 self.overlay_cache.clear();
356 }
357
358 pub(crate) fn handle_mouse_drag(&mut self, point: Point) {
362 if let Some(pos) = self.calculate_cursor_from_point(point) {
363 self.cursors.primary_mut().position = pos;
364 }
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_cursor_movement() {
374 let mut editor = CodeEditor::new("line1\nline2", "py");
375 editor.move_cursor(ArrowDirection::Down);
376 assert_eq!(editor.cursors.primary_position().0, 1);
377 editor.move_cursor(ArrowDirection::Right);
378 assert_eq!(editor.cursors.primary_position().1, 1);
379 }
380
381 #[test]
382 fn test_page_down() {
383 let content = (0..100)
385 .map(|i| format!("line {i}"))
386 .collect::<Vec<_>>()
387 .join("\n");
388 let mut editor = CodeEditor::new(&content, "py");
389
390 editor.page_down();
391 assert!(editor.cursors.primary_position().0 >= 25);
393 assert!(editor.cursors.primary_position().0 <= 35);
394 }
395
396 #[test]
397 fn test_page_up() {
398 let content = (0..100)
400 .map(|i| format!("line {i}"))
401 .collect::<Vec<_>>()
402 .join("\n");
403 let mut editor = CodeEditor::new(&content, "py");
404
405 editor.cursors.primary_mut().position = (50, 0);
407 editor.page_up();
408
409 assert!(editor.cursors.primary_position().0 >= 15);
411 assert!(editor.cursors.primary_position().0 <= 25);
412 }
413
414 #[test]
415 fn test_page_down_at_end() {
416 let content =
417 (0..10).map(|i| format!("line {i}")).collect::<Vec<_>>().join("\n");
418 let mut editor = CodeEditor::new(&content, "py");
419
420 editor.page_down();
421 assert_eq!(editor.cursors.primary_position().0, 9);
423 }
424
425 #[test]
426 fn test_page_up_at_start() {
427 let content = (0..100)
428 .map(|i| format!("line {i}"))
429 .collect::<Vec<_>>()
430 .join("\n");
431 let mut editor = CodeEditor::new(&content, "py");
432
433 editor.cursors.primary_mut().position = (0, 0);
435 editor.page_up();
436 assert_eq!(editor.cursors.primary_position().0, 0);
437 }
438
439 #[test]
440 fn test_cursor_click_cjk() {
441 use iced::Point;
442 let mut editor = CodeEditor::new("你好", "txt");
443 editor.set_line_numbers_enabled(false);
444
445 let full_char_width = editor.full_char_width();
446 let half_width = full_char_width / 2.0;
447 let padding = 5.0;
448
449 editor
455 .handle_mouse_click(Point::new((half_width - 2.0) + padding, 10.0));
456
457 assert_eq!(editor.cursors.primary_position(), (0, 0));
458
459 editor
462 .handle_mouse_click(Point::new((half_width + 2.0) + padding, 10.0));
463 assert_eq!(editor.cursors.primary_position(), (0, 1));
464
465 editor.handle_mouse_click(Point::new(
469 (full_char_width + half_width - 2.0) + padding,
470 10.0,
471 ));
472 assert_eq!(editor.cursors.primary_position(), (0, 1));
473
474 editor.handle_mouse_click(Point::new(
478 (full_char_width + half_width + 2.0) + padding,
479 10.0,
480 ));
481 assert_eq!(editor.cursors.primary_position(), (0, 2));
482 }
483
484 #[test]
485 fn test_multi_cursor_move_left() {
486 let mut editor = CodeEditor::new("abc\ndef", "rs");
487 editor.cursors.primary_mut().position = (0, 2);
488 editor.cursors.add_cursor((1, 2));
489
490 editor.move_cursor(ArrowDirection::Left);
491
492 let positions: Vec<(usize, usize)> =
494 editor.cursors.iter().map(|c| c.position).collect();
495 assert!(positions.contains(&(0, 1)));
496 assert!(positions.contains(&(1, 1)));
497 }
498
499 #[test]
500 fn test_multi_cursor_move_right() {
501 let mut editor = CodeEditor::new("abc\ndef", "rs");
502 editor.cursors.primary_mut().position = (0, 1);
503 editor.cursors.add_cursor((1, 1));
504
505 editor.move_cursor(ArrowDirection::Right);
506
507 let positions: Vec<(usize, usize)> =
508 editor.cursors.iter().map(|c| c.position).collect();
509 assert!(positions.contains(&(0, 2)));
510 assert!(positions.contains(&(1, 2)));
511 }
512
513 #[test]
514 fn test_multi_cursor_move_deduplicates() {
515 let mut editor = CodeEditor::new("abc", "rs");
516 editor.cursors.primary_mut().position = (0, 0);
518 editor.cursors.add_cursor((0, 1));
519 assert_eq!(editor.cursors.len(), 2);
520
521 editor.move_cursor(ArrowDirection::Right);
522
523 assert_eq!(editor.cursors.len(), 2);
525 }
526}