iced_code_editor/canvas_editor/
canvas_impl.rs

1//! Canvas rendering implementation using Iced's `canvas::Program`.
2
3use iced::advanced::input_method;
4use iced::mouse;
5use iced::widget::canvas::{self, Geometry};
6use iced::{Color, Event, Point, Rectangle, Size, Theme, keyboard};
7use syntect::easy::HighlightLines;
8use syntect::highlighting::{Style, ThemeSet};
9use syntect::parsing::SyntaxSet;
10
11fn is_cursor_in_bounds(cursor: &mouse::Cursor, bounds: Rectangle) -> bool {
12    match cursor {
13        mouse::Cursor::Available(point) => bounds.contains(*point),
14        mouse::Cursor::Levitating(point) => bounds.contains(*point),
15        mouse::Cursor::Unavailable => false,
16    }
17}
18
19/// Computes geometry (x start and width) for a text segment used in rendering or highlighting.
20///
21/// Returns: (x_start, width)
22///
23/// Parameters:
24/// - `line_content`: full text content of the current line.
25/// - `visual_start_col`: start column index of the current visual line.
26/// - `segment_start_col`: start column index of the target segment (e.g. highlight).
27/// - `segment_end_col`: end column index of the target segment.
28/// - `base_offset`: base X offset (usually gutter_width + padding).
29///
30/// This function handles CJK character widths correctly to keep highlights accurate.
31fn calculate_segment_geometry(
32    line_content: &str,
33    visual_start_col: usize,
34    segment_start_col: usize,
35    segment_end_col: usize,
36    base_offset: f32,
37    full_char_width: f32,
38    char_width: f32,
39) -> (f32, f32) {
40    // Calculate prefix width relative to visual line start
41    let prefix_len = segment_start_col.saturating_sub(visual_start_col);
42    let prefix_text: String =
43        line_content.chars().skip(visual_start_col).take(prefix_len).collect();
44    let prefix_width =
45        measure_text_width(&prefix_text, full_char_width, char_width);
46
47    // Calculate segment width
48    let segment_len = segment_end_col.saturating_sub(segment_start_col);
49    let segment_text: String = line_content
50        .chars()
51        .skip(segment_start_col)
52        .take(segment_len)
53        .collect();
54    let segment_width =
55        measure_text_width(&segment_text, full_char_width, char_width);
56
57    (base_offset + prefix_width, segment_width)
58}
59
60use super::wrapping::WrappingCalculator;
61use super::{ArrowDirection, CodeEditor, Message, measure_text_width};
62use iced::widget::canvas::Action;
63
64impl canvas::Program<Message> for CodeEditor {
65    type State = ();
66
67    fn draw(
68        &self,
69        _state: &Self::State,
70        renderer: &iced::Renderer,
71        _theme: &Theme,
72        bounds: Rectangle,
73        _cursor: mouse::Cursor,
74    ) -> Vec<Geometry> {
75        let geometry = self.cache.draw(renderer, bounds.size(), |frame| {
76            // Initialize wrapping calculator
77            let wrapping_calc = WrappingCalculator::new(
78                self.wrap_enabled,
79                self.wrap_column,
80                self.full_char_width,
81                self.char_width,
82            );
83            let visual_lines = wrapping_calc.calculate_visual_lines(
84                &self.buffer,
85                bounds.width,
86                self.gutter_width(),
87            );
88
89            // Calculate visible line range based on viewport for optimized rendering
90            // Use bounds.height as fallback when viewport_height is not yet initialized
91            let effective_viewport_height = if self.viewport_height > 0.0 {
92                self.viewport_height
93            } else {
94                bounds.height
95            };
96            let first_visible_line =
97                (self.viewport_scroll / self.line_height).floor() as usize;
98            let visible_lines_count =
99                (effective_viewport_height / self.line_height).ceil() as usize
100                    + 2;
101            let last_visible_line = (first_visible_line + visible_lines_count)
102                .min(visual_lines.len());
103
104            // Load syntax highlighting
105            let syntax_set = SyntaxSet::load_defaults_newlines();
106            let theme_set = ThemeSet::load_defaults();
107            let syntax_theme = &theme_set.themes["base16-ocean.dark"];
108
109            let syntax_ref = match self.syntax.as_str() {
110                "py" | "python" => syntax_set.find_syntax_by_extension("py"),
111                "lua" => syntax_set.find_syntax_by_extension("lua"),
112                "rs" | "rust" => syntax_set.find_syntax_by_extension("rs"),
113                "js" | "javascript" => {
114                    syntax_set.find_syntax_by_extension("js")
115                }
116                "html" | "htm" => syntax_set.find_syntax_by_extension("html"),
117                "xml" | "svg" => syntax_set.find_syntax_by_extension("xml"),
118                "css" => syntax_set.find_syntax_by_extension("css"),
119                "json" => syntax_set.find_syntax_by_extension("json"),
120                "md" | "markdown" => syntax_set.find_syntax_by_extension("md"),
121                _ => Some(syntax_set.find_syntax_plain_text()),
122            };
123
124            // Draw only visible lines (virtual scrolling optimization)
125            for (idx, visual_line) in visual_lines
126                .iter()
127                .enumerate()
128                .skip(first_visible_line)
129                .take(last_visible_line - first_visible_line)
130            {
131                let y = idx as f32 * self.line_height;
132
133                // Note: Gutter background is handled by a container in view.rs
134                // to ensure proper clipping when the pane is resized.
135
136                // Draw line number only for first segment
137                if self.line_numbers_enabled {
138                    if visual_line.is_first_segment() {
139                        let line_num = visual_line.logical_line + 1;
140                        let line_num_text = format!("{}", line_num);
141                        // Calculate actual text width and center in gutter
142                        let text_width = measure_text_width(
143                            &line_num_text,
144                            self.full_char_width,
145                            self.char_width,
146                        );
147                        let x_pos = (self.gutter_width() - text_width) / 2.0;
148                        frame.fill_text(canvas::Text {
149                            content: line_num_text,
150                            position: Point::new(x_pos, y + 2.0),
151                            color: self.style.line_number_color,
152                            size: self.font_size.into(),
153                            font: self.font,
154                            ..canvas::Text::default()
155                        });
156                    } else {
157                        // Draw wrap indicator for continuation lines
158                        frame.fill_text(canvas::Text {
159                            content: "↪".to_string(),
160                            position: Point::new(
161                                self.gutter_width() - 20.0,
162                                y + 2.0,
163                            ),
164                            color: self.style.line_number_color,
165                            size: self.font_size.into(),
166                            font: self.font,
167                            ..canvas::Text::default()
168                        });
169                    }
170                }
171
172                // Highlight current line (based on logical line)
173                if visual_line.logical_line == self.cursor.0 {
174                    frame.fill_rectangle(
175                        Point::new(self.gutter_width(), y),
176                        Size::new(
177                            bounds.width - self.gutter_width(),
178                            self.line_height,
179                        ),
180                        self.style.current_line_highlight,
181                    );
182                }
183
184                // Draw text content with syntax highlighting
185                let full_line_content =
186                    self.buffer.line(visual_line.logical_line);
187
188                // Convert character indices to byte indices for UTF-8 string slicing
189                let start_byte = full_line_content
190                    .char_indices()
191                    .nth(visual_line.start_col)
192                    .map_or(full_line_content.len(), |(idx, _)| idx);
193                let end_byte = full_line_content
194                    .char_indices()
195                    .nth(visual_line.end_col)
196                    .map_or(full_line_content.len(), |(idx, _)| idx);
197                let line_segment = &full_line_content[start_byte..end_byte];
198
199                if let Some(syntax) = syntax_ref {
200                    let mut highlighter =
201                        HighlightLines::new(syntax, syntax_theme);
202
203                    // Highlight the full line to get correct token colors
204                    let full_line_ranges = highlighter
205                        .highlight_line(full_line_content, &syntax_set)
206                        .unwrap_or_else(|_| {
207                            vec![(Style::default(), full_line_content)]
208                        });
209
210                    // Extract only the ranges that fall within our segment
211                    let mut x_offset = self.gutter_width() + 5.0;
212                    let mut char_pos = 0;
213
214                    for (style, text) in full_line_ranges {
215                        let text_len = text.chars().count();
216                        let text_end = char_pos + text_len;
217
218                        // Check if this token intersects with our segment
219                        if text_end > visual_line.start_col
220                            && char_pos < visual_line.end_col
221                        {
222                            // Calculate the intersection
223                            let segment_start =
224                                char_pos.max(visual_line.start_col);
225                            let segment_end = text_end.min(visual_line.end_col);
226
227                            let text_start_offset =
228                                segment_start.saturating_sub(char_pos);
229                            let text_end_offset = text_start_offset
230                                + (segment_end - segment_start);
231
232                            // Convert character offsets to byte offsets for UTF-8 slicing
233                            let start_byte = text
234                                .char_indices()
235                                .nth(text_start_offset)
236                                .map_or(text.len(), |(idx, _)| idx);
237                            let end_byte = text
238                                .char_indices()
239                                .nth(text_end_offset)
240                                .map_or(text.len(), |(idx, _)| idx);
241
242                            let segment_text = &text[start_byte..end_byte];
243
244                            let color = Color::from_rgb(
245                                f32::from(style.foreground.r) / 255.0,
246                                f32::from(style.foreground.g) / 255.0,
247                                f32::from(style.foreground.b) / 255.0,
248                            );
249
250                            frame.fill_text(canvas::Text {
251                                content: segment_text.to_string(),
252                                position: Point::new(x_offset, y + 2.0),
253                                color,
254                                size: self.font_size.into(),
255                                font: self.font,
256                                ..canvas::Text::default()
257                            });
258
259                            x_offset += measure_text_width(
260                                segment_text,
261                                self.full_char_width,
262                                self.char_width,
263                            );
264                        }
265
266                        char_pos = text_end;
267                    }
268                } else {
269                    // Fallback to plain text
270                    frame.fill_text(canvas::Text {
271                        content: line_segment.to_string(),
272                        position: Point::new(
273                            self.gutter_width() + 5.0,
274                            y + 2.0,
275                        ),
276                        color: self.style.text_color,
277                        size: self.font_size.into(),
278                        font: self.font,
279                        ..canvas::Text::default()
280                    });
281                }
282            }
283
284            // Draw search match highlights
285            if self.search_state.is_open && !self.search_state.query.is_empty()
286            {
287                let query_len = self.search_state.query.chars().count();
288
289                for (match_idx, search_match) in
290                    self.search_state.matches.iter().enumerate()
291                {
292                    // Determine if this is the current match
293                    let is_current = self.search_state.current_match_index
294                        == Some(match_idx);
295
296                    let highlight_color = if is_current {
297                        // Orange for current match
298                        Color { r: 1.0, g: 0.6, b: 0.0, a: 0.4 }
299                    } else {
300                        // Yellow for other matches
301                        Color { r: 1.0, g: 1.0, b: 0.0, a: 0.3 }
302                    };
303
304                    // Convert logical position to visual line
305                    let start_visual = WrappingCalculator::logical_to_visual(
306                        &visual_lines,
307                        search_match.line,
308                        search_match.col,
309                    );
310                    let end_visual = WrappingCalculator::logical_to_visual(
311                        &visual_lines,
312                        search_match.line,
313                        search_match.col + query_len,
314                    );
315
316                    if let (Some(start_v), Some(end_v)) =
317                        (start_visual, end_visual)
318                    {
319                        if start_v == end_v {
320                            // Match within same visual line
321                            let y = start_v as f32 * self.line_height;
322                            let vl = &visual_lines[start_v];
323                            let line_content =
324                                self.buffer.line(vl.logical_line);
325
326                            // Use calculate_segment_geometry to compute match position and width
327                            let (x_start, match_width) =
328                                calculate_segment_geometry(
329                                    line_content,
330                                    vl.start_col,
331                                    search_match.col,
332                                    search_match.col + query_len,
333                                    self.gutter_width() + 5.0,
334                                    self.full_char_width,
335                                    self.char_width,
336                                );
337                            let x_end = x_start + match_width;
338
339                            frame.fill_rectangle(
340                                Point::new(x_start, y + 2.0),
341                                Size::new(
342                                    x_end - x_start,
343                                    self.line_height - 4.0,
344                                ),
345                                highlight_color,
346                            );
347                        } else {
348                            // Match spans multiple visual lines
349                            for (v_idx, vl) in visual_lines
350                                .iter()
351                                .enumerate()
352                                .skip(start_v)
353                                .take(end_v - start_v + 1)
354                            {
355                                let y = v_idx as f32 * self.line_height;
356
357                                let match_start_col = search_match.col;
358                                let match_end_col =
359                                    search_match.col + query_len;
360
361                                let sel_start_col = if v_idx == start_v {
362                                    match_start_col
363                                } else {
364                                    vl.start_col
365                                };
366                                let sel_end_col = if v_idx == end_v {
367                                    match_end_col
368                                } else {
369                                    vl.end_col
370                                };
371
372                                let line_content =
373                                    self.buffer.line(vl.logical_line);
374
375                                let (x_start, sel_width) =
376                                    calculate_segment_geometry(
377                                        line_content,
378                                        vl.start_col,
379                                        sel_start_col,
380                                        sel_end_col,
381                                        self.gutter_width() + 5.0,
382                                        self.full_char_width,
383                                        self.char_width,
384                                    );
385                                let x_end = x_start + sel_width;
386
387                                frame.fill_rectangle(
388                                    Point::new(x_start, y + 2.0),
389                                    Size::new(
390                                        x_end - x_start,
391                                        self.line_height - 4.0,
392                                    ),
393                                    highlight_color,
394                                );
395                            }
396                        }
397                    }
398                }
399            }
400
401            // Draw selection highlight
402            if let Some((start, end)) = self.get_selection_range()
403                && start != end
404            {
405                let selection_color = Color { r: 0.3, g: 0.5, b: 0.8, a: 0.3 };
406
407                if start.0 == end.0 {
408                    // Single line selection - need to handle wrapped segments
409                    let start_visual = WrappingCalculator::logical_to_visual(
410                        &visual_lines,
411                        start.0,
412                        start.1,
413                    );
414                    let end_visual = WrappingCalculator::logical_to_visual(
415                        &visual_lines,
416                        end.0,
417                        end.1,
418                    );
419
420                    if let (Some(start_v), Some(end_v)) =
421                        (start_visual, end_visual)
422                    {
423                        if start_v == end_v {
424                            // Selection within same visual line
425                            let y = start_v as f32 * self.line_height;
426                            let vl = &visual_lines[start_v];
427                            let line_content =
428                                self.buffer.line(vl.logical_line);
429
430                            let (x_start, sel_width) =
431                                calculate_segment_geometry(
432                                    line_content,
433                                    vl.start_col,
434                                    start.1,
435                                    end.1,
436                                    self.gutter_width() + 5.0,
437                                    self.full_char_width,
438                                    self.char_width,
439                                );
440                            let x_end = x_start + sel_width;
441
442                            frame.fill_rectangle(
443                                Point::new(x_start, y + 2.0),
444                                Size::new(
445                                    x_end - x_start,
446                                    self.line_height - 4.0,
447                                ),
448                                selection_color,
449                            );
450                        } else {
451                            // Selection spans multiple visual lines (same logical line)
452                            for (v_idx, vl) in visual_lines
453                                .iter()
454                                .enumerate()
455                                .skip(start_v)
456                                .take(end_v - start_v + 1)
457                            {
458                                let y = v_idx as f32 * self.line_height;
459
460                                let sel_start_col = if v_idx == start_v {
461                                    start.1
462                                } else {
463                                    vl.start_col
464                                };
465                                let sel_end_col = if v_idx == end_v {
466                                    end.1
467                                } else {
468                                    vl.end_col
469                                };
470
471                                let line_content =
472                                    self.buffer.line(vl.logical_line);
473
474                                let (x_start, sel_width) =
475                                    calculate_segment_geometry(
476                                        line_content,
477                                        vl.start_col,
478                                        sel_start_col,
479                                        sel_end_col,
480                                        self.gutter_width() + 5.0,
481                                        self.full_char_width,
482                                        self.char_width,
483                                    );
484                                let x_end = x_start + sel_width;
485
486                                frame.fill_rectangle(
487                                    Point::new(x_start, y + 2.0),
488                                    Size::new(
489                                        x_end - x_start,
490                                        self.line_height - 4.0,
491                                    ),
492                                    selection_color,
493                                );
494                            }
495                        }
496                    }
497                } else {
498                    // Multi-line selection
499                    let start_visual = WrappingCalculator::logical_to_visual(
500                        &visual_lines,
501                        start.0,
502                        start.1,
503                    );
504                    let end_visual = WrappingCalculator::logical_to_visual(
505                        &visual_lines,
506                        end.0,
507                        end.1,
508                    );
509
510                    if let (Some(start_v), Some(end_v)) =
511                        (start_visual, end_visual)
512                    {
513                        for (v_idx, vl) in visual_lines
514                            .iter()
515                            .enumerate()
516                            .skip(start_v)
517                            .take(end_v - start_v + 1)
518                        {
519                            let y = v_idx as f32 * self.line_height;
520
521                            let sel_start_col = if vl.logical_line == start.0
522                                && v_idx == start_v
523                            {
524                                start.1
525                            } else {
526                                vl.start_col
527                            };
528
529                            let sel_end_col =
530                                if vl.logical_line == end.0 && v_idx == end_v {
531                                    end.1
532                                } else {
533                                    vl.end_col
534                                };
535
536                            let line_content =
537                                self.buffer.line(vl.logical_line);
538
539                            let (x_start, sel_width) =
540                                calculate_segment_geometry(
541                                    line_content,
542                                    vl.start_col,
543                                    sel_start_col,
544                                    sel_end_col,
545                                    self.gutter_width() + 5.0,
546                                    self.full_char_width,
547                                    self.char_width,
548                                );
549                            let x_end = x_start + sel_width;
550
551                            frame.fill_rectangle(
552                                Point::new(x_start, y + 2.0),
553                                Size::new(
554                                    x_end - x_start,
555                                    self.line_height - 4.0,
556                                ),
557                                selection_color,
558                            );
559                        }
560                    }
561                }
562            }
563
564            // Cursor drawing logic (only when the editor has focus)
565            // -------------------------------------------------------------------------
566            // Core notes:
567            // 1. Choose the drawing path based on whether IME preedit is present.
568            // 2. Require both `is_focused()` (Iced focus) and `has_canvas_focus()` (internal focus)
569            //    so the cursor is drawn only in the active editor, avoiding multiple cursors.
570            // 3. Use `WrappingCalculator` to map logical (line, col) to visual (x, y)
571            //    for correct cursor positioning with line wrapping.
572            // -------------------------------------------------------------------------
573            if self.show_cursor
574                && self.cursor_visible
575                && self.is_focused()
576                && self.has_canvas_focus
577                && self.ime_preedit.is_some()
578            {
579                // [Branch A] IME preedit rendering mode
580                // ---------------------------------------------------------------------
581                // When the user is composing with an IME (e.g. pinyin before commit),
582                // draw a preedit region instead of the normal caret, including:
583                // - preedit background (highlighting the composing text)
584                // - preedit text content (preedit.content)
585                // - preedit selection (underline or selection background)
586                // - preedit caret
587                // ---------------------------------------------------------------------
588                if let Some(cursor_visual) =
589                    WrappingCalculator::logical_to_visual(
590                        &visual_lines,
591                        self.cursor.0,
592                        self.cursor.1,
593                    )
594                {
595                    let vl = &visual_lines[cursor_visual];
596                    let line_content = self.buffer.line(vl.logical_line);
597
598                    // Compute the preedit region start X
599                    // Use calculate_segment_geometry to ensure correct CJK width handling
600                    let (cursor_x, _) = calculate_segment_geometry(
601                        line_content,
602                        vl.start_col,
603                        self.cursor.1,
604                        self.cursor.1,
605                        self.gutter_width() + 5.0,
606                        self.full_char_width,
607                        self.char_width,
608                    );
609                    let cursor_y = cursor_visual as f32 * self.line_height;
610
611                    if let Some(preedit) = self.ime_preedit.as_ref() {
612                        let preedit_width = measure_text_width(
613                            &preedit.content,
614                            self.full_char_width,
615                            self.char_width,
616                        );
617
618                        // 1. Draw preedit background (light translucent)
619                        // This indicates the text is not committed yet
620                        frame.fill_rectangle(
621                            Point::new(cursor_x, cursor_y + 2.0),
622                            Size::new(preedit_width, self.line_height - 4.0),
623                            Color { r: 1.0, g: 1.0, b: 1.0, a: 0.08 },
624                        );
625
626                        // 2. Draw preedit selection (if any)
627                        // IME may mark a selection inside preedit text (e.g. segmentation)
628                        // The range uses UTF-8 byte indices, so slices must be safe
629                        if let Some(range) = preedit.selection.as_ref()
630                            && range.start != range.end
631                        {
632                            // Validate indices before slicing to prevent panic
633                            if let Some((start, end)) =
634                                validate_selection_indices(
635                                    &preedit.content,
636                                    range.start,
637                                    range.end,
638                                )
639                            {
640                                let selected_prefix = &preedit.content[..start];
641                                let selected_text =
642                                    &preedit.content[start..end];
643
644                                let selection_x = cursor_x
645                                    + measure_text_width(
646                                        selected_prefix,
647                                        self.full_char_width,
648                                        self.char_width,
649                                    );
650                                let selection_w = measure_text_width(
651                                    selected_text,
652                                    self.full_char_width,
653                                    self.char_width,
654                                );
655
656                                frame.fill_rectangle(
657                                    Point::new(selection_x, cursor_y + 2.0),
658                                    Size::new(
659                                        selection_w,
660                                        self.line_height - 4.0,
661                                    ),
662                                    Color { r: 0.3, g: 0.5, b: 0.8, a: 0.3 },
663                                );
664                            }
665                        }
666
667                        // 3. Draw preedit text itself
668                        frame.fill_text(canvas::Text {
669                            content: preedit.content.clone(),
670                            position: Point::new(cursor_x, cursor_y + 2.0),
671                            color: self.style.text_color,
672                            size: self.font_size.into(),
673                            font: self.font,
674                            ..canvas::Text::default()
675                        });
676
677                        // 4. Draw bottom underline (IME state indicator)
678                        frame.fill_rectangle(
679                            Point::new(
680                                cursor_x,
681                                cursor_y + self.line_height - 3.0,
682                            ),
683                            Size::new(preedit_width, 1.0),
684                            self.style.text_color,
685                        );
686
687                        // 5. Draw preedit caret
688                        // If IME provides a caret position (usually selection end), draw a thin bar
689                        if let Some(range) = preedit.selection.as_ref() {
690                            let caret_end =
691                                range.end.min(preedit.content.len());
692
693                            // Validate caret position to avoid panic on invalid UTF-8 boundary
694                            if caret_end <= preedit.content.len()
695                                && preedit.content.is_char_boundary(caret_end)
696                            {
697                                let caret_prefix =
698                                    &preedit.content[..caret_end];
699                                let caret_x = cursor_x
700                                    + measure_text_width(
701                                        caret_prefix,
702                                        self.full_char_width,
703                                        self.char_width,
704                                    );
705
706                                frame.fill_rectangle(
707                                    Point::new(caret_x, cursor_y + 2.0),
708                                    Size::new(2.0, self.line_height - 4.0),
709                                    self.style.text_color,
710                                );
711                            }
712                        }
713                    }
714                }
715            } else if self.show_cursor
716                && self.cursor_visible
717                && self.is_focused()
718                && self.has_canvas_focus
719            {
720                // [Branch B] Normal caret rendering mode
721                // ---------------------------------------------------------------------
722                // When there is no IME preedit, draw the standard editor caret.
723                // Key checks:
724                // - is_focused(): the widget has Iced focus
725                // - has_canvas_focus: internal focus state (mouse clicks, etc.)
726                // - draw only when both are true to avoid ghost cursors
727                // ---------------------------------------------------------------------
728
729                // Map logical cursor position (Line, Col) to visual line index
730                // to handle line wrapping changes
731                if let Some(cursor_visual) =
732                    WrappingCalculator::logical_to_visual(
733                        &visual_lines,
734                        self.cursor.0,
735                        self.cursor.1,
736                    )
737                {
738                    let vl = &visual_lines[cursor_visual];
739                    let line_content = self.buffer.line(vl.logical_line);
740
741                    // Compute exact caret X position
742                    // Account for gutter width, left padding, and rendered prefix width
743                    let (cursor_x, _) = calculate_segment_geometry(
744                        line_content,
745                        vl.start_col,
746                        self.cursor.1,
747                        self.cursor.1,
748                        self.gutter_width() + 5.0,
749                        self.full_char_width,
750                        self.char_width,
751                    );
752                    let cursor_y = cursor_visual as f32 * self.line_height;
753
754                    // Draw standard caret (2px vertical bar)
755                    frame.fill_rectangle(
756                        Point::new(cursor_x, cursor_y + 2.0),
757                        Size::new(2.0, self.line_height - 4.0),
758                        self.style.text_color,
759                    );
760                }
761            }
762        });
763
764        vec![geometry]
765    }
766
767    fn update(
768        &self,
769        _state: &mut Self::State,
770        event: &Event,
771        bounds: Rectangle,
772        cursor: mouse::Cursor,
773    ) -> Option<Action<Message>> {
774        match event {
775            Event::Keyboard(keyboard::Event::KeyPressed {
776                key,
777                modifiers,
778                text,
779                ..
780            }) => {
781                // Only process keyboard events if this editor has focus
782                let focused_id = super::FOCUSED_EDITOR_ID
783                    .load(std::sync::atomic::Ordering::Relaxed);
784                if focused_id != self.editor_id {
785                    return None;
786                }
787
788                // Cursor outside canvas bounds
789                if !is_cursor_in_bounds(&cursor, bounds) {
790                    return None;
791                }
792
793                // Only process keyboard events if canvas has focus
794                if !self.has_canvas_focus {
795                    return None;
796                }
797
798                if self.ime_preedit.is_some()
799                    && !(modifiers.control() || modifiers.command())
800                {
801                    return None;
802                }
803
804                // Handle Ctrl+C / Ctrl+Insert (copy)
805                if (modifiers.control()
806                    && matches!(key, keyboard::Key::Character(c) if c.as_str() == "c"))
807                    || (modifiers.control()
808                        && matches!(
809                            key,
810                            keyboard::Key::Named(keyboard::key::Named::Insert)
811                        ))
812                {
813                    return Some(Action::publish(Message::Copy).and_capture());
814                }
815
816                // Handle Ctrl+Z (undo)
817                if modifiers.control()
818                    && matches!(key, keyboard::Key::Character(z) if z.as_str() == "z")
819                {
820                    return Some(Action::publish(Message::Undo).and_capture());
821                }
822
823                // Handle Ctrl+Y (redo)
824                if modifiers.control()
825                    && matches!(key, keyboard::Key::Character(y) if y.as_str() == "y")
826                {
827                    return Some(Action::publish(Message::Redo).and_capture());
828                }
829
830                // Handle Ctrl+F (open search)
831                if modifiers.control()
832                    && matches!(key, keyboard::Key::Character(f) if f.as_str() == "f")
833                    && self.search_replace_enabled
834                {
835                    return Some(
836                        Action::publish(Message::OpenSearch).and_capture(),
837                    );
838                }
839
840                // Handle Ctrl+H (open search and replace)
841                if modifiers.control()
842                    && matches!(key, keyboard::Key::Character(h) if h.as_str() == "h")
843                    && self.search_replace_enabled
844                {
845                    return Some(
846                        Action::publish(Message::OpenSearchReplace)
847                            .and_capture(),
848                    );
849                }
850
851                // Handle Escape (close search dialog if open)
852                if matches!(
853                    key,
854                    keyboard::Key::Named(keyboard::key::Named::Escape)
855                ) {
856                    return Some(
857                        Action::publish(Message::CloseSearch).and_capture(),
858                    );
859                }
860
861                // Handle Tab (cycle forward in search dialog if open)
862                if matches!(
863                    key,
864                    keyboard::Key::Named(keyboard::key::Named::Tab)
865                ) && self.search_state.is_open
866                {
867                    if modifiers.shift() {
868                        // Shift+Tab: cycle backward
869                        return Some(
870                            Action::publish(Message::SearchDialogShiftTab)
871                                .and_capture(),
872                        );
873                    } else {
874                        // Tab: cycle forward
875                        return Some(
876                            Action::publish(Message::SearchDialogTab)
877                                .and_capture(),
878                        );
879                    }
880                }
881
882                // Handle F3 (find next) and Shift+F3 (find previous)
883                if matches!(key, keyboard::Key::Named(keyboard::key::Named::F3))
884                    && self.search_replace_enabled
885                {
886                    if modifiers.shift() {
887                        return Some(
888                            Action::publish(Message::FindPrevious)
889                                .and_capture(),
890                        );
891                    } else {
892                        return Some(
893                            Action::publish(Message::FindNext).and_capture(),
894                        );
895                    }
896                }
897
898                // Handle Ctrl+V / Shift+Insert (paste) - read clipboard and send paste message
899                if (modifiers.control()
900                    && matches!(key, keyboard::Key::Character(v) if v.as_str() == "v"))
901                    || (modifiers.shift()
902                        && matches!(
903                            key,
904                            keyboard::Key::Named(keyboard::key::Named::Insert)
905                        ))
906                {
907                    // Return an action that requests clipboard read
908                    return Some(Action::publish(
909                        Message::Paste(String::new()),
910                    ));
911                }
912
913                // Handle Ctrl+Home (go to start of document)
914                if modifiers.control()
915                    && matches!(
916                        key,
917                        keyboard::Key::Named(keyboard::key::Named::Home)
918                    )
919                {
920                    return Some(
921                        Action::publish(Message::CtrlHome).and_capture(),
922                    );
923                }
924
925                // Handle Ctrl+End (go to end of document)
926                if modifiers.control()
927                    && matches!(
928                        key,
929                        keyboard::Key::Named(keyboard::key::Named::End)
930                    )
931                {
932                    return Some(
933                        Action::publish(Message::CtrlEnd).and_capture(),
934                    );
935                }
936
937                // Handle Shift+Delete (delete selection)
938                if modifiers.shift()
939                    && matches!(
940                        key,
941                        keyboard::Key::Named(keyboard::key::Named::Delete)
942                    )
943                {
944                    return Some(
945                        Action::publish(Message::DeleteSelection).and_capture(),
946                    );
947                }
948
949                // PRIORITY 1: Check if 'text' field has valid printable character
950                // This handles:
951                // - Numpad keys with NumLock ON (key=Named(ArrowDown), text=Some("2"))
952                // - Regular typing with shift, accents, international layouts
953                if let Some(text_content) = text
954                    && !text_content.is_empty()
955                    && !modifiers.control()
956                    && !modifiers.alt()
957                {
958                    // Check if it's a printable character (not a control character)
959                    // This filters out Enter (\n), Tab (\t), Delete (U+007F), etc.
960                    if let Some(first_char) = text_content.chars().next()
961                        && !first_char.is_control()
962                    {
963                        return Some(
964                            Action::publish(Message::CharacterInput(
965                                first_char,
966                            ))
967                            .and_capture(),
968                        );
969                    }
970                }
971
972                // PRIORITY 2: Handle special named keys (navigation, editing)
973                // These are only processed if text didn't contain a printable character
974                let message = match key {
975                    keyboard::Key::Named(keyboard::key::Named::Backspace) => {
976                        Some(Message::Backspace)
977                    }
978                    keyboard::Key::Named(keyboard::key::Named::Delete) => {
979                        Some(Message::Delete)
980                    }
981                    keyboard::Key::Named(keyboard::key::Named::Enter) => {
982                        Some(Message::Enter)
983                    }
984                    keyboard::Key::Named(keyboard::key::Named::Tab) => {
985                        // Insert 4 spaces for Tab
986                        Some(Message::Tab)
987                    }
988                    keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
989                        Some(Message::ArrowKey(
990                            ArrowDirection::Up,
991                            modifiers.shift(),
992                        ))
993                    }
994                    keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
995                        Some(Message::ArrowKey(
996                            ArrowDirection::Down,
997                            modifiers.shift(),
998                        ))
999                    }
1000                    keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
1001                        Some(Message::ArrowKey(
1002                            ArrowDirection::Left,
1003                            modifiers.shift(),
1004                        ))
1005                    }
1006                    keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
1007                        Some(Message::ArrowKey(
1008                            ArrowDirection::Right,
1009                            modifiers.shift(),
1010                        ))
1011                    }
1012                    keyboard::Key::Named(keyboard::key::Named::PageUp) => {
1013                        Some(Message::PageUp)
1014                    }
1015                    keyboard::Key::Named(keyboard::key::Named::PageDown) => {
1016                        Some(Message::PageDown)
1017                    }
1018                    keyboard::Key::Named(keyboard::key::Named::Home) => {
1019                        Some(Message::Home(modifiers.shift()))
1020                    }
1021                    keyboard::Key::Named(keyboard::key::Named::End) => {
1022                        Some(Message::End(modifiers.shift()))
1023                    }
1024                    // PRIORITY 3: Fallback to extracting from 'key' if text was empty/control char
1025                    // This handles edge cases where text field is not populated
1026                    _ => {
1027                        if !modifiers.control()
1028                            && !modifiers.alt()
1029                            && let keyboard::Key::Character(c) = key
1030                            && !c.is_empty()
1031                        {
1032                            return c
1033                                .chars()
1034                                .next()
1035                                .map(Message::CharacterInput)
1036                                .map(|msg| Action::publish(msg).and_capture());
1037                        }
1038                        None
1039                    }
1040                };
1041
1042                message.map(|msg| Action::publish(msg).and_capture())
1043            }
1044            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
1045                cursor.position_in(bounds).map(|position| {
1046                    // Don't capture the event so it can bubble up for focus management
1047                    Action::publish(Message::MouseClick(position))
1048                })
1049            }
1050            Event::Mouse(mouse::Event::CursorMoved { .. }) => {
1051                // Handle mouse drag for selection only when cursor is within bounds
1052                cursor.position_in(bounds).map(|position| {
1053                    Action::publish(Message::MouseDrag(position)).and_capture()
1054                })
1055            }
1056            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
1057                // Only handle mouse release when cursor is within bounds
1058                // This prevents capturing events meant for other widgets
1059                if cursor.is_over(bounds) {
1060                    Some(Action::publish(Message::MouseRelease).and_capture())
1061                } else {
1062                    None
1063                }
1064            }
1065            Event::InputMethod(event) => {
1066                let focused_id = super::FOCUSED_EDITOR_ID
1067                    .load(std::sync::atomic::Ordering::Relaxed);
1068                if focused_id != self.editor_id {
1069                    return None;
1070                }
1071
1072                if !is_cursor_in_bounds(&cursor, bounds) {
1073                    return None;
1074                }
1075
1076                if !self.has_canvas_focus {
1077                    return None;
1078                }
1079
1080                // IME event handling
1081                // ---------------------------------------------------------------------
1082                // Core mapping: convert Iced IME events into editor Messages
1083                //
1084                // Flow:
1085                // 1. Opened: IME activated (e.g. switching input method). Clear old preedit state.
1086                // 2. Preedit: User is composing (e.g. typing "nihao" before commit).
1087                //    - content: current candidate text
1088                //    - selection: selection range within the text, in bytes
1089                // 3. Commit: User confirms a candidate and commits text into the buffer.
1090                // 4. Closed: IME closed or lost focus.
1091                //
1092                // Safety checks:
1093                // - handle only when `focused_id` matches this editor ID
1094                // - handle only when `has_canvas_focus` is true
1095                // This ensures IME events are not delivered to the wrong widget.
1096                // ---------------------------------------------------------------------
1097                let message = match event {
1098                    input_method::Event::Opened => Message::ImeOpened,
1099                    input_method::Event::Preedit(content, selection) => {
1100                        Message::ImePreedit(content.clone(), selection.clone())
1101                    }
1102                    input_method::Event::Commit(content) => {
1103                        Message::ImeCommit(content.clone())
1104                    }
1105                    input_method::Event::Closed => Message::ImeClosed,
1106                };
1107
1108                Some(Action::publish(message).and_capture())
1109            }
1110            _ => None,
1111        }
1112    }
1113}
1114
1115/// Validates that the selection indices fall on valid UTF-8 character boundaries
1116/// to prevent panics during string slicing.
1117///
1118/// # Arguments
1119///
1120/// * `content` - The string content to check against
1121/// * `start` - The start byte index
1122/// * `end` - The end byte index
1123///
1124/// # Returns
1125///
1126/// `Some((start, end))` if indices are valid, `None` otherwise.
1127fn validate_selection_indices(
1128    content: &str,
1129    start: usize,
1130    end: usize,
1131) -> Option<(usize, usize)> {
1132    let len = content.len();
1133    // Clamp indices to content length
1134    let start = start.min(len);
1135    let end = end.min(len);
1136
1137    // Ensure start is not greater than end
1138    if start > end {
1139        return None;
1140    }
1141
1142    // Verify that indices fall on valid UTF-8 character boundaries
1143    if content.is_char_boundary(start) && content.is_char_boundary(end) {
1144        Some((start, end))
1145    } else {
1146        None
1147    }
1148}
1149
1150#[cfg(test)]
1151mod tests {
1152    use super::*;
1153    use crate::canvas_editor::{CHAR_WIDTH, FONT_SIZE, compare_floats};
1154    use std::cmp::Ordering;
1155
1156    #[test]
1157    fn test_calculate_segment_geometry_ascii() {
1158        // "Hello World"
1159        // "Hello " (6 chars) -> prefix
1160        // "World" (5 chars) -> segment
1161        // width("Hello ") = 6 * CHAR_WIDTH
1162        // width("World") = 5 * CHAR_WIDTH
1163        let content = "Hello World";
1164        let (x, w) = calculate_segment_geometry(
1165            content, 0, 6, 11, 0.0, FONT_SIZE, CHAR_WIDTH,
1166        );
1167
1168        let expected_x = CHAR_WIDTH * 6.0;
1169        let expected_w = CHAR_WIDTH * 5.0;
1170
1171        assert_eq!(
1172            compare_floats(x, expected_x),
1173            Ordering::Equal,
1174            "X position mismatch for ASCII"
1175        );
1176        assert_eq!(
1177            compare_floats(w, expected_w),
1178            Ordering::Equal,
1179            "Width mismatch for ASCII"
1180        );
1181    }
1182
1183    #[test]
1184    fn test_calculate_segment_geometry_cjk() {
1185        // "你好世界"
1186        // "你好" (2 chars) -> prefix
1187        // "世界" (2 chars) -> segment
1188        // width("你好") = 2 * FONT_SIZE
1189        // width("世界") = 2 * FONT_SIZE
1190        let content = "你好世界";
1191        let (x, w) = calculate_segment_geometry(
1192            content, 0, 2, 4, 10.0, FONT_SIZE, CHAR_WIDTH,
1193        );
1194
1195        let expected_x = 10.0 + FONT_SIZE * 2.0;
1196        let expected_w = FONT_SIZE * 2.0;
1197
1198        assert_eq!(
1199            compare_floats(x, expected_x),
1200            Ordering::Equal,
1201            "X position mismatch for CJK"
1202        );
1203        assert_eq!(
1204            compare_floats(w, expected_w),
1205            Ordering::Equal,
1206            "Width mismatch for CJK"
1207        );
1208    }
1209
1210    #[test]
1211    fn test_calculate_segment_geometry_mixed() {
1212        // "Hi你好"
1213        // "Hi" (2 chars) -> prefix
1214        // "你好" (2 chars) -> segment
1215        // width("Hi") = 2 * CHAR_WIDTH
1216        // width("你好") = 2 * FONT_SIZE
1217        let content = "Hi你好";
1218        let (x, w) = calculate_segment_geometry(
1219            content, 0, 2, 4, 0.0, FONT_SIZE, CHAR_WIDTH,
1220        );
1221
1222        let expected_x = CHAR_WIDTH * 2.0;
1223        let expected_w = FONT_SIZE * 2.0;
1224
1225        assert_eq!(
1226            compare_floats(x, expected_x),
1227            Ordering::Equal,
1228            "X position mismatch for mixed content"
1229        );
1230        assert_eq!(
1231            compare_floats(w, expected_w),
1232            Ordering::Equal,
1233            "Width mismatch for mixed content"
1234        );
1235    }
1236
1237    #[test]
1238    fn test_calculate_segment_geometry_empty_range() {
1239        let content = "Hello";
1240        let (x, w) = calculate_segment_geometry(
1241            content, 0, 0, 0, 0.0, FONT_SIZE, CHAR_WIDTH,
1242        );
1243        assert!((x - 0.0).abs() < f32::EPSILON);
1244        assert!((w - 0.0).abs() < f32::EPSILON);
1245    }
1246
1247    #[test]
1248    fn test_calculate_segment_geometry_with_visual_offset() {
1249        // content: "0123456789"
1250        // visual_start_col: 2 (starts at '2')
1251        // segment: "34" (indices 3 to 5)
1252        // prefix: from visual start (2) to segment start (3) -> "2" (length 1)
1253        // prefix width: 1 * CHAR_WIDTH
1254        // segment width: 2 * CHAR_WIDTH
1255        let content = "0123456789";
1256        let (x, w) = calculate_segment_geometry(
1257            content, 2, 3, 5, 5.0, FONT_SIZE, CHAR_WIDTH,
1258        );
1259
1260        let expected_x = 5.0 + CHAR_WIDTH * 1.0;
1261        let expected_w = CHAR_WIDTH * 2.0;
1262
1263        assert_eq!(
1264            compare_floats(x, expected_x),
1265            Ordering::Equal,
1266            "X position mismatch with visual offset"
1267        );
1268        assert_eq!(
1269            compare_floats(w, expected_w),
1270            Ordering::Equal,
1271            "Width mismatch with visual offset"
1272        );
1273    }
1274
1275    #[test]
1276    fn test_calculate_segment_geometry_out_of_bounds() {
1277        // Content length is 5 ("Hello")
1278        // Request start at 10, end at 15
1279        // visual_start 0
1280        // Prefix should consume whole string ("Hello") and stop.
1281        // Segment should be empty.
1282        let content = "Hello";
1283        let (x, w) = calculate_segment_geometry(
1284            content, 0, 10, 15, 0.0, FONT_SIZE, CHAR_WIDTH,
1285        );
1286
1287        let expected_x = CHAR_WIDTH * 5.0; // Width of "Hello"
1288        let expected_w = 0.0;
1289
1290        assert_eq!(
1291            compare_floats(x, expected_x),
1292            Ordering::Equal,
1293            "X position mismatch for out of bounds start"
1294        );
1295        assert!(
1296            (w - expected_w).abs() < f32::EPSILON,
1297            "Width should be 0 for out of bounds segment"
1298        );
1299    }
1300
1301    #[test]
1302    fn test_calculate_segment_geometry_special_chars() {
1303        // Emoji "👋" (width > 1 => FONT_SIZE)
1304        // Tab "\t" (width None => 0.0)
1305        let content = "A👋\tB";
1306        // Measure "👋" (index 1 to 2)
1307        // Indices in chars: 'A' (0), '👋' (1), '\t' (2), 'B' (3)
1308
1309        // Segment covering Emoji
1310        let (x, w) = calculate_segment_geometry(
1311            content, 0, 1, 2, 0.0, FONT_SIZE, CHAR_WIDTH,
1312        );
1313        let expected_x_emoji = CHAR_WIDTH; // 'A'
1314        let expected_w_emoji = FONT_SIZE; // '👋'
1315
1316        assert_eq!(
1317            compare_floats(x, expected_x_emoji),
1318            Ordering::Equal,
1319            "X pos for emoji"
1320        );
1321        assert_eq!(
1322            compare_floats(w, expected_w_emoji),
1323            Ordering::Equal,
1324            "Width for emoji"
1325        );
1326
1327        // Segment covering Tab
1328        let (x_tab, w_tab) = calculate_segment_geometry(
1329            content, 0, 2, 3, 0.0, FONT_SIZE, CHAR_WIDTH,
1330        );
1331        let expected_x_tab = CHAR_WIDTH + FONT_SIZE; // 'A' + '👋'
1332        let expected_w_tab = 0.0; // Tab width is 0 in this implementation
1333
1334        assert_eq!(
1335            compare_floats(x_tab, expected_x_tab),
1336            Ordering::Equal,
1337            "X pos for tab"
1338        );
1339        assert_eq!(
1340            compare_floats(w_tab, expected_w_tab),
1341            Ordering::Equal,
1342            "Width for tab"
1343        );
1344    }
1345
1346    #[test]
1347    fn test_calculate_segment_geometry_inverted_range() {
1348        // Start 5, End 3
1349        // Should result in empty segment at start 5
1350        let content = "0123456789";
1351        let (x, w) = calculate_segment_geometry(
1352            content, 0, 5, 3, 0.0, FONT_SIZE, CHAR_WIDTH,
1353        );
1354
1355        let expected_x = CHAR_WIDTH * 5.0;
1356        let expected_w = 0.0;
1357
1358        assert_eq!(
1359            compare_floats(x, expected_x),
1360            Ordering::Equal,
1361            "X pos for inverted range"
1362        );
1363        assert!(
1364            (w - expected_w).abs() < f32::EPSILON,
1365            "Width for inverted range"
1366        );
1367    }
1368
1369    #[test]
1370    fn test_validate_selection_indices() {
1371        // Test valid ASCII indices
1372        let content = "Hello";
1373        assert_eq!(validate_selection_indices(content, 0, 5), Some((0, 5)));
1374        assert_eq!(validate_selection_indices(content, 1, 3), Some((1, 3)));
1375
1376        // Test valid multi-byte indices (Chinese "你好")
1377        // "你" is 3 bytes (0-3), "好" is 3 bytes (3-6)
1378        let content = "你好";
1379        assert_eq!(validate_selection_indices(content, 0, 6), Some((0, 6)));
1380        assert_eq!(validate_selection_indices(content, 0, 3), Some((0, 3)));
1381        assert_eq!(validate_selection_indices(content, 3, 6), Some((3, 6)));
1382
1383        // Test invalid indices (splitting multi-byte char)
1384        assert_eq!(validate_selection_indices(content, 1, 3), None); // Split first char
1385        assert_eq!(validate_selection_indices(content, 0, 4), None); // Split second char
1386
1387        // Test out of bounds (should be clamped if on boundary, but here len is 6)
1388        // If we pass start=0, end=100 -> clamped to 0, 6. 6 is boundary.
1389        assert_eq!(validate_selection_indices(content, 0, 100), Some((0, 6)));
1390
1391        // Test inverted range
1392        assert_eq!(validate_selection_indices(content, 3, 0), None);
1393    }
1394}