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 visual_lines = self.visual_lines_cached(self.viewport_width);
129
130 for cursor in self.cursors.as_mut_slice() {
131 if let Some(new_pos) = compute_next_position(
132 cursor.position,
133 direction,
134 &self.buffer,
135 &visual_lines,
136 ) {
137 cursor.position = new_pos;
138 }
139 }
140
141 self.cursors.sort_and_merge();
143
144 self.overlay_cache.clear();
147 }
148
149 pub(crate) fn calculate_cursor_from_point(
156 &self,
157 point: Point,
158 ) -> Option<(usize, usize)> {
159 if point.x < self.gutter_width() {
161 return None; }
163
164 let visual_line_idx = (point.y / self.line_height) as usize;
166
167 let visual_lines = self.visual_lines_cached(self.viewport_width);
170
171 if visual_line_idx >= visual_lines.len() {
172 let last_line = self.buffer.line_count().saturating_sub(1);
174 let last_col = self.buffer.line_len(last_line);
175 return Some((last_line, last_col));
176 }
177
178 let visual_line = &visual_lines[visual_line_idx];
179
180 let x_in_text =
182 point.x - self.gutter_width() - 5.0 + self.horizontal_scroll_offset;
183
184 let line_content = self.buffer.line(visual_line.logical_line);
186
187 let mut current_width = 0.0;
188 let mut col_offset = 0;
189
190 for c in line_content
192 .chars()
193 .skip(visual_line.start_col)
194 .take(visual_line.end_col - visual_line.start_col)
195 {
196 let char_width = super::measure_char_width(
197 c,
198 self.full_char_width,
199 self.char_width,
200 );
201
202 if current_width + char_width / 2.0 > x_in_text {
203 break;
204 }
205 current_width += char_width;
206 col_offset += 1;
207 }
208
209 let col = visual_line.start_col + col_offset;
210 Some((visual_line.logical_line, col))
211 }
212
213 pub(crate) fn handle_mouse_click(&mut self, point: Point) {
217 let before = self.cursors.primary_position();
218 if let Some(pos) = self.calculate_cursor_from_point(point) {
219 self.cursors.primary_mut().position = pos;
220 if self.cursors.primary_position() != before {
221 self.overlay_cache.clear();
223 }
224 }
225 }
226
227 pub(crate) fn scroll_to_cursor(&self) -> Task<Message> {
229 let visual_lines = self.visual_lines_cached(self.viewport_width);
232
233 let pos = self.cursors.primary_position();
234 let cursor_visual =
235 WrappingCalculator::logical_to_visual(&visual_lines, pos.0, pos.1);
236
237 let cursor_y = if let Some(visual_idx) = cursor_visual {
238 visual_idx as f32 * self.line_height
239 } else {
240 pos.0 as f32 * self.line_height
242 };
243
244 let viewport_top = self.viewport_scroll;
245 let viewport_bottom = self.viewport_scroll + self.viewport_height;
246
247 let top_margin = self.line_height * 2.0;
249 let bottom_margin = self.line_height * 2.0;
250
251 let new_v_scroll = if cursor_y < viewport_top + top_margin {
253 Some((cursor_y - top_margin).max(0.0))
255 } else if cursor_y + self.line_height > viewport_bottom - bottom_margin
256 {
257 Some(
259 cursor_y + self.line_height + bottom_margin
260 - self.viewport_height,
261 )
262 } else {
263 None
264 };
265
266 let vertical_task = if let Some(new_scroll) = new_v_scroll {
267 scroll_to(
268 self.scrollable_id.clone(),
269 scrollable::AbsoluteOffset { x: 0.0, y: new_scroll },
270 )
271 } else {
272 Task::none()
273 };
274
275 let h_task = if !self.wrap_enabled {
277 let cursor_content_x = if let Some(visual_idx) = cursor_visual {
279 let vl = &visual_lines[visual_idx];
280 let line_content = self.buffer.line(vl.logical_line);
281 let prefix: String = line_content
282 .chars()
283 .skip(vl.start_col)
284 .take(pos.1.saturating_sub(vl.start_col))
285 .collect();
286 self.gutter_width()
287 + 5.0
288 + measure_text_width(
289 &prefix,
290 self.full_char_width,
291 self.char_width,
292 )
293 } else {
294 self.gutter_width() + 5.0
295 };
296
297 let left_boundary = self.gutter_width() + self.char_width;
298 let right_boundary = self.viewport_width - self.char_width * 2.0;
299 let cursor_viewport_x =
300 cursor_content_x - self.horizontal_scroll_offset;
301
302 let new_h_offset = if cursor_viewport_x < left_boundary {
303 (cursor_content_x - left_boundary).max(0.0)
304 } else if cursor_viewport_x > right_boundary {
305 cursor_content_x - right_boundary
306 } else {
307 self.horizontal_scroll_offset };
309
310 if (new_h_offset - self.horizontal_scroll_offset).abs() > 0.5 {
311 scroll_to(
312 self.horizontal_scrollable_id.clone(),
313 scrollable::AbsoluteOffset { x: new_h_offset, y: 0.0 },
314 )
315 } else {
316 Task::none()
317 }
318 } else {
319 Task::none()
320 };
321
322 Task::batch([vertical_task, h_task])
323 }
324
325 fn move_cursors_by_line(&mut self, map_line: impl Fn(usize) -> usize) {
335 for cursor in self.cursors.as_mut_slice() {
336 let new_line = map_line(cursor.position.0);
337 let line_len = self.buffer.line_len(new_line);
338 cursor.position = (new_line, cursor.position.1.min(line_len));
339 }
340 self.cursors.sort_and_merge();
341 self.overlay_cache.clear();
342 }
343
344 pub(crate) fn page_up(&mut self) {
346 let lines_per_page = (self.viewport_height / self.line_height) as usize;
347 self.move_cursors_by_line(|line| line.saturating_sub(lines_per_page));
348 }
349
350 pub(crate) fn page_down(&mut self) {
352 let lines_per_page = (self.viewport_height / self.line_height) as usize;
353 let max_line = self.buffer.line_count().saturating_sub(1);
354 self.move_cursors_by_line(|line| (line + lines_per_page).min(max_line));
355 }
356
357 pub(crate) fn handle_mouse_drag(&mut self, point: Point) {
361 if let Some(pos) = self.calculate_cursor_from_point(point) {
362 self.cursors.primary_mut().position = pos;
363 }
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[test]
372 fn test_cursor_movement() {
373 let mut editor = CodeEditor::new("line1\nline2", "py");
374 editor.move_cursor(ArrowDirection::Down);
375 assert_eq!(editor.cursors.primary_position().0, 1);
376 editor.move_cursor(ArrowDirection::Right);
377 assert_eq!(editor.cursors.primary_position().1, 1);
378 }
379
380 #[test]
381 fn test_page_down() {
382 let content = (0..100)
384 .map(|i| format!("line {i}"))
385 .collect::<Vec<_>>()
386 .join("\n");
387 let mut editor = CodeEditor::new(&content, "py");
388
389 editor.page_down();
390 assert!(editor.cursors.primary_position().0 >= 25);
392 assert!(editor.cursors.primary_position().0 <= 35);
393 }
394
395 #[test]
396 fn test_page_up() {
397 let content = (0..100)
399 .map(|i| format!("line {i}"))
400 .collect::<Vec<_>>()
401 .join("\n");
402 let mut editor = CodeEditor::new(&content, "py");
403
404 editor.cursors.primary_mut().position = (50, 0);
406 editor.page_up();
407
408 assert!(editor.cursors.primary_position().0 >= 15);
410 assert!(editor.cursors.primary_position().0 <= 25);
411 }
412
413 #[test]
414 fn test_page_down_at_end() {
415 let content =
416 (0..10).map(|i| format!("line {i}")).collect::<Vec<_>>().join("\n");
417 let mut editor = CodeEditor::new(&content, "py");
418
419 editor.page_down();
420 assert_eq!(editor.cursors.primary_position().0, 9);
422 }
423
424 #[test]
425 fn test_page_up_at_start() {
426 let content = (0..100)
427 .map(|i| format!("line {i}"))
428 .collect::<Vec<_>>()
429 .join("\n");
430 let mut editor = CodeEditor::new(&content, "py");
431
432 editor.cursors.primary_mut().position = (0, 0);
434 editor.page_up();
435 assert_eq!(editor.cursors.primary_position().0, 0);
436 }
437
438 #[test]
439 fn test_cursor_click_cjk() {
440 use iced::Point;
441 let mut editor = CodeEditor::new("你好", "txt");
442 editor.set_line_numbers_enabled(false);
443 editor.set_folding_enabled(false);
446
447 let full_char_width = editor.full_char_width();
448 let half_width = full_char_width / 2.0;
449 let padding = 5.0;
450
451 editor
457 .handle_mouse_click(Point::new((half_width - 2.0) + padding, 10.0));
458
459 assert_eq!(editor.cursors.primary_position(), (0, 0));
460
461 editor
464 .handle_mouse_click(Point::new((half_width + 2.0) + padding, 10.0));
465 assert_eq!(editor.cursors.primary_position(), (0, 1));
466
467 editor.handle_mouse_click(Point::new(
471 (full_char_width + half_width - 2.0) + padding,
472 10.0,
473 ));
474 assert_eq!(editor.cursors.primary_position(), (0, 1));
475
476 editor.handle_mouse_click(Point::new(
480 (full_char_width + half_width + 2.0) + padding,
481 10.0,
482 ));
483 assert_eq!(editor.cursors.primary_position(), (0, 2));
484 }
485
486 #[test]
487 fn test_multi_cursor_move_left() {
488 let mut editor = CodeEditor::new("abc\ndef", "rs");
489 editor.cursors.primary_mut().position = (0, 2);
490 editor.cursors.add_cursor((1, 2));
491
492 editor.move_cursor(ArrowDirection::Left);
493
494 let positions: Vec<(usize, usize)> =
496 editor.cursors.iter().map(|c| c.position).collect();
497 assert!(positions.contains(&(0, 1)));
498 assert!(positions.contains(&(1, 1)));
499 }
500
501 #[test]
502 fn test_multi_cursor_move_right() {
503 let mut editor = CodeEditor::new("abc\ndef", "rs");
504 editor.cursors.primary_mut().position = (0, 1);
505 editor.cursors.add_cursor((1, 1));
506
507 editor.move_cursor(ArrowDirection::Right);
508
509 let positions: Vec<(usize, usize)> =
510 editor.cursors.iter().map(|c| c.position).collect();
511 assert!(positions.contains(&(0, 2)));
512 assert!(positions.contains(&(1, 2)));
513 }
514
515 #[test]
516 fn test_multi_cursor_move_deduplicates() {
517 let mut editor = CodeEditor::new("abc", "rs");
518 editor.cursors.primary_mut().position = (0, 0);
520 editor.cursors.add_cursor((0, 1));
521 assert_eq!(editor.cursors.len(), 2);
522
523 editor.move_cursor(ArrowDirection::Right);
524
525 assert_eq!(editor.cursors.len(), 2);
527 }
528}