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::WrappingCalculator;
15use super::{ArrowDirection, CodeEditor, Message};
16
17impl CodeEditor {
18 pub fn set_cursor(&mut self, line: usize, col: usize) -> Task<Message> {
33 let line = line.min(self.buffer.line_count().saturating_sub(1));
34 let line_len = self.buffer.line(line).chars().count();
35 let col = col.min(line_len);
36
37 self.cursor = (line, col);
38 self.is_dragging = false;
41 self.selection_start = None;
42 self.selection_end = None;
43
44 self.last_blink = Instant::now();
46
47 self.overlay_cache.clear();
48 self.scroll_to_cursor()
49 }
50
51 pub(crate) fn move_cursor(&mut self, direction: ArrowDirection) {
53 let (line, col) = self.cursor;
54
55 match direction {
56 ArrowDirection::Up | ArrowDirection::Down => {
57 let wrapping_calc = WrappingCalculator::new(
59 self.wrap_enabled,
60 self.wrap_column,
61 self.full_char_width,
62 self.char_width,
63 );
64 let visual_lines = wrapping_calc.calculate_visual_lines(
65 &self.buffer,
66 self.viewport_width,
67 self.gutter_width(),
68 );
69
70 if let Some(current_visual) =
72 WrappingCalculator::logical_to_visual(
73 &visual_lines,
74 line,
75 col,
76 )
77 {
78 let target_visual = match direction {
79 ArrowDirection::Up => {
80 if current_visual > 0 {
81 current_visual - 1
82 } else {
83 return; }
85 }
86 ArrowDirection::Down => {
87 if current_visual + 1 < visual_lines.len() {
88 current_visual + 1
89 } else {
90 return; }
92 }
93 _ => {
94 return;
96 }
97 };
98
99 let target_vl = &visual_lines[target_visual];
100 let current_vl = &visual_lines[current_visual];
101
102 let new_col = if target_vl.logical_line == line {
104 let offset_in_current =
107 col.saturating_sub(current_vl.start_col);
108 let target_col =
110 target_vl.start_col + offset_in_current;
111 if target_col >= target_vl.end_col {
114 target_vl
115 .end_col
116 .saturating_sub(1)
117 .max(target_vl.start_col)
118 } else {
119 target_col
120 }
121 } else {
122 let target_line_len =
124 self.buffer.line_len(target_vl.logical_line);
125 (target_vl.start_col + col.min(target_vl.len()))
126 .min(target_line_len)
127 };
128
129 self.cursor = (target_vl.logical_line, new_col);
130 }
131 }
132 ArrowDirection::Left => {
133 if col > 0 {
134 self.cursor.1 -= 1;
135 } else if line > 0 {
136 let prev_line_len = self.buffer.line_len(line - 1);
138 self.cursor = (line - 1, prev_line_len);
139 }
140 }
141 ArrowDirection::Right => {
142 let line_len = self.buffer.line_len(line);
143 if col < line_len {
144 self.cursor.1 += 1;
145 } else if line + 1 < self.buffer.line_count() {
146 self.cursor = (line + 1, 0);
148 }
149 }
150 }
151 self.overlay_cache.clear();
154 }
155
156 pub(crate) fn calculate_cursor_from_point(
163 &self,
164 point: Point,
165 ) -> Option<(usize, usize)> {
166 if point.x < self.gutter_width() {
168 return None; }
170
171 let visual_line_idx = (point.y / self.line_height) as usize;
173
174 let visual_lines = self.visual_lines_cached(self.viewport_width);
177
178 if visual_line_idx >= visual_lines.len() {
179 let last_line = self.buffer.line_count().saturating_sub(1);
181 let last_col = self.buffer.line_len(last_line);
182 return Some((last_line, last_col));
183 }
184
185 let visual_line = &visual_lines[visual_line_idx];
186
187 let x_in_text =
189 point.x - self.gutter_width() - 5.0 + self.horizontal_scroll_offset;
190
191 let line_content = self.buffer.line(visual_line.logical_line);
193
194 let mut current_width = 0.0;
195 let mut col_offset = 0;
196
197 for c in line_content
199 .chars()
200 .skip(visual_line.start_col)
201 .take(visual_line.end_col - visual_line.start_col)
202 {
203 let char_width = super::measure_char_width(
204 c,
205 self.full_char_width,
206 self.char_width,
207 );
208
209 if current_width + char_width / 2.0 > x_in_text {
210 break;
211 }
212 current_width += char_width;
213 col_offset += 1;
214 }
215
216 let col = visual_line.start_col + col_offset;
217 Some((visual_line.logical_line, col))
218 }
219
220 pub(crate) fn handle_mouse_click(&mut self, point: Point) {
224 let before = self.cursor;
225 if let Some(cursor) = self.calculate_cursor_from_point(point) {
226 self.cursor = cursor;
227 if self.cursor != before {
228 self.overlay_cache.clear();
230 }
231 }
232 }
233
234 pub(crate) fn scroll_to_cursor(&self) -> Task<Message> {
236 let visual_lines = self.visual_lines_cached(self.viewport_width);
239
240 let cursor_visual = WrappingCalculator::logical_to_visual(
241 &visual_lines,
242 self.cursor.0,
243 self.cursor.1,
244 );
245
246 let cursor_y = if let Some(visual_idx) = cursor_visual {
247 visual_idx as f32 * self.line_height
248 } else {
249 self.cursor.0 as f32 * self.line_height
251 };
252
253 let viewport_top = self.viewport_scroll;
254 let viewport_bottom = self.viewport_scroll + self.viewport_height;
255
256 let top_margin = self.line_height * 2.0;
258 let bottom_margin = self.line_height * 2.0;
259
260 let new_v_scroll = if cursor_y < viewport_top + top_margin {
262 Some((cursor_y - top_margin).max(0.0))
264 } else if cursor_y + self.line_height > viewport_bottom - bottom_margin
265 {
266 Some(
268 cursor_y + self.line_height + bottom_margin
269 - self.viewport_height,
270 )
271 } else {
272 None
273 };
274
275 let vertical_task = if let Some(new_scroll) = new_v_scroll {
276 scroll_to(
277 self.scrollable_id.clone(),
278 scrollable::AbsoluteOffset { x: 0.0, y: new_scroll },
279 )
280 } else {
281 Task::none()
282 };
283
284 let h_task = if !self.wrap_enabled {
286 let cursor_content_x = if let Some(visual_idx) = cursor_visual {
288 let vl = &visual_lines[visual_idx];
289 let line_content = self.buffer.line(vl.logical_line);
290 let prefix: String = line_content
291 .chars()
292 .skip(vl.start_col)
293 .take(self.cursor.1.saturating_sub(vl.start_col))
294 .collect();
295 self.gutter_width()
296 + 5.0
297 + measure_text_width(
298 &prefix,
299 self.full_char_width,
300 self.char_width,
301 )
302 } else {
303 self.gutter_width() + 5.0
304 };
305
306 let left_boundary = self.gutter_width() + self.char_width;
307 let right_boundary = self.viewport_width - self.char_width * 2.0;
308 let cursor_viewport_x =
309 cursor_content_x - self.horizontal_scroll_offset;
310
311 let new_h_offset = if cursor_viewport_x < left_boundary {
312 (cursor_content_x - left_boundary).max(0.0)
313 } else if cursor_viewport_x > right_boundary {
314 cursor_content_x - right_boundary
315 } else {
316 self.horizontal_scroll_offset };
318
319 if (new_h_offset - self.horizontal_scroll_offset).abs() > 0.5 {
320 scroll_to(
321 self.horizontal_scrollable_id.clone(),
322 scrollable::AbsoluteOffset { x: new_h_offset, y: 0.0 },
323 )
324 } else {
325 Task::none()
326 }
327 } else {
328 Task::none()
329 };
330
331 Task::batch([vertical_task, h_task])
332 }
333
334 pub(crate) fn page_up(&mut self) {
336 let lines_per_page = (self.viewport_height / self.line_height) as usize;
337
338 let current_line = self.cursor.0;
339 let new_line = current_line.saturating_sub(lines_per_page);
340 let line_len = self.buffer.line_len(new_line);
341
342 self.cursor = (new_line, self.cursor.1.min(line_len));
343 self.overlay_cache.clear();
344 }
345
346 pub(crate) fn page_down(&mut self) {
348 let lines_per_page = (self.viewport_height / self.line_height) as usize;
349
350 let current_line = self.cursor.0;
351 let max_line = self.buffer.line_count().saturating_sub(1);
352 let new_line = (current_line + lines_per_page).min(max_line);
353 let line_len = self.buffer.line_len(new_line);
354
355 self.cursor = (new_line, self.cursor.1.min(line_len));
356 self.overlay_cache.clear();
357 }
358
359 pub(crate) fn handle_mouse_drag(&mut self, point: Point) {
363 if let Some(cursor) = self.calculate_cursor_from_point(point) {
364 self.cursor = cursor;
365 self.selection_end = Some(self.cursor);
366 }
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 #[test]
375 fn test_cursor_movement() {
376 let mut editor = CodeEditor::new("line1\nline2", "py");
377 editor.move_cursor(ArrowDirection::Down);
378 assert_eq!(editor.cursor.0, 1);
379 editor.move_cursor(ArrowDirection::Right);
380 assert_eq!(editor.cursor.1, 1);
381 }
382
383 #[test]
384 fn test_page_down() {
385 let content = (0..100)
387 .map(|i| format!("line {i}"))
388 .collect::<Vec<_>>()
389 .join("\n");
390 let mut editor = CodeEditor::new(&content, "py");
391
392 editor.page_down();
393 assert!(editor.cursor.0 >= 25);
395 assert!(editor.cursor.0 <= 35);
396 }
397
398 #[test]
399 fn test_page_up() {
400 let content = (0..100)
402 .map(|i| format!("line {i}"))
403 .collect::<Vec<_>>()
404 .join("\n");
405 let mut editor = CodeEditor::new(&content, "py");
406
407 editor.cursor = (50, 0);
409 editor.page_up();
410
411 assert!(editor.cursor.0 >= 15);
413 assert!(editor.cursor.0 <= 25);
414 }
415
416 #[test]
417 fn test_page_down_at_end() {
418 let content =
419 (0..10).map(|i| format!("line {i}")).collect::<Vec<_>>().join("\n");
420 let mut editor = CodeEditor::new(&content, "py");
421
422 editor.page_down();
423 assert_eq!(editor.cursor.0, 9);
425 }
426
427 #[test]
428 fn test_page_up_at_start() {
429 let content = (0..100)
430 .map(|i| format!("line {i}"))
431 .collect::<Vec<_>>()
432 .join("\n");
433 let mut editor = CodeEditor::new(&content, "py");
434
435 editor.cursor = (0, 0);
437 editor.page_up();
438 assert_eq!(editor.cursor.0, 0);
439 }
440
441 #[test]
442 fn test_cursor_click_cjk() {
443 use iced::Point;
444 let mut editor = CodeEditor::new("你好", "txt");
445 editor.set_line_numbers_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.cursor, (0, 0));
460
461 editor
464 .handle_mouse_click(Point::new((half_width + 2.0) + padding, 10.0));
465 assert_eq!(editor.cursor, (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.cursor, (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.cursor, (0, 2));
484 }
485}